第 10章 Java的线程处理
第 10章 Java的线程处理
10.1 线程的基本概念
10.2 线程的属性
10.3 线程组
10.4 多线程程序的开发
第 10章 Java的线程处理
10.1 线程的基本概念
我们已经对多任务非常熟悉, Windows和 Linux都
是多任务的操作系统 。 这些操作系统可以同时运行两
个或两个以上的程序, 并且看起来这些程序似乎在同
时运行 。 当然, 除非你的计算机拥有多个处理器, 否
则这些程序是不可能同时运行的 。 操作系统负责把系
统资源分配给这些运行中的程序, 并让人感觉它们是
并发活动的 。 图 10.1显示了支持多任务的操作系统和不
支持多任务的操作系统运行程序的情况 。
第 10章 Java的线程处理
图 10.1
P r o c e s s
P r o c e s s
P r o c e s s
时间
支持多任务的系统
P r o c e s s
P r o c e s s
P r o c e s s
时间
不支持多任务的系统
第 10章 Java的线程处理
实现多任务通常有两种方法,一种称为抢占式多
任务 (preemptive multitasking);一种叫合作式多任务
(cooperative multitasking)。对于抢占式多任务,操作系
统自行决定何时中断一个程序,将执行时间分给其他
程序。相反,对于合作式多任务操作系统将与程序进
行协商,只有程序自愿放弃控制时才被中断。虽然抢
占式多任务实现起来困难一些,但却有效得多。对于
合作式多任务来说,一个运行不好的程序会占有整个
系统。
第 10章 Java的线程处理
多线程把操作系统的多任务原理应用到程序中,
进一步发展了这一原理 。 应用了多线程技术的程序如
同多任务操作系统一样, 可以同时执行多个任务 。 每
个任务被称为一个线程 —— 它是线程控制流的简称 。
实际上, 多线程的应用非常广泛, 例如, 浏览器在下
载数据的同时还可以浏览其他网页, 或者当某个网页
下载太慢时, 还可以控制浏览器中止这个网页浏览 。
Java语言本身也使用一个线程在后台收集无用的内存
单元 —— 这样就减少了用户管理内存的麻烦 !
第 10章 Java的线程处理
通常,我们把操作系统的多个任务称为进程
(Process),而程序中的多任务则称为线程。那么,线程
和进程之间有什么区别呢?最基本的区别就是每个进
程都拥有一组完整的属于自己的变量,而线程则共享
这些数据。看起来这样似乎不如进程安全,确实如此,
本章后面将会更详细地讨论。但线程的优势在于创建
和注销线程的开销比运行新的进程少得多,所以现在
主流的操作系统都支持多线程。而且,和进程间的通
信相比,线程间的通信要快得多,也方便得多。
第 10章 Java的线程处理
10.1.1 线程
不少程序语言都提供对线程的支持, 同这些语言
相比, Java的特点是从最底层开始就对线程提供支持 。
除此以外, 标准的 Java类是可重载的, 它允许在一个
给定的应用程序中由多个线程调用同一方法, 而线程
彼此之间又互不干扰 。 Java的这些特点为多线程应用
程序的设计奠定了基础 。
究竟什么是线程呢? 正如图 10.2中所示, 一个线程
是给定的指令的序列 (你所编写的代码 ),一个栈 (在给
定的方法中定义的变量 ),以及一些共享数据 (类一级的
变量 )。 线程也可以从全局类中访问静态数据 。
第 10章 Java的线程处理
图 10.2
J a v a V i r t u a l Ma c h i n e
A p p l e t o r A p p l i c a t i o n
G l o b a l
D a t a
T h r e a d A
P r o g, C n t r,
L o c a l S t a c k
T h r e a d B
P r o g, C n t r,
L o c a l S t a c k
T h r e a d N
P r o g, C n t r,
L o c a l S t a c k

