第11章多线程编程
和其他多数计算机语言不同,Java内置支持多线程编程(multithreaded programming。多线程程序包含两条或两条以上并发运行的部分。程序中每个这样的部分都叫一个线程(thread),每个线程都有独立的执行路径。因此,多线程是多任务处理的一种特殊形式。
你一定知道多任务处理,因为它实际上被所有的现代操作系统所支持。然而,多任务处理有两种截然不同的类型:基于进程的和基于线程的。认识两者的不同是十分重要的。对很多读者,基于进程的多任务处理是更熟悉的形式。进程(process)本质上是一个执行的程序。因此,基于进程(process-based) 的多任务处理的特点是允许你的计算机同时运行两个或更多的程序。举例来说,基于进程的多任务处理使你在运用文本编辑器的时候可以同时运行Java编译器。在基于进程的多任务处理中,程序是调度程序所分派的最小代码单位。
在基于线程(thread-based) 的多任务处理环境中,线程是最小的执行单位。这意味着一个程序可以同时执行两个或者多个任务的功能。例如,一个文本编辑器可以在打印的同时格式化文本。所以,多进程程序处理“大图片”,而多线程程序处理细节问题。
多线程程序比多进程程序需要更少的管理费用。进程是重量级的任务,需要分配它们自己独立的地址空间。进程间通信是昂贵和受限的。进程间的转换也是很需要花费的。另一方面,线程是轻量级的选手。它们共享相同的地址空间并且共同分享同一个进程。线程间通信是便宜的,线程间的转换也是低成本的。当Java程序使用多进程任务处理环境时,多进程程序不受Java的控制,而多线程则受Java控制。
多线程帮助你写出CPU最大利用率的高效程序,因为空闲时间保持最低。这对Java运行的交互式的网络互连环境是至关重要的,因为空闲时间是公共的。举个例子来说,网络的数据传输速率远低于计算机处理能力,本地文件系统资源的读写速度远低于CPU的处理能力,当然,用户输入也比计算机慢很多。在传统的单线程环境中,你的程序必须等待每一个这样的任务完成以后才能执行下一步——尽管CPU有很多空闲时间。多线程使你能够获得并充分利用这些空闲时间。
如果你在Windows 98 或Windows 2000这样的操作系统下有编程经验,那么你已经熟悉了多线程。然而,Java管理线程使多线程处理尤其方便,因为很多细节对你来说是易于处理的。
11.1 Java线程模型
Java运行系统在很多方面依赖于线程,所有的类库设计都考虑到多线程。实际上,Java 使用线程来使整个环境异步。这有利于通过防止CPU循环的浪费来减少无效部分。
为更好的理解多线程环境的优势可以将它与它的对照物相比较。单线程系统的处理途径是使用一种叫作轮询的事件循环方法。在该模型中,单线程控制在一无限循环中运行,
第1部分 Java语言 192
轮询一个事件序列来决定下一步做什么。一旦轮询装置返回信号表明,已准备好读取网络
文件,事件循环调度控制管理到适当的事件处理程序。直到事件处理程序返回,系统中没
有其他事件发生。这就浪费了CPU时间。这导致了程序的一部分独占了系统,阻止了其他
事件的执行。总的来说,单线程环境,当一个线程因为等待资源时阻塞(block,挂起执行),
整个程序停止运行。
Java多线程的优点在于取消了主循环/轮询机制。一个线程可以暂停而不影响程序的其
他部分。例如,当一个线程从网络读取数据或等待用户输入时产生的空闲时间可以被利用
到其他地方。多线程允许活的循环在每一帧间隙中沉睡一秒而不暂停整个系统。在Java程
序中出现线程阻塞,仅有一个线程暂停,其他线程继续运行。
线程存在于好几种状态。线程可以正在运行(running)。只要获得CPU时间它就可以
运行。运行的线程可以被挂起(suspend),并临时中断它的执行。一个挂起的线程可以被
恢复(resume,允许它从停止的地方继续运行。一个线程可以在等待资源时被阻塞(block)。
在任何时候,线程可以终止(terminate),这立即中断了它的运行。一旦终止,线程不能
被恢复。
11.1.1 线程优先级
Java给每个线程安排优先级以决定与其他线程比较时该如何对待该线程。线程优先级
是详细说明线程间优先关系的整数。作为绝对值,优先级是毫无意义的;当只有一个线程
时,优先级高的线程并不比优先权低的线程运行的快。相反,线程的优先级是用来决定何
时从一个运行的线程切换到另一个。这叫“上下文转换”(context switch)。决定上下文转换
发生的规则很简单:
?线程可以自动放弃控制。在I/O未决定的情况下,睡眠或阻塞由明确的让步来完成。
在这种假定下,所有其他的线程被检测,准备运行的最高优先级线程被授予CPU。
?线程可以被高优先级的线程抢占。在这种情况下,低优先级线程不主动放弃,处理
器只是被先占——无论它正在干什么——处理器被高优先级的线程占据。基本上,
一旦高优先级线程要运行,它就执行。这叫做有优先权的多任务处理。
当两个相同优先级的线程竞争CPU周期时,情形有一点复杂。对于Windows98这样的
操作系统,等优先级的线程是在循环模式下自动划分时间的。对于其他操作系统,例如
Solaris 2.x,等优先级线程相对于它们的对等体自动放弃。如果不这样,其他的线程就不会
运行。
警告:不同的操作系统下等优先级线程的上下文转换可能会产生错误。
11.1.2 同步性
因为多线程在你的程序中引入了一个异步行为,所以在你需要的时候必须有加强同步
性的方法。举例来说,如果你希望两个线程相互通信并共享一个复杂的数据结构,例如链
表序列,你需要某些方法来确保它们没有相互冲突。也就是说,你必须防止一个线程写入
数据而另一个线程正在读取链表中的数据。为此目的,Java在进程间同步性的老模式基础
第11章多线程编程 193
上实行了另一种方法:管程(monitor)。管程是一种由C.A.R.Hoare首先定义的控制机制。
你可以把管程想象成一个仅控制一个线程的小盒子。一旦线程进入管程,所有线程必须等
待直到该线程退出了管程。用这种方法,管程可以用来防止共享的资源被多个线程操纵。
很多多线程系统把管程作为程序必须明确的引用和操作的对象。Java提供一个清晰的
解决方案。没有“Monitor”类;相反,每个对象都拥有自己的隐式管程,当对象的同步方
法被调用时管程自动载入。一旦一个线程包含在一个同步方法中,没有其他线程可以调用
相同对象的同步方法。这就使你可以编写非常清晰和简洁的多线程代码,因为同步支持是
语言内置的。
11.1.3 消息传递
在你把程序分成若干线程后,你就要定义各线程之间的联系。用大多数其他语言规划
时,你必须依赖于操作系统来确立线程间通信。这样当然增加花费。然而,Java提供了多
线程间谈话清洁的、低成本的途径——通过调用所有对象都有的预先确定的方法。Java的
消息传递系统允许一个线程进入一个对象的一个同步方法,然后在那里等待,直到其他线
程明确通知它出来。
11.1.4 Thread 类和Runnable 接口
Java的多线程系统建立于Thread类,它的方法,它的共伴接口Runnable基础上。Thread
类封装了线程的执行。既然你不能直接引用运行着的线程的状态,你要通过它的代理处理
它,于是Thread 实例产生了。为创建一个新的线程,你的程序必须扩展Thread 或实现
Runnable接口。
Thread类定义了好几种方法来帮助管理线程。本章用到的方法如表11-1所示:
表11-1 管理线程的方法
方法意义
getName 获得线程名称
getPriority 获得线程优先级
jsAlive 判定线程是否仍在运行
join 等待一个线程终止
run 线程的入口点.
sleep 在一段时间内挂起线程
start 通过调用运行方法来启动线程
到目前为止,本书所应用的例子都是用单线程的。本章剩余部分解释如何用Thread 和
Runnable 来创建、管理线程。让我们从所有Java程序都有的线程:主线程开始。
11.2 主线程
当Java程序启动时,一个线程立刻运行,该线程通常叫做程序的主线程(main thread),
第1部分 Java语言 194 因为它是程序开始时就执行的。主线程的重要性体现在两方面:
?它是产生其他子线程的线程
?通常它必须最后完成执行,因为它执行各种关闭动作。
尽管主线程在程序启动时自动创建,但它可以由一个Thread对象控制。为此,你必须
调用方法currentThread()获得它的一个引用,currentThread()是Thread类的公有的静态成员。
它的通常形式如下:
static Thread currentThread( )
该方法返回一个调用它的线程的引用。一旦你获得主线程的引用,你就可以像控制其
他线程那样控制主线程。
让我们从复习下面例题开始:
// Controlling the main Thread.
class CurrentThreadDemo {
public static void main(String args[]) {
Thread t = Thread.currentThread();
System.out.println("Current thread: " + t);
// change the name of the thread
t.setName("My Thread");
System.out.println("After name change: " + t);
try {
for(int n = 5; n > 0; n--) {
System.out.println(n);
Thread.sleep(1000);
}
} catch (InterruptedException e) {
System.out.println("Main thread interrupted");
}
}
}
在本程序中,当前线程(自然是主线程)的引用通过调用currentThread()获得,该引用
保存在局部变量t中。然后,程序显示了线程的信息。接着程序调用setName()改变线程的内
部名称。线程信息又被显示。然后,一个循环数从5开始递减,每数一次暂停一秒。暂停是
由sleep()方法来完成的。Sleep()语句明确规定延迟时间是1毫秒。注意循环外的try/catch块。
Thread类的sleep()方法可能引发一个InterruptedException异常。这种情形会在其他线程想要
打搅沉睡线程时发生。本例只是打印了它是否被打断的消息。在实际的程序中,你必须灵
活处理此类问题。下面是本程序的输出:
Current thread: Thread[main,5,main]
After name change: Thread[My Thread,5,main]
5
4
3
第11章多线程编程 195 2
1
注意t作为语句println()中参数运用时输出的产生。该显示顺序:线程名称,优先级以及
组的名称。默认情况下,主线程的名称是main。它的优先级是5,这也是默认值,main也是
所属线程组的名称。一个线程组(thread group)是一种将线程作为一个整体集合的状态控
制的数据结构。这个过程由专有的运行时环境来处理,在此就不赘述了。线程名改变后,t
又被输出。这次,显示了新的线程名。
让我们更仔细的研究程序中Thread类定义的方法。sleep()方法按照毫秒级的时间指示使
线程从被调用到挂起。它的通常形式如下:
static void sleep(long milliseconds) throws InterruptedException
挂起的时间被明确定义为毫秒。该方法可能引发InterruptedException异常。
sleep()方法还有第二种形式,显示如下,该方法允许你指定时间是以毫秒还是以纳秒
为周期。
static void sleep(long milliseconds, int nanoseconds) throws
InterruptedException
第二种形式仅当允许以纳秒为时间周期时可用。
如上述程序所示,你可以用setName()设置线程名称,用getName()来获得线程名称(该
过程在程序中没有体现)。这些方法都是Thread 类的成员,声明如下:
final void setName(String threadName)
final String getName( )
这里,threadName 特指线程名称。
11.3 创建线程
大多数情况,通过实例化一个Thread对象来创建一个线程。Java定义了两种方式:
?实现Runnable 接口。
?可以继承Thread类。
下面的两小节依次介绍了每一种方式。
11.3.1 实现Runnable接口
创建线程的最简单的方法就是创建一个实现Runnable 接口的类。Runnable抽象了一个
执行代码单元。你可以通过实现Runnable接口的方法创建每一个对象的线程。为实现
Runnable 接口,一个类仅需实现一个run()的简单方法,该方法声明如下:
public void run( )
在run()中可以定义代码来构建新的线程。理解下面内容是至关重要的:run()方法能够
第1部分 Java语言 196
像主线程那样调用其他方法,引用其他类,声明变量。仅有的不同是run()在程序中确立另
一个并发的线程执行入口。当run()返回时,该线程结束。
在你已经创建了实现Runnable接口的类以后,你要在类内部实例化一个Thread类的对
象。Thread 类定义了好几种构造函数。我们会用到的如下:
Thread(Runnable threadOb, String threadName)
该构造函数中,threadOb是一个实现Runnable接口类的实例。这定义了线程执行的起点。
新线程的名称由threadName定义。
建立新的线程后,它并不运行直到调用了它的start()方法,该方法在Thread 类中定义。
本质上,start() 执行的是一个对run()的调用。 Start()方法声明如下:
void start( )
下面的例子是创建一个新的线程并启动它运行:
// Create a second thread.
class NewThread implements Runnable {
Thread t;
NewThread() {
// Create a new, second thread
t = new Thread(this, "Demo Thread");
System.out.println("Child thread: " + t);
t.start(); // Start the thread
}
// This is the entry point for the second thread.
public void run() {
try {
for(int i = 5; i > 0; i--) {
System.out.println("Child Thread: " + i);
Thread.sleep(500);
}
} catch (InterruptedException e) {
System.out.println("Child interrupted.");
}
System.out.println("Exiting child thread.");
}
}
class ThreadDemo {
public static void main(String args[]) {
new NewThread(); // create a new thread
try {
for(int i = 5; i > 0; i--) {
System.out.println("Main Thread: " + i);
Thread.sleep(1000);
}
} catch (InterruptedException e) {
System.out.println("Main thread interrupted.");
}
第11章多线程编程 197 System.out.println("Main thread exiting.");
}
}
在NewThread 构造函数中,新的Thread对象由下面的语句创建::
t = new Thread(this, "Demo Thread");
通过前面的语句this 表明在this对象中你想要新的线程调用run()方法。然后,start() 被
调用,以run()方法为开始启动了线程的执行。这使子线程for 循环开始执行。调用start()之
后,NewThread 的构造函数返回到main()。当主线程被恢复,它到达for 循环。两个线程继
续运行,共享CPU,直到它们的循环结束。该程序的输出如下:
Child thread: Thread[Demo Thread,5,main]
Main Thread: 5
Child Thread: 5
Child Thread: 4
Main Thread: 4
Child Thread: 3
Child Thread: 2
Main Thread: 3
Child Thread: 1
Exiting child thread.
Main Thread: 2
Main Thread: 1
Main thread exiting.
如前面提到的,在多线程程序中,通常主线程必须是结束运行的最后一个线程。实际
上,一些老的JVM,如果主线程先于子线程结束,Java的运行时间系统就可能“挂起”。
前述程序保证了主线程最后结束,因为主线程沉睡周期1000毫秒,而子线程仅为500毫秒。
这就使子线程在主线程结束之前先结束。简而言之,你将看到等待线程结束的更好途径。
11.3.2 扩展Thread
创建线程的另一个途径是创建一个新类来扩展Thread类,然后创建该类的实例。当一
个类继承Thread时,它必须重载run()方法,这个run()方法是新线程的入口。它也必须调用
start()方法去启动新线程执行。下面用扩展thread类重写前面的程序:
// Create a second thread by extending Thread
class NewThread extends Thread {
NewThread() {
// Create a new, second thread
super("Demo Thread");
System.out.println("Child thread: " + this);
start(); // Start the thread
}
// This is the entry point for the second thread.
public void run() {
try {
for(int i = 5; i > 0; i--) {
System.out.println("Child Thread: " + i);
第1部分 Java语言 198 Thread.sleep(500);
}
} catch (InterruptedException e) {
System.out.println("Child interrupted.");
}
System.out.println("Exiting child thread.");
}
}
class ExtendThread {
public static void main(String args[]) {
new NewThread(); // create a new thread
try {
for(int i = 5; i > 0; i--) {
System.out.println("Main Thread: " + i);
Thread.sleep(1000);
}
} catch (InterruptedException e) {
System.out.println("Main thread interrupted.");
}
System.out.println("Main thread exiting.");
}
}
该程序生成和前述版本相同的输出。子线程是由实例化NewThread对象生成的,该对
象从Thread类派生。注意NewThread 中super()的调用。该方法调用了下列形式的Thread构
造函数:
public Thread(String threadName)
这里,threadName指定线程名称。
11.3.3 选择合适方法
到这里,你一定会奇怪为什么Java有两种创建子线程的方法,哪一种更好呢。所有的
问题都归于一点。Thread类定义了多种方法可以被派生类重载。对于所有的方法,惟一的
必须被重载的是run()方法。这当然是实现Runnable接口所需的同样的方法。很多Java程序员
认为类仅在它们被加强或修改时应该被扩展。因此,如果你不重载Thread的其他方法时,
最好只实现Runnable 接口。这当然由你决定。然而,在本章的其他部分,我们应用实现
runnable接口的类来创建线程。
11.4 创建多线程
到目前为止,我们仅用到两个线程:主线程和一个子线程。然而,你的程序可以创建
所需的更多线程。例如,下面的程序创建了三个子线程:
// Create multiple threads.
class NewThread implements Runnable {
第11章多线程编程 199 String name; // name of thread
Thread t;
NewThread(String threadname) {
name = threadname;
t = new Thread(this, name);
System.out.println("New thread: " + t);
t.start(); // Start the thread
}
// This is the entry point for thread.
public void run() {
try {
for(int i = 5; i > 0; i--) {
System.out.println(name + ": " + i);
Thread.sleep(1000);
}
} catch (InterruptedException e) {
System.out.println(name + "Interrupted");
}
System.out.println(name + " exiting.");
}
}
class MultiThreadDemo {
public static void main(String args[]) {
new NewThread("One"); // start threads
new NewThread("Two");
new NewThread("Three");
try {
// wait for other threads to end
Thread.sleep(10000);
} catch (InterruptedException e) {
System.out.println("Main thread Interrupted");
}
System.out.println("Main thread exiting.");
}
}
程序输出如下所示:
New thread: Thread[One,5,main]
New thread: Thread[Two,5,main]
New thread: Thread[Three,5,main]
One: 5
Two: 5
Three: 5
One: 4
Two: 4
Three: 4
One: 3
Three: 3
Two: 3
第1部分 Java语言 200 One: 2
Three: 2
Two: 2
One: 1
Three: 1
Two: 1
One exiting.
Two exiting.
Three exiting.
Main thread exiting.
如你所见,一旦启动,所有三个子线程共享CPU。注意main()中对sleep(10000)的调用。
这使主线程沉睡十秒确保它最后结束。
11.5 使用isAlive()和join()
如前所述,通常你希望主线程最后结束。在前面的例子中,这点是通过在main()中调
用sleep()来实现的,经过足够长时间的延迟以确保所有子线程都先于主线程结束。然而,
这不是一个令人满意的解决方法,它也带来一个大问题:一个线程如何知道另一线程已经
结束?幸运的是,Thread类提供了回答此问题的方法。
有两种方法可以判定一个线程是否结束。第一,可以在线程中调用isAlive()。这种方法
由Thread定义,它的通常形式如下:
final boolean isAlive( )
如果所调用线程仍在运行,isAlive()方法返回true,如果不是则返回false。
但isAlive()很少用到,等待线程结束的更常用的方法是调用join(),描述如下:
final void join( ) throws InterruptedException
该方法等待所调用线程结束。该名字来自于要求线程等待直到指定线程参与的概念。
join()的附加形式允许给等待指定线程结束定义一个最大时间。
下面是前面例子的改进版本。运用join()以确保主线程最后结束。同样,它也演示了
isAlive()方法。
// Using join() to wait for threads to finish.
class NewThread implements Runnable {
String name; // name of thread
Thread t;
NewThread(String threadname) {
name = threadname;
t = new Thread(this, name);
System.out.println("New thread: " + t);
t.start(); // Start the thread
}
// This is the entry point for thread.
public void run() {
第11章多线程编程 201 try {
for(int i = 5; i > 0; i--) {
System.out.println(name + ": " + i);
Thread.sleep(1000);
}
} catch (InterruptedException e) {
System.out.println(name + " interrupted.");
}
System.out.println(name + " exiting.");
}
}
class DemoJoin {
public static void main(String args[]) {
NewThread ob1 = new NewThread("One");
NewThread ob2 = new NewThread("Two");
NewThread ob3 = new NewThread("Three");
System.out.println("Thread One is alive: "
+ ob1.t.isAlive());
System.out.println("Thread Two is alive: "
+ ob2.t.isAlive());
System.out.println("Thread Three is alive: "
+ ob3.t.isAlive());
// wait for threads to finish
try {
System.out.println("Waiting for threads to finish.");
ob1.t.join();
ob2.t.join();
ob3.t.join();
} catch (InterruptedException e) {
System.out.println("Main thread Interrupted");
}
System.out.println("Thread One is alive: "
+ ob1.t.isAlive());
System.out.println("Thread Two is alive: "
+ ob2.t.isAlive());
System.out.println("Thread Three is alive: "
+ ob3.t.isAlive());
System.out.println("Main thread exiting.");
}
}
程序输出如下所示:
New thread: Thread[One,5,main]
New thread: Thread[Two,5,main]
New thread: Thread[Three,5,main]
Thread One is alive: true
Thread Two is alive: true
Thread Three is alive: true
Waiting for threads to finish.
One: 5
第1部分 Java语言 202 Two: 5
Three: 5
One: 4
Two: 4
Three: 4
One: 3
Two: 3
Three: 3
One: 2
Two: 2
Three: 2
One: 1
Two: 1
Three: 1
Two exiting.
Three exiting.
One exiting.
Thread One is alive: false
Thread Two is alive: false
Thread Three is alive: false
Main thread exiting.
如你所见,调用join()后返回,线程终止执行。
11.6 线程优先级
线程优先级被线程调度用来判定何时每个线程允许运行。理论上,优先级高的线程比
优先级低的线程获得更多的CPU时间。实际上,线程获得的CPU时间通常由包括优先级在
内的多个因素决定(例如,一个实行多任务处理的操作系统如何更有效的利用CPU时间)。
一个优先级高的线程自然比优先级低的线程优先。举例来说,当低优先级线程正在运行,
而一个高优先级的线程被恢复(例如从沉睡中或等待I/O中),它将抢占低优先级线程所使
用的CPU。
理论上,等优先级线程有同等的权利使用CPU。但你必须小心了。记住,Java是被设
计成能在很多环境下工作的。一些环境下实现多任务处理从本质上与其他环境不同。为安
全起见,等优先级线程偶尔也受控制。这保证了所有线程在无优先级的操作系统下都有机
会运行。实际上,在无优先级的环境下,多数线程仍然有机会运行,因为很多线程不可避
免的会遭遇阻塞,例如等待输入输出。遇到这种情形,阻塞的线程挂起,其他线程运行。
但是如果你希望多线程执行的顺利的话,最好不要采用这种方法。同样,有些类型的任务
是占CPU的。对于这些支配CPU类型的线程,有时你希望能够支配它们,以便使其他线程
可以运行。
设置线程的优先级,用setPriority()方法,该方法也是Tread 的成员。它的通常形式为:
final void setPriority(int level)
这里,level指定了对所调用的线程的新的优先权的设置。Level的值必须在
MIN_PRIORITY到MAX_PRIORITY范围内。通常,它们的值分别是1和10。要返回一个线
第11章多线程编程 203
程为默认的优先级,指定NORM_PRIORITY,通常值为5。这些优先级在Thread中都被定义
为final型变量。
你可以通过调用Thread的getPriority()方法来获得当前的优先级设置。该方法如下:
final int getPriority( )
当涉及调度时,Java的执行可以有本质上不同的行为。Windows 95/98/NT/2000 的工作
或多或少如你所愿。但其他版本可能工作的完全不同。大多数矛盾发生在你使用有优先级
行为的线程,而不是协同的腾出CPU时间。最安全的办法是获得可预先性的优先权,Java
获得跨平台的线程行为的方法是自动放弃对CPU的控制。
下面的例子阐述了两个不同优先级的线程,运行于具有优先权的平台,这与运行于无
优先级的平台不同。一个线程通过Thread.NORM_PRIORITY设置了高于普通优先级两级的
级数,另一线程设置的优先级则低于普通级两级。两线程被启动并允许运行10秒。每个线
程执行一个循环,记录反复的次数。10秒后,主线程终止了两线程。每个线程经过循环的
次数被显示。
// Demonstrate thread priorities.
class clicker implements Runnable {
int click = 0;
Thread t;
private volatile boolean running = true;
public clicker(int p) {
t = new Thread(this);
t.setPriority(p);
}
public void run() {
while (running) {
click++;
}
}
public void stop() {
running = false;
}
public void start() {
t.start();
}
}
class HiLoPri {
public static void main(String args[]) {
Thread.currentThread().setPriority(Thread.MAX_PRIORITY);
clicker hi = new clicker(Thread.NORM_PRIORITY + 2);
clicker lo = new clicker(Thread.NORM_PRIORITY - 2);
lo.start();
hi.start();
try {
第1部分 Java语言 204 Thread.sleep(10000);
} catch (InterruptedException e) {
System.out.println("Main thread interrupted.");
}
lo.stop();
hi.stop();
// Wait for child threads to terminate.
try {
hi.t.join();
lo.t.join();
} catch (InterruptedException e) {
System.out.println("InterruptedException caught");
}
System.out.println("Low-priority thread: " + lo.click);
System.out.println("High-priority thread: " + hi.click);
}
}
该程序在Windows 98下运行的输出,表明线程确实上下转换,甚至既不屈从于CPU,
也不被输入输出阻塞。优先级高的线程获得大约90%的CPU时间。
Low-priority thread: 4408112
High-priority thread: 589626904
当然,该程序的精确的输出结果依赖于你的CPU的速度和运行的其他任务的数量。当
同样的程序运行于无优先级的系统,将会有不同的结果。
上述程序还有个值得注意的地方。注意running前的关键字volatile。尽管volatile 在下章
会被很仔细的讨论,用在此处以确保running的值在下面的循环中每次都得到验证。
while (running) {
click++;
}
如果不用volatile,Java可以自由的优化循环:running的值被存在CPU的一个寄存器中,
每次重复不一定需要复检。volatile的运用阻止了该优化,告知Java running可以改变,改变
方式并不以直接代码形式显示。
11.7 线程同步
当两个或两个以上的线程需要共享资源,它们需要某种方法来确定资源在某一刻仅被
一个线程占用。达到此目的的过程叫做同步(synchronization)。像你所看到的,Java为此
提供了独特的,语言水平上的支持。
同步的关键是管程(也叫信号量semaphore)的概念。管程是一个互斥独占锁定的对象,
或称互斥体(mutex)。在给定的时间,仅有一个线程可以获得管程。当一个线程需要锁定,
它必须进入管程。所有其他的试图进入已经锁定的管程的线程必须挂起直到第一个线程退
第11章多线程编程 205
出管程。这些其他的线程被称为等待管程。一个拥有管程的线程如果愿意的话可以再次进
入相同的管程。
如果你用其他语言例如C或C++时用到过同步,你会知道它用起来有一点诡异。这是因
为很多语言它们自己不支持同步。相反,对同步线程,程序必须利用操作系统源语。幸运
的是Java通过语言元素实现同步,大多数的与同步相关的复杂性都被消除。
你可以用两种方法同步化代码。两者都包括synchronized关键字的运用,下面分别说明
这两种方法。
11.7.1 使用同步方法
Java中同步是简单的,因为所有对象都有它们与之对应的隐式管程。进入某一对象的
管程,就是调用被synchronized关键字修饰的方法。当一个线程在一个同步方法内部,所有
试图调用该方法(或其他同步方法)的同实例的其他线程必须等待。为了退出管程,并放
弃对对象的控制权给其他等待的线程,拥有管程的线程仅需从同步方法中返回。
为理解同步的必要性,让我们从一个应该使用同步却没有用的简单例子开始。下面的
程序有三个简单类。首先是Callme,它有一个简单的方法call( )。call( )方法有一个名为msg
的String参数。该方法试图在方括号内打印msg 字符串。有趣的事是在调用call( ) 打印左括
号和msg字符串后,调用Thread.sleep(1000),该方法使当前线程暂停1秒。
下一个类的构造函数Caller,引用了Callme的一个实例以及一个String,它们被分别存
在target 和 msg 中。构造函数也创建了一个调用该对象的run( )方法的新线程。该线程立
即启动。Caller类的run( )方法通过参数msg字符串调用Callme实例target的call( ) 方法。最后,
Synch类由创建Callme的一个简单实例和Caller的三个具有不同消息字符串的实例开始。
Callme的同一实例传给每个Caller实例。
// This program is not synchronized.
class Callme {
void call(String msg) {
System.out.print("[" + msg);
try {
Thread.sleep(1000);
} catch(InterruptedException e) {
System.out.println("Interrupted");
}
System.out.println("]");
}
}
class Caller implements Runnable {
String msg;
Callme target;
Thread t;
public Caller(Callme targ, String s) {
target = targ;
msg = s;
t = new Thread(this);
t.start();
第1部分 Java语言 206 }
public void run() {
target.call(msg);
}
}
class Synch {
public static void main(String args[]) {
Callme target = new Callme();
Caller ob1 = new Caller(target, "Hello");
Caller ob2 = new Caller(target, "Synchronized");
Caller ob3 = new Caller(target, "World");
// wait for threads to end
try {
ob1.t.join();
ob2.t.join();
ob3.t.join();
} catch(InterruptedException e) {
System.out.println("Interrupted");
}
}
}
该程序的输出如下:
Hello[Synchronized[World]
]
]
在本例中,通过调用sleep( ),call( )方法允许执行转换到另一个线程。该结果是三个消
息字符串的混合输出。该程序中,没有阻止三个线程同时调用同一对象的同一方法的方法
存在。这是一种竞争,因为三个线程争着完成方法。例题用sleep( )使该影响重复和明显。
在大多数情况,竞争是更为复杂和不可预知的,因为你不能确定何时上下文转换会发生。
这使程序时而运行正常时而出错。
为达到上例所想达到的目的,必须有权连续的使用call( )。也就是说,在某一时刻,必
须限制只有一个线程可以支配它。为此,你只需在call( ) 定义前加上关键字synchronized,
如下:
class Callme {
synchronized void call(String msg) {
...
这防止了在一个线程使用call( )时其他线程进入call( )。在synchronized加到call( )前面以
后,程序输出如下:
[Hello]
[Synchronized]
[World]
任何时候在多线程情况下,你有一个方法或多个方法操纵对象的内部状态,都必须用
第11章多线程编程 207
synchronized 关键字来防止状态出现竞争。记住,一旦线程进入实例的同步方法,没有其
他线程可以进入相同实例的同步方法。然而,该实例的其他不同步方法却仍然可以被调用。
11.7.2 同步语句
尽管在创建的类的内部创建同步方法是获得同步的简单和有效的方法,但它并非在任
何时候都有效。这其中的原因,请跟着思考。假设你想获得不为多线程访问设计的类对象
的同步访问,也就是,该类没有用到synchronized方法。而且,该类不是你自己,而是第三
方创建的,你不能获得它的源代码。这样,你不能在相关方法前加synchronized修饰符。怎
样才能使该类的一个对象同步化呢?很幸运,解决方法很简单:你只需将对这个类定义的
方法的调用放入一个synchronized块内就可以了。
下面是synchronized语句的普通形式:
synchronized(object) {
// statements to be synchronized
}
其中,object是被同步对象的引用。如果你想要同步的只是一个语句,那么不需要花括
号。一个同步块确保对object成员方法的调用仅在当前线程成功进入object管程后发生。
下面是前面程序的修改版本,在run( )方法内用了同步块:
// This program uses a synchronized block.
class Callme {
void call(String msg) {
System.out.print("[" + msg);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
System.out.println("Interrupted");
}
System.out.println("]");
}
}
class Caller implements Runnable {
String msg;
Callme target;
Thread t;
public Caller(Callme targ, String s) {
target = targ;
msg = s;
t = new Thread(this);
t.start();
}
// synchronize calls to call()
public void run() {
synchronized(target) { // synchronized block
target.call(msg);
}
第1部分 Java语言 208 }
}
class Synch1 {
public static void main(String args[]) {
Callme target = new Callme();
Caller ob1 = new Caller(target, "Hello");
Caller ob2 = new Caller(target, "Synchronized");
Caller ob3 = new Caller(target, "World");
// wait for threads to end
try {
ob1.t.join();
ob2.t.join();
ob3.t.join();
} catch(InterruptedException e) {
System.out.println("Interrupted");
}
}
}
这里,call( )方法没有被synchronized修饰。而synchronized是在Caller类的run( )方法中
声明的。这可以得到上例中同样正确的结果,因为每个线程运行前都等待先前的一个线程
结束。
11.8 线程间通信
上述例题无条件的阻塞了其他线程异步访问某个方法。Java对象中隐式管程的应用是
很强大的,但是你可以通过进程间通信达到更微妙的境界。这在Java中是尤为简单的。
像前面所讨论过的,多线程通过把任务分成离散的和合乎逻辑的单元代替了事件循环
程序。线程还有第二优点:它远离了轮询。轮询通常由重复监测条件的循环实现。一旦条
件成立,就要采取适当的行动。这浪费了CPU时间。举例来说,考虑经典的序列问题,当
一个线程正在产生数据而另一个程序正在消费它。为使问题变得更有趣,假设数据产生器
必须等待消费者完成工作才能产生新的数据。在轮询系统,消费者在等待生产者产生数据
时会浪费很多CPU周期。一旦生产者完成工作,它将启动轮询,浪费更多的CPU时间等待
消费者的工作结束,如此下去。很明显,这种情形不受欢迎。
为避免轮询,Java包含了通过wait( ),notify( )和notifyAll( )方法实现的一个进程间通信
机制。这些方法在对象中是用final方法实现的,所以所有的类都含有它们。这三个方法仅
在synchronized方法中才能被调用。尽管这些方法从计算机科学远景方向上来说具有概念的
高度先进性,实际中用起来是很简单的:
? wait( ) 告知被调用的线程放弃管程进入睡眠直到其他线程进入相同管程并且调用
notify( )。
? notify( ) 恢复相同对象中第一个调用 wait( ) 的线程。
? notifyAll( ) 恢复相同对象中所有调用 wait( ) 的线程。具有最高优先级的线程最先
第11章多线程编程 209 运行。
这些方法在Object中被声明,如下所示:
final void wait( ) throws InterruptedException
final void notify( )
final void notifyAll( )
wait( )存在的另外的形式允许你定义等待时间。
下面的例子程序错误的实行了一个简单生产者/消费者的问题。它由四个类组成:Q,
设法获得同步的序列;Producer,产生排队的线程对象;Consumer,消费序列的线程对象;
以及PC,创建单个Q,Producer,和Consumer的小类。
// An incorrect implementation of a producer and consumer.
class Q {
int n;
synchronized int get() {
System.out.println("Got: " + n);
return n;
}
synchronized void put(int n) {
this.n = n;
System.out.println("Put: " + n);
}
}
class Producer implements Runnable {
Q q;
Producer(Q q) {
this.q = q;
new Thread(this, "Producer").start();
}
public void run() {
int i = 0;
while(true) {
q.put(i++);
}
}
}
class Consumer implements Runnable {
Q q;
Consumer(Q q) {
this.q = q;
new Thread(this, "Consumer").start();
}
public void run() {
第1部分 Java语言 210 while(true) {
q.get();
}
}
}
class PC {
public static void main(String args[]) {
Q q = new Q();
new Producer(q);
new Consumer(q);
System.out.println("Press Control-C to stop.");
}
}
尽管Q类中的put( )和get( )方法是同步的,没有东西阻止生产者超越消费者,也没有东
西阻止消费者消费同样的序列两次。这样,你就得到下面的错误输出(输出将随处理器速
度和装载的任务而改变):
Put: 1
Got: 1
Got: 1
Got: 1
Got: 1
Got: 1
Put: 2
Put: 3
Put: 4
Put: 5
Put: 6
Put: 7
Got: 7
生产者生成1后,消费者依次获得同样的1五次。生产者在继续生成2到7,消费者没有
机会获得它们。
用Java正确的编写该程序是用wait( )和notify( )来对两个方向进行标志,如下所示:
// A correct implementation of a producer and consumer.
class Q {
int n;
boolean valueSet = false;
synchronized int get() {
if(!valueSet)
try {
wait();
} catch(InterruptedException e) {
System.out.println("InterruptedException caught");
}
System.out.println("Got: " + n);
valueSet = false;