第 10章 Java的线程处理
每个线程都有其自己的堆栈和程序计数器 (PC)。用
户可以把程序计数器 (PC)设想为用于跟踪线程正在执
行的指令,而堆栈用于跟踪线程的上下文 (上下文是当
线程执行到某处时,当前的局部变量的值 )。虽然用户
可以编写出在线程之间传送数据的子程序,但在正常
情况下,一个线程不能访问另外一个线程的栈变量。
第 10章 Java的线程处理
一个线程或执行上下文由三个主要部分组成:
① 一个虚拟处理机
② CPU执行的代码
③ 代码操作的数据
代码可以或不可以由多个线程共享, 这和数据是
独立的 。 两个线程如果执行同一个类的实例代码, 则
它们可以共享相同的代码 。
第 10章 Java的线程处理
类似地, 数据可以或不可以由多个线程共享, 这
和代码是独立的 。 两个线程如果共享对一个公共对象
的存取, 则它们可以共享相同的数据 。
在 Java编程中, 虚拟处理机封装在 Thread类的一个
实例里 。 构造线程时, 定义其上下文的代码和数据是
由传递给它的构造函数的对象指定的 。
第 10章 Java的线程处理
10.1.2 创建线程
在 Java平台中, 创建一个线程非常简单, 最直接的
方法就是从线程类 java.lang.Thread继承 。 在缺省情况下,
线程类可以被所有的 Java应用程序调用 。 为了使用线
程类, 我们需要了解 The java.lang.Thread 类中定义的五
个方法:
● run():该方法用于线程的执行 。 你需要重载该方法,
以便让线程做特定的工作 。
● start():该方法使得线程启动 run()方法 。
第 10章 Java的线程处理
● stop():该方法同 start()方法的作用相反, 用于停止线
程的运行 。
● suspend():该方法同 stop()方法不同的是, 它并不终止
未完成的线程, 而只是挂起线程, 以后还可恢复 。
● resume():该方法重新启动已经挂起的线程 。
下面我们看一个通过派生 Thread类来创建线程的实例 。
第 10章 Java的线程处理
例 10.1 TestThreads.java
public class TestThreads
{
public static void main (String args [])
{
MyThread a = new MyThread("Thread A");
MyThread b = new MyThread("Thread B");
MyThread c = new MyThread("Thread C");
a.start();
b.start();
c.start();
第 10章 Java的线程处理
}
}
class MyThread extends Thread
{
String which;
MyThread (String which)
{
this.which = which;
}
public void run()
第 10章 Java的线程处理
{
int iterations = (int)(Math.random()*100) %15;
int sleepinterval = (int)(Math.random()*1000);
System.out.println(which + " running for " +
iterations +" iterations");
System.out.println(which + " sleeping for " +
sleepinterval + "ms between loops");
for (int i = 0; i < iterations; i++)
{
System.out.println(which +" " + i);
try
{
第 10章 Java的线程处理
Thread.sleep(sleepinterval);
}
catch (InterruptedException e)
{}
}
}
}
这个例子演示了如何从现有的 Thread类中派生出一个新类。
第 10章 Java的线程处理
新创建的类重载了 run()方法, 但实现 run()方法不
必很严格, 因为 Thread类可提供一个缺省的 run()方法,
尽管它不是特别有用 。 其运行结果如下:
Thread A running for 2 iterations
Thread A sleeping for 913ms between loops
Thread A 0
Thread B running for 12 iterations
Thread B sleeping for 575ms between loops
Thread B 0
第 10章 Java的线程处理
Thread C running for 4 iterations
Thread C sleeping for 370ms between
loops
Thread C 0
Thread C 1
Thread B 1
Thread C 2
Thread A 1
Thread C 3
Thread B 2
Thread B 3
第 10章 Java的线程处理
Thread B 4
Thread B 5
Thread B 6
Thread B 7
Thread B 8
Thread B 9
Thread B 10
Thread B 11
第 10章 Java的线程处理
10.1.3 使用 Runnable接口
在不少场合, 你不能重新定义类的父母, 或者不能
定义派生的线程类, 但也许你的类的层次要求父类为特
定的类, 然而, Java语言是不支持多父类的 。 在这些情
况下, 可以通过 Runnable接口来实现多线程的功能 。
实际上, Thread类本身也实现了 Runnable接口 。 一
个 Runnable接口提供了一个 public void run()方法 。 下面
我们来看一个用 Runnable接口创建线程的实例 。
第 10章 Java的线程处理
例 10.2 RunnableTest.java
public class RunnableTest
{
public static void main(String args[])
{
Test r = new Test();
Thread t = new Thread(r);
t.start();
}
}
第 10章 Java的线程处理
class Test implements Runnable
{
int i;
public void run()
{
while (true)
{
System.out.println("Hello " + i++);
if (i == 10)
break;
第 10章 Java的线程处理
}
}
}
上面程序的运行结果非常简单, 这里不再列出 。
使用 Runnable接口, 需要我们实现 run()方法 。 我们也
需要创建 Thread对象的一个实例, 它最终是用来调用
run()方法的 。 首先, main()方法构造了 Test类的一个实
例 r。 实例 r有它自己的数据, 在这里就是整数 i。 因为
实例 r是传给 Thread的类构造函数的, 所以 r的整数 i就是
线程运行时刻所操作的数据 。 线程总是从它所装载的
Runnable实例 (在本例中, 这个实例就是 r)的 run()方法
开始运行 。
第 10章 Java的线程处理
一个多线程编程环境允许创建基于同一个 Runnable
实例的多个线程 。 这可以通过以下方法来做到:
Thread t1= new Thread(r);
Thread t2= new Thread(r);
此时, 这两个线程共享数据和代码 。
第 10章 Java的线程处理
10.1.4 方法的选择
以上例子虽然展示了如何使用 Runnable接口创建一
个线程, 但是它并不典型 。 我们说过, 使用 Runnable
结构的主要原因是必须从其他父类继承 。 那么, 什么
时候才是使用 Runnable接口的最佳时机呢 。 给定各种
方法的选择, 你如何决定使用哪个? 下面分别列出了
选用这两种方法的几个原则 。
第 10章 Java的线程处理
使用 Runnable的原因:
● 从面向对象的角度来看, Thread类是一个虚拟处理
机严格的封装, 因此只有当处理机模型修改或扩展时,
才应该继承类 。 正因为这个原因和区别一个正在运行
的线程的处理机, 代码和数据部分的意义, 本教程采
用了这种方法 。
● 由于 Java技术只允许单一继承, 所以如果你已经继
承了 Thread,你就不能再继承其他任何类, 例如 Applet。
在某些情况下, 这会使你只能采用实现 Runnable的方
法 。
第 10章 Java的线程处理
● 因为有时你必须实现 Runnable,所以你可能喜欢保
持一致, 并总是使用这种方法 。 继承 Thread的优点:
● 当一个 run()方法体现在继承 Thread类的类中, 用 this
指向实际控制运行的 Thread实例 。 因此代码简单了一
些, 许多 Java编程语言的程序员使用扩展 Thread的机制 。
注:如果你采用这种方法, 在你的代码生命周期
的后期, 单继承模型可能会给你带来困难 。 下面的例
子中分别使用了两种方式创建线程, 大家可以分析一
下原因, 以进一步理解如何使用这两个线程模型 。
第 10章 Java的线程处理
例 10.3 TimerTest.java
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
import java.util.*;
public class TimerTest
{
public static void main(String[] args)
{
第 10章 Java的线程处理
JFrame f = new TimerTestFrame();
f.show();
}
}
class TimerTestFrame extends JFrame
{
public TimerTestFrame()
{
setSize(450,300);
setTitle("TimerTest");
第 10章 Java的线程处理
addWindowListener(new WindowAdapter()
{
public void windowClosing(WindowEvent e)
{
System.exit(0);
}
});
Container c = getContentPane();
c.setLayout(new GridLayout(2,3));
第 10章 Java的线程处理
c.add(new ClockCanvas("San Jose","GMT-8"));
c.add(new ClockCanvas("Taipei","GMT+8"));
c.add(new ClockCanvas("Berlin","GMT+1"));
c.add(new ClockCanvas("New York","GMT-5"));
c.add(new ClockCanvas("Cairo","GMT+2"));
c.add(new ClockCanvas("Bombay","GMT+5"));
}
}
interface TimerListener
第 10章 Java的线程处理
{
void timeElapsed(Timer t);
}
class Timer extends Thread
{
private TimerListener target;
private int interval;
public Timer(int i,TimerListener t)
{
target = t;
第 10章 Java的线程处理
interval = i;
setDaemon(true);
}
public void run()
{
try
{
while (!interrupted())
{
sleep(interval);
target.timeElapsed(this);
第 10章 Java的线程处理
}
}
catch(InterruptedException e) {}
}
}
class ClockCanvas extends JPanel
implements TimerListener
第 10章 Java的线程处理
{
private int seconds = 0;
private String city;
private int offset;
private GregorianCalendar calendar;
private final int LOCAL = 16;
public ClockCanvas(String c,String tz)
{
city = c;
第 10章 Java的线程处理
calendar = new
GregorianCalendar(TimeZone.getTimeZone(tz));
Timer t = new Timer(1000,this);
t.start();
setSize(125,125);
}
public void paintComponent(Graphics g)
{
super.paintComponent(g);
g.drawOval(0,0,100,100);
第 10章 Java的线程处理
double hourAngle = 2 * Math.PI
* (seconds - 3 * 60 * 60) / (12 * 60 * 60);
double minuteAngle = 2 * Math.PI
* (seconds - 15 * 60) / (60 * 60);
double secondAngle = 2 * Math.PI
* (seconds - 15) / 60;
g.drawLine(50,50,50 + (int)(30
* Math.cos(hourAngle)),
50 + (int)(30 * Math.sin(hourAngle)));
g.drawLine(50,50,50 + (int)(40 *
Math.cos(minuteAngle)),
50 + (int)(40 * Math.sin(minuteAngle)));
第 10章 Java的线程处理
g.drawLine(50,50,50 + (int)(45 * Math.cos(secondAngle)),
50 + (int)(45 * Math.sin(secondAngle)));
g.drawString(city,0,115);
}
public void timeElapsed(Timer t)
{
calendar.setTime(new Date());
seconds = calendar.get(Calendar.HOUR) * 60 * 60
第 10章 Java的线程处理
+ calendar.get(Calendar.MINUTE) * 60
+ calendar.get(Calendar.SECOND);
repaint();
}
}
这个例子实现了一个多国时间的现实窗口。程序
中,Timer 类是直接从 Thread类继承的,而
ClockCanvas类是通过实现 Runnable接口来实现线程的
功能的。显然,这是因为 ClockCanvas类必须从 JPanel
类继承用来画出时钟。程序的运行结果如图 10.3所示。
第 10章 Java的线程处理
图 10.3
第 10章 Java的线程处理
10.2 线 程 的 属 性
10.2.1 线程的状态
线程有四种状态, 分别为
● new(初始态 ):一个线程在调用 new()方法之后, 调
用 start()方法之前所处的状态 。 在初始态中, 可以调用
start()和 stop()方法 。
第 10章 Java的线程处理
● rRunnable(可运行状态 ):一旦线程调用了 start()方法,
线程就转到 Runnable()状态 。 注意, 如果线程处于
Runnable状态, 它也有可能不在运行, 这是因为还存
在优先级和调度问题 。
● blocked(阻塞 /挂起状态 ):线程处于阻塞状态 。 这是
由两种可能性造成的:因挂起而暂停;由于某些原因
而阻塞, 例如等待 IO请求的完成等 。
● dead(终止状态 ):线程转到退出状态 。 这有两种可
能性,run()方法执行结束;调用了 stop()方法 。
第 10章 Java的线程处理
一个 Thread对象在它的生命周期中会处于各种不同
的状态 。 图 10.4形象地说明了这点 。
尽管线程变为可运行的,但它并不立即开始运行。
在一个只带有一个处理机的机器上,某一个时刻只能进
行一个动作。在 Java中,线程是抢占式的,但并不一定
是分时的 (一个常见的概念错误是认为“抢占式”只不
过是“分时”的一种别称而已 )。抢占式调度模型是指可
能有多个线程是可运行的,但只有一个线程在实际运行。
第 10章 Java的线程处理
这个线程会一直运行, 直至它不再是可运行的,
或者另一个具有更高优先级的线程成为可运行的 。 对
于后面一种情形, 则是因低优先级线程被高优先级线
程抢占了运行的机会 。
一个线程可能因为各种原因而不再是可运行的:线
程的代码可能执行了一个 Thread.sleep()调用, 要求这
个线程暂停一段固定的时间;这个线程可能在等待访
问某个资源, 而且在这个资源可访问之前, 这个线程
无法继续运行 。
第 10章 Java的线程处理
图 10.4
N e w
R u n n i n gR u n n a b l e
D e a d
B l o c k e d i n
o b j e c t ’ s
w a i t ( ) p o o l
B l o c k e d i n
o b j e c t ’ s
l o c k p o o l
S c h e d u l e r
c o m p l e t e s
r u n ( )
s t a r t ( )
s l e e p ( )
or
j o i n ( )
s l e e p ( ) t i m e o u t
or
t h r e a d j o i n ( ) s
or
i n t e r u p t ( )
i n t e r r u p t()
n o t i f y ( )
l o c k
a v a i l a b l e
s y n c h r o n i z e d ( )
T h r e a d st a t e s
Ot h e r w i se
B l o c k e d
第 10章 Java的线程处理
所有可运行线程根据优先级保存在池中 。 当一个
被阻塞的线程变成可运行时, 它会被放回相应的可运
行池 。 优先级最高的非空池中的线程会得到处理机时
间 (被运行 )。
因为 Java线程不一定是分时的, 所有你必须确保你
的代码中的线程会不时地给另外一个线程运行的机会 。
这可以通过在各种时间间隔中发出 sleep()调用来做到 。
第 10章 Java的线程处理
来看如下程序段:
public class Test implements Runnable
{
public void run()
{
while (true)
{
// do lots of interesting stuff
// Give other threads a chance
try
第 10章 Java的线程处理
{
Thread.sleep(10);
}
catch (InterruptedException e)
{
// This thread's sleep was interrupted
// by another thread
}
}
}
}
第 10章 Java的线程处理
注意 try和 catch块的使用 。 Thread.sleep()和其他使
线程暂停一段时间的方法是可中断的 。 线程可以调用
另外一个线程的 interrupt()方法, 这将向暂停的线程发
出一个 InterruptedException。
Thread类的 sleep()方法对当前线程操作, 因此被称
作 Thread.sleep(x),它是一个静态方法 。 sleep()的参数
指定以毫秒为单位的线程最小休眠时间, 除非线程因
为中断而提早恢复执行, 否则它不会在这段时间之前
恢复执行 。
第 10章 Java的线程处理
Thread类的另一个方法 yield(),可以用来使具有相
同优先级的线程获得执行的机会 。 如果具有相同优先
级的其他线程是可运行的, yield()将把调用线程放到可
运行池中并使另一个线程运行 。 如果没有相同优先级
的可运行进程, yield()什么都不做 。
sleep()调用会给较低优先级线程一个运行的机会 。
yield()方法只会给相同优先级线程一个执行的机会 。
第 10章 Java的线程处理
10.2.2 线程的调度
到目前为止, 我们已经学习了创建和管理线程的
基本知识 。 你需要做的就是启动一个线程, 并让它运
行 。 你的应用程序也许希望等待一个线程执行完毕,
也许打算发送一个信息给线程, 或者只打算让线程在
处理之前休眠一会儿 。 线程类提供了四种对线程进行
操作的重要方法,sleep(),join(),wait()和 notify()。
第 10章 Java的线程处理
sleep()方法是使线程停止一段时间的方法 。 在
sleep时间间隔期满后, 线程不一定立即恢复执行 。 这
是因为在那个时刻, 其他线程可能正在运行而且没有
被调度为放弃执行, 除非:
(a), 醒来, 的线程具有更高的优先级;
(b) 正在运行的线程因为其他原因而阻塞 。
第 10章 Java的线程处理
如果一个应用程序需要执行很多时间, 比如一个
耗时很长的计算工作, 你可以把该计算工作设计成线
程 。 但是, 假定还有另外一个线程需要计算结果, 当
计算结果出来后, 如何让那个线程知道计算结果呢?
解决该问题的一个方法是让第二个线程一直不停地检
查一些变量的状态, 直到这些变量的状态发生改变 。
这样的方式在 Unix风格的服务器中常常用到 。 Java提
供了一个更加简单的机制, 即线程类中的 join()方法 。
第 10章 Java的线程处理
join()方法使得一个线程等待另外一个线程结束后
再执行 。 例如, 一个 GUI (或者其他线程 )使用 join()方
法等待一个子线程执行完毕:
CompleteCalcThread t = new CompleteCalcThread();
t.start();
// 做一会儿其他的事情
// 然后等待
t.join();
// 使用计算结果 ??
第 10章 Java的线程处理
join()方法有三种格式:
● void join():等待线程执行完毕 。
● void join(long timeout):最多等待某段时间让线程完成 。
● void join(long milliseconds,int nanoseconds):最多等
待某段时间 (毫秒+纳秒 ),让线程完成 。
第 10章 Java的线程处理
线程 API isAlive()同 join()相关联时, 是很有用的 。
一个线程在 start(此时 run()方法已经启动 )之后, 在 stop
之前的某时刻处于 isAlive状态 。
对于编写线程的程序员来说, 还有其他两个有用
的方法, 即 wait()和 notify()。 使用这两个 API,我们可
以精确地控制线程的执行过程 。 关于这两个方法的使
用, 将在后面详细解释 。
第 10章 Java的线程处理
10.2.3 线程的优先级
线程可以设定优先级, 高优先级的线程可以安排在
低优先级线程之前完成 。 一个应用程序可以通过使用线
程中的 setPriority(int)方法来设置线程的优先级大小 。
对于多线程程序, 每个线程的重要程度是不尽相同
的, 如多个线程在等待获得 CPU时间时, 往往需要优先
级高的线程优先抢占到 CPU时间得以执行;又如多个线
程交替执行时, 优先级决定了级别高的线程得到 CPU的
次数多一些且时间长一些 。 这样, 高优先级的线程处理
的任务效率就高一些 。
第 10章 Java的线程处理
Java中,线程的优先级从低到高以整数 1~10表示,
共分为 10级。设置优先级是通过调用线程对象的
setPriority()方法来进行的。设置优先级的语句为
Thread threadone=new Thread();
// 用 Thread类的子类创建线程
Thread threadtwo=new Thread();
threadone.setPriority(6);
// 设置 threadone的优先级为 6
threadtwo.setPriority(3);
// 设置 threadtwo的优先级为 3
第 10章 Java的线程处理
threadone.start(); threadtwo.start();
// strat()方法启动线程
这样,线程 threadone将会优先于线程 threadtwo执
行,并将占有更多的 CPU时间。该例中,优先级设置
放在线程启动前。也可以在启动后进行优先级设置,
以满足不同的优先级需求。
第 10章 Java的线程处理
10.3 线 程 组
通常, 一个程序可能包含若干线程, 如何来管理
这些线程呢? 把这些线程按功能分类是个不错的办法 。
Java语言提供了线程组, 线程组可以让你同时控制一
组线程 。 实际上, 线程组就是一种可以管理一组线程
的类 。
第 10章 Java的线程处理
可以用构造方法 ThreadGroup()来构造一个线程组,
如下所示:
String grounName = ? ;
ThreadGroup g = new ThreadGroup(groupName);
ThreadGroup()方法的参数表示一个线程组, 因此该串
参数必须是惟一的 。 也可以用 Thread类的构造方法往
一个指定的线程组里添加新的线程:
Thread t = new Thread(g,threadName);
第 10章 Java的线程处理
activeCount()方法用于检测某个指定线程组是否有
线程处于活动状态:
if (g.activeCount() = = 0)
{// 线程 g的所有线程都已停止 }
要中断一个线程组中的所有线程,可以调用
ThreadGroup类的方法 interrupt():
g.interrupt();
线程组可以嵌套, 即线程组可以拥有子线程组 。
缺省时, 一个新创建的线程或线程组都属于当前线程
组所属的线程组 。
第 10章 Java的线程处理
线程组的常用方法如下:
● ThreadGroup(String name):创建一个新线程组, 它的
父线程组是当前线程组 。
● ThreadGroup(ThreadGroup parent,String name):创建一
个新线程组, 其父线程组由 parent参数指定 。
● int activeCount():返回当前线程组活动线程的上限 。
● int enumerate(Thread[] list):得到当前线程组各个活动
线程的地址 。
● ThreadGroup getParent():得到当前线程组的父线程组 。
● Void interrupt():中断线程组中所有线程及其子线程组
中所有线程。
第 10章 Java的线程处理
10.4 多线程程序的开发
10.4.1 synchronized的基本概念
关键字 synchronized提供 Java编程语言一种机制,
允许程序员控制共享数据的线程 。 本节重点讨论其使
用方法 。
我们已经知道,进程允许两个或者更多个线程同
时执行。实际上,这些线程也可以共享对象和数据,
但在这种情形下,不同的线程在同一时间内不能存取
同一数据,这是因为在开始设计 Java的时候,就采用
了线程的概念。
第 10章 Java的线程处理
Java语言定义了一个特殊的关键字 synchronized(同
步 ),该关键字可以应用到代码块上 (代码块也包括入口
方法 )。 该关键字的目的是防止多个线程在同一时间执
行同一代码块内的代码 。
定义一个同步方法的格式如下:
[public|private] synchronized {type}
methodname(...)
一个简单的应用例子如下:
public class someClass
{
第 10章 Java的线程处理
public void aMethod()
{
...
synchronized(this)
{
// Synchronized code block
}
...
}
}
第 10章 Java的线程处理
同步化的关键字可以保证在同一时间内只有一个
线程可以执行某代码段, 而任何其他要用到该段代码
的线程将被阻塞, 直到第一个线程执行完该段代码,
如图 10.5所示对象锁标志 synchronized到底是如何做到
保证资源访问同步的呢? 在 Java技术中, 每个对象都
有一个和它相关联的标志 。 这个标志可以被认为是
,锁标志, 。 synchronized关键字能保证多线程之间的
同步运行, 即允许独占地存取对象 。 当线程运行到
synchronized语句时, 它检查作为参数传递的对象, 并
在继续执行之前试图从对象获得锁标志 。
第 10章 Java的线程处理
图 10.5
O b j e c t t h i s
C o d e o r
b e h a v i o r
D a t a o r
s t a t e
O b j e c t t h i s
C o d e o r
b e h a v i o r
D a t a o r
s t a t e
T h r e a d b e f o r e s y n c h r o n i z e d ( t h i s )
p u b l i c v o i d p u s h ( c h a r c ) {
s y n c h r o n i z e d ( t h i s ) {
d a t a [ i d x ] = c ;
i d x + + ;
}
}
T h r e a d a f t e r s y n c h r o n i z e d ( t h i s )
p u b l i c v o i d p u s h ( c h a r c ) {
s y n c h r o n i z e d ( t h i s ) {
d a t a [ i d x ] = c ;
i d x + + ;
}
}
第 10章 Java的线程处理
意识到它自身并没有保护数据是很重要的 。 因为
如果同一个对象的 pop()方法没有受到 synchronized的影
响, 且 pop()是由另一个线程调用的, 那么仍然存在破
坏 data的一致性的危险 。 如果要使锁有效, 所有存取共
享数据的方法必须在同一把锁上同步 。
图 10.6显示了如果 pop()受到 synchronized的影响,
且另一个线程在原线程持有那个对象的锁时试图执行
pop()方法时所发生的事情:
第 10章 Java的线程处理
图 10.6
O b j e c t t h i s
l o c k f l a g m i s s i n g
C o d e o r
b e h a v i o r
D a t a o r
s t a t e
T h r e a d,t r y i n g t o e x e c u t e
s y n c h r o n i z e d ( t h i s )
p u b l i c c h a r p o p ( ) {
s y n c h r o n i z e d ( t h i s ) {
i d x - - ;
r e t u r n d a t a [ i d x ] ;
}
}
W a i t i n g f o r
O b j e c t l o c k
第 10章 Java的线程处理
当线程试图执行 synchronized(this)语句时, 它试图
从 this对象获取锁标志 。 由于得不到标志, 所以线程不
能继续运行 。 然后, 线程加入到与那个对象锁相关联
的等待线程池中 。 当标志返回给对象时, 某个等待这
个标志的线程将得到这把锁并继续运行 。
由于等待一个对象的锁标志的线程在得到标志之
前不能恢复运行, 所以让持有锁标志的线程在不再需
要的时候返回标志是很重要的 。
第 10章 Java的线程处理
锁标志将自动返回给它的对象 。 持有锁标志的线
程执行到 synchronized()代码块末尾时将释放锁 。 Java技
术特别注意了保证即使出现中断或异常而使得执行流
跳出 synchronized()代码块, 锁也会自动返回 。 此外,
如果一个线程对同一个对象两次发出 synchronized调用,
则在跳出最外层的块时, 标志会正确地释放, 而最内
层的将被忽略 。
这些规则使得与其他系统中的等价功能相比, 管
理同步块的使用简单了很多 。
第 10章 Java的线程处理
10.4.2 多线程的控制
线程有两个缺陷:死锁和饥饿 。 所谓死锁, 就是
一个或者多个线程, 在一个给定的任务中, 协同作用,
互相干涉, 从而导致一个或者更多线程永远等待下去 。
与此类似, 所谓饥饿, 就是一个线程永久性地占有资
源, 使得其他线程得不到该资源 。
第 10章 Java的线程处理
首先我们看一下死锁的问题 。 一个简单的例子就
是:你到 ATM机上取钱, 却看到如下的信息, 现在没
有现金, 请等会儿再试 。,, 你需要钱, 所以你就等
了一会儿再试, 但是你又看到了同样的信息;与此同
时, 在你后面, 一辆运款车正等待着把钱放进 ATM机
中, 但是运款车到不了 ATM取款机,因为你的汽车挡着
道 。 在这种情况下,就发生了所谓的死锁 。
第 10章 Java的线程处理
在饥饿的情形下, 系统并不处于死锁状态中, 因
为有一个进程仍在处理之中, 只是其他进程永远得不
到执行的机会而已 。 在什么样的环境下, 会导致饥饿
的发生, 并没有预先确定好的规则 。 但一旦发生下面
四种情况之一, 就会导致死锁的发生 。
● 相互排斥:一个线程或者进程永远占有一共享资源,
例如, 独占该资源 。
● 循环等待:进程 A等待进程 B,而后者又在等待进程
C,而进程 C又在等待进程 A。
第 10章 Java的线程处理
● 部分分配:资源被部分分配 。 例如, 进程 A和 B都需
要访问一个文件, 并且都要用到打印机, 进程 A获得了
文件资源, 进程 B获得了打印机资源 。
● 缺少优先权:一个进程访问了某个资源, 但是一直
不释放该资源, 即使该进程处于阻塞状态 。
为了避免出现死锁的情况, 我们就必须在多线程
程序中做同步管理, 为此必须编写使它们交互的程序 。
第 10章 Java的线程处理
java.lang.Object类中提供了两个用于线程通信的方
法,wait()和 notify()。 如果线程对一个同步对象 x发出
一个 wait()调用, 则该线程会暂停执行, 直到另一个线
程对同一个同步对象 x也发出一个 wait()调用 。
为了让线程对一个对象调用 wait()或 notify(),线程
必须锁定那个特定的对象 。 也就是说, 只能在它们被
调用的实例的同步块内使用 wait()和 notify()。 当某个线
程执行包含对一个特定对象执行 wait()调用的同步代码
时, 这个线程就被放到与那个对象相关的等待池中 。
此外, 调用 wait()的线程自动释放对象的锁标志 。
第 10章 Java的线程处理
对一个特定对象执行 notify()调用时, 将从对象的
等待池中移走一个任意的线程, 并放到锁池中 。 锁池
中的对象一直在等待, 直到可以获得对象的锁标记 。
notifyAll()方法将从等待池中移走所有等待那个对象的
线程, 并把它们放到锁池中 。 只有锁池中的线程能获
取对象的锁标记, 锁标记允许线程从上次因调用 wait()
而中断的地方开始继续运行 。
第 10章 Java的线程处理
在许多实现了 wait()/notify()机制的系统中, 醒来的
线程必定是那个等待时间最长的线程 。 然而, 在 Java
技术中, 并不能保证这点 。
应注意的是, 不管是否有线程在等待, 用户都可
以调用 notify()。 如果对一个对象调用 notify()方法, 而
在这个对象的锁标记等待池中并没有阻塞的线程, 那
么调用 notify()将不起任何作用 。 对 notify()的调用不会
被存储 。
第 10章 Java的线程处理
下面给出一个线程交互的实例, 用于说明如何使
用 synchronized关键字和 wait(),notify()方法来解决线程
的同步问题 。
例 10.4 DemoThread.java
import java.lang.Runnable;
import java.lang.Thread;
public class DemoThread implements Runnable
{
第 10章 Java的线程处理
public DemoThread()
{
TestThread testthread1 = new TestThread(this,"1");
TestThread testthread2 = new TestThread(this,"2");
testthread2.start();
testthread1.start();
}
public static void main(String[] args)
第 10章 Java的线程处理
{
DemoThread demoThread1 = new DemoThread();
}
public void run()
{
TestThread t = (TestThread) Thread.currentThread();
try
{
if (!t.getName().equalsIgnoreCase("1"))
第 10章 Java的线程处理
{
synchronized(this)
{
wait();
}
}
while(true)
{
第 10章 Java的线程处理
System.out.println("@time in thread" +
t.getName()+ "="+
t.increaseTime());
if(t.getTime()%10 == 0)
{
synchronized(this)
{
System.out.println("***************************
*****");
notify();
if (t.getTime()==100)
第 10章 Java的线程处理
break;
wait();
}
}
}
}
catch(Exception e)
{
e.printStackTrace();
}
}
}
第 10章 Java的线程处理
class TestThread extends Thread
{
private int time = 0;
public TestThread(Runnable r,String name)
{
super(r,name);
}
public int getTime()
{
return time;
第 10章 Java的线程处理
}
public int increaseTime()
{
return ++time;
}
}
本例实现了两个线程, 每个线程输出 1~ 100的数
字 。 工作程序是:第一个线程输出 1~ 10,停止, 通知
第二个线程;第二个线程输出 1~ 10,停止, 再通知第
一个线程;第一个线程输出 11~ 20??
第 10章 Java的线程处理
在 Java中, 每个对象都有个对象锁标志 (Object lock
flag) 与之想关联, 当一个线程 A调用对象的一段
synchronized代码时, 它首先要获取与这个对象关联的
对象锁标志, 然后才执行相应的代码, 执行结束后,
把这个对象锁标志返回给对象 。 因此, 在线程 A执行
synchronized代码期间, 如果另一个线程 B也要执行同
一对象的一段 synchronized代码 (不一定与线程 A执行的
相同 ),那么它必须等到线程 A执行完后, 才能继续 。
第 10章 Java的线程处理
在 synchronized代码被执行期间, 线程可以调用对
象的 wait()方法, 释放对象锁标志, 进入等待状态, 并
且可以调用 notify()或者 notifyAll()方法通知正在等待的
其他线程 。 notify()方法通知等待队列中的第一个线程,
notifyAll()方法通知的是等待队列中的所有线程 。
程序的部分输出结果如下:
@time in thread1=1
@time in thread1=2
@time in thread1=3
@time in thread1=4
@time in thread1=5
第 10章 Java的线程处理
@time in thread1=6
@time in thread1=7
@time in thread1=8
@time in thread1=9
@time in thread1=10
********************************
@time in thread2=1
@time in thread2=2
@time in thread2=3
@time in thread2=4
@time in thread2=5
@time in thread2=6
第 10章 Java的线程处理
@time in thread2=7
@time in thread2=8
@time in thread2=9
@time in thread2=10
********************************
线程中还有三个控制线程执行的方法,suspend()、
resume()和 stop()方法 。 但从 JDK1.2开始, Java标准不赞
成使用它们来控制线程的执行, 而是用 wait()和 notify()
来代替它们 。 这里我们只对这三个方法做个简单地介
绍 。
第 10章 Java的线程处理
● suspend()和 resume()方法
resume()方法的惟一作用就是恢复被挂起的线程 。
所以, 如果没有 suspend(),resume()也就没有存在的必
要 。 从设计的角度来看, 有两个原因使得使用 suspend()
非常危险:它容易产生死锁;它允许一个线程控制另
一个线程代码的执行 。 下面分别介绍这两种危险 。
假设有两个线程,threadA和 threadB。 当正在执行
它们的代码时, threadB获得一个对象的锁, 然后继续
它的任务 。 当 threadA的执行代码调用 threadB.suspend()
时, 将使 threadB停止执行它的代码 。
第 10章 Java的线程处理
如果 threadB.suspend()没有使 threadB释放它所持有
的锁, 就会发生死锁 。 如果调用 threadB.resume()的线
程需要 threadB仍持有的锁, 这两个线程就会陷入死锁 。
假设 threadA调用 threadB.suspend()。 如果 threadB
被挂起时 threadA获得控制, 那么 threadB就永远得不到
机会来进行清除工作, 例如使它正在操作的共享数据
处于稳定状态 。 为了安全起见, 只有 threadB才可以决
定何时停止它自己的代码 。
第 10章 Java的线程处理
你应该使用对同步对象调用 wait()和 notify()的机制
来代替 suspend()和 resume()进行线程控制 。 这种方法是
通过执行 wait()调用来强制线程决定何时, 挂起, 自己 。
这使得同步对象的锁被自动释放, 并给予线程一个在
调用 wait()之前稳定任何数据的机会 。
● stop()方法
stop()方法的情形是类似的, 但结果有所不同 。 如
果一个线程在持有一个对象锁的时候被停止, 它将在
终止之前释放它持有的锁 。 这避免了前面所讨论的死
锁问题, 但它又引入了其他问题 。
第 10章 Java的线程处理
在前面的范例中, 如果线程在已将字符加入栈但
还没有使下标值加 1之后被停止, 则在释放锁的时候会
得到一个不一致的栈结构 。
总会有一些关键操作需要不可分割地执行, 而且
在线程执行这些操作时被停止就会破坏操作的不可分
割性 。
第 10章 Java的线程处理
一个关于停止线程的独立而又重要的问题涉及线
程的总体设计策略 。 创建线程来执行某个特定作业,
并存活于整个程序的生命周期 。 换言之, 你不会这样
来设计程序:随意地创建和处理线程, 或创建无数个
对话框或 socket端点 。 每个线程都会消耗系统资源, 而
系统资源并不是无限的 。 这并不是暗示一个线程必须
连续执行;它只是简单地意味着应当使用合适而安全
的 wait()和 notify()机制来控制线程 。
第 10章 Java的线程处理
10.4.3 多线程之间的通信
Java 语言提供了各种各样的输入 /输出流, 使我们
能够很方便地对数据进行操作 。 其中, 管道 (Pipe)流是
一种特殊的流, 用于在不同线程间直接传送数据 (一个
线程发送数据到输出管道, 另一个线程从输入管道中
读数据 )。 通过使用管道, 就可实现不同线程间的通信,
无需求助于类似临时文件之类的东西 。
第 10章 Java的线程处理
Java提供了两个特殊的, 专门的类用于处理管道,
它们就是 PipedInputStream类和 PipedOutputStream类 。
PipedInputStream代表了数据在管道中的输出端, 也就
是线程向管道读数据的一端; PipedOutputStream代表
了数据在管道中的输入端, 也就是线程向管道写数据
的一端, 这两个类一起使用可以提供数据的管道流 。
为了创建一个管道流,我们必须首先创建一个
PipedOutStream对象,然后创建 PipedInputStream对象,
前面我们已经介绍过。
第 10章 Java的线程处理
一旦创建了一个管道, 就可以像操作文件一样对
管道进行数据的读 /写 。 下面我们来看一个运用管道实
现线程间通信的实例 。
例 10.5 PipeTest.java
import java.util.*;
import java.io.*;
public class PipeTest
{
public static void main(String args[])
第 10章 Java的线程处理
{
try
{
/* set up pipes */
PipedOutputStream pout1 = new PipedOutputStream();
PipedInputStream pin1 = new PipedInputStream(pout1)
PipedOutputStream pout2 = new PipedOutputStream();
PipedInputStream pin2 = new PipedInputStream(pout2);
第 10章 Java的线程处理
/* construct threads */
Producer prod = new Producer(pout1);
Filter filt = new Filter(pin1,pout2);
Consumer cons = new Consumer(pin2);
/* start threads */
prod.start();
filt.start();
cons.start();
第 10章 Java的线程处理
}
catch (IOException e){}
}
}
class Producer extends Thread
{
private DataOutputStream out;
private Random rand = new Random();
public Producer(OutputStream os)
{
第 10章 Java的线程处理
out = new DataOutputStream(os);
}
public void run()
{
while (true)
{
try
{
double num = rand.nextDouble();
out.writeDouble(num);
out.flush();
第 10章 Java的线程处理
sleep(Math.abs(rand.nextInt() % 1000));
}
catch(Exception e)
{
System.out.println("Error," + e);
}
}
}
}
第 10章 Java的线程处理
class Filter extends Thread
{
private DataInputStream in;
private DataOutputStream out;
private double total = 0;
private int count = 0;
public Filter(InputStream is,OutputStream os)
{
in = new DataInputStream(is);
out = new DataOutputStream(os);
第 10章 Java的线程处理
}
public void run()
{
for (;;)
{
try
{
double x = in.readDouble();
total += x;
count++;
if (count != 0) out.writeDouble(total / count);
第 10章 Java的线程处理
}
catch(IOException e)
{
System.out.println("Error," + e);
}
}
}
}
class Consumer extends Thread
{
第 10章 Java的线程处理
private double old_avg = 0;
private DataInputStream in;
public Consumer(InputStream is)
{
in = new DataInputStream(is);
}
public void run()
{
for(;;)
第 10章 Java的线程处理
{
try
{
double avg = in.readDouble();
if (Math.abs(avg - old_avg) > 0.01)
{
System.out.println("Current average is " + avg);
old_avg = avg;
}
}
catch(IOException e)
{
第 10章 Java的线程处理
System.out.println("Error," + e);
}
}
}
}
上面的例子是一个示范管道流的程序。它拥有一
个在随机时间产生随机数的生产者线程;一个用于读
取输入数据并不断地计算它们的平均值的过滤线程;
一个在屏幕上输出结果的消费者线程。这个程序不会
自动结束,用户可以通过 Ctrl+C键来终止它。