并发


第 21 章  并发
Java 平台被设计为·1·
第 21 章  并发
Java 平台被设计为从根本上就支持并发程序,在 Java 程序设计语言和 Java 类库中有基本的并发支
持。此外, Java 平台还包括高层次的并发 API,这些高层次的并发 API 都包含在 java.util.concurrent 包中。
本章就重点讲述并发程序设计的一些基本概念及 Java 的并发程序设计。
21.1   线程管理
在计算机系统中,并发指的是一系列的任务在同一个计算机中同时运行。如果是在多个处理器的计
算机中或者多核处理器中,这种同时运行是真正地同时发生,如果是在单核处理器的计算机中,则看上
去似乎是同时发生的。
所有现代的计算机都支持执行并发任务,如阅读邮件的同时听音乐和浏览网页,这种并发称为“进
程级”并发。但是在同一个进程中,我们也能同时有多个任务发生,这种运行在一个进程中的并发任务
称为“线程”。
另外一个与并发有关的概念是“并行”。对这个概念,有许多不同的定义和理解。
21.1.1   创建并运行一个线程
这一小节学习怎样在一个 Java 应用程序中创建和运行一个线程。在 Java 中,创建一个线程有两种
方式:
  继承 Thread 类并重写 run()方法
  构建一个实现了 Runnable 接口的类,然后创建一个 Thread 类的对象,并将该 Runnable 对象作
为参数传给它
Runnable 接口定义了一个单一的方法 run(),在 run()方法中包含要在线程中执行的代码。Runnable
对象被传递给 Thread 的构造器中,如下面的 RunnableDemo.java 所示。
【例】使用 Runnable 接口创建线程。
public class RunnableDemo implements Runnable {
public void run() {    //实现 Runnable 接口,则必须实现该接口中的 run 方法
System.out.println("这是一个线程!");
}
public static void main(String[] args) {
(new Thread(new RunnableDemo())).start();  //调用 start()方法,开始执行线程
}

2
在上面的代码中,创建了两个线程:调用 main()方法创建的主线程和调用 Thread 类的 start()方法创
建的执行线程。
当一个程序中所有的线程都执行完毕后,该程序结束。更准确地讲,当该程序中所有的守护线程执
行完毕后,该程序就结束。如果主线程先结束了,那么剩下的其它线程将继续它们的执行直到结束。如
果其中一个线程使用 System.exit()结束程序的执行,则所有的线程都将结束它们的执行。
【例】在下面这个示例程序中,我们使用第二种方式,创建一个简单的程序,在程序中,创建并运
行 10 个线程,每个线程计算并打印 1 到 10 之间一个数的乘法表。
public class Calculator implements Runnable {
private int number;
public Calculator(int number){
this.number = number;
}
@Override
public void run() {
for(int i=1; i<=10; i++){
System.out.printf("%s: %d * %d = %d\n",Thread.currentThread().getName(),number,i,number*i);
}
}
public static void main(String[] args) {
for(int i=1; i<=10; i++){
Calculator calculator = new Calculator(i);
Thread thread = new Thread(calculator);
thread.start();
}
}
}
编译并执行该程序,观察输出结果(多执行几次,会观察到每次输出的顺序都是不同的)。
也可以通过继承 Thread 类来创建一个新的线程。Thread 类已经实现了 Runable 接口,不过其 run()
方法什么也不做。应用程序可以通过子类化 Thread,实现其自身的 run()方法。如下面的 ThreadDemo.java
所示。
【例】使用 Thread 的子类创建线程。
public class ThreadDemo extends Thread {
public void run() {            //重写从 Thread 继承过来的 run 方法
System.out.println("这是一个线程!");
}
public static void main(String[] args) {
(new ThreadDemo()).start();
}
}
请注意,两种创建线程的方式都通过调用 Thread.start()方法来启动新的线程。两种用法应该用哪一
·3·
种呢?对于第一种用法,使用了一个 Runnable 对象,是比较常用的一种,因为 Runnable 对象可以子类
化一个非 Thread 类。第二种方法在简单的应用程序中更容易使用,但是它限制了完成异步任务的类只
能从 Thread 类继承。
21.1.2   获取和设置线程信息
Thread 类保存有相应的属性信息,用来标识一个线程、获知其状态、控制其优先级等。这些属性是:
  ID:这个属性存储每个线程的唯一标识。
  Name:这个属性存储线程的名称。
  Priority:这个属性存储 Thread 对象的优先级。线程可以有一个 1 到 10 的优先级,其中 1 是最
低优先级,10 是最高优先级。不建议改变线程的优先级。
  Status:这个属性存储线程的状态。在 Java 中,线程有 6 种状态: new, runnable, blocked, waiting, 
time waiting,或 terminated。
【例】下面这个示例程序中,为 10 个线程指定名称和优先级,然后显示它们的状态信息直到它们
结束。这些线程将计算某个数的乘法表。
import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;
import java.lang.Thread.State;
public class ThreadManagement {
public static void main(String[] args) {
Thread[] threads = new Thread[10];  //保存 10 个线程
Thread.State[] status = new Thread.State[10];  //保存这 10 个线程的状态
for (int i = 0; i < 10; i++) {
threads[i] = new Thread(new Calculator(i));
if (i % 2 == 0) {
threads[i].setPriority(Thread.MAX_PRIORITY);     //设置为最高优先级
} else {
threads[i].setPriority(Thread.MIN_PRIORITY);     //设置为最低优先级
}
threads[i].setName("Thread" + i);
}
try (FileWriter file = new FileWriter(".\\threadlog.txt");// "threadlog.txt"
PrintWriter pw = new PrintWriter(file);) {
for (int i = 0; i < 10; i++) {
pw.println("Main: Status of Thread " + i + " : " + threads[i].getState());
status[i] = threads[i].getState();
}
//开始执行 10 个线程
for (int i = 0; i < 10; i++) {
threads[i].start();

4
boolean finish = false;
while (!finish) {
for (int i = 0; i < 10; i++) {
if (threads[i].getState() != status[i]) {
writeThreadInfo(pw, threads[i], status[i]);
status[i] = threads[i].getState();
}
}
finish = true;
for (int i = 0; i < 10; i++) {
finish = finish && (threads[i].getState() == State.TERMINATED);
}
}
} catch (IOException ex) {
ex.printStackTrace();
}
}
private static void writeThreadInfo(PrintWriter pw, Thread thread, State state) {
pw.printf("Main : Id %d - %s\n", thread.getId(), thread.getName());
pw.printf("Main : Priority: %d\n", thread.getPriority());
pw.printf("Main : Old State: %s\n", state);
pw.printf("Main : New State: %s\n", thread.getState());
pw.printf("Main : ************************************\n");
}
}
21.1.3   中断一个线程
如果一个程序有多个执行线程的话,那么只有当它所有的非守护线程结束执行,或当其中一个线程
使用 System.exit()方法时,该程序才会结束。但是有时候,我们想要终止整个应用程序,或者想要取消
一个 Thread 对象正在执行的任务,这时就需要提前结束该线程。
Java 提供了中断机制来告诉一个线程我们想要结束它。这个机制要求 Thread 必须判断它是否已经
被中断了,并且能决定是否响应中断请求。Thread 可以忽视该请求,并继续其执行。
【例】下面这个示例程序中,创建一个线程,1 秒钟以后,使用中断机制强迫它结束。
public class PrimeGenerator extends Thread {
@Override
public void run() {
long number = 1L;
while (true) {
if (isPrime(number)) {
System.out.printf("Number %d is Prime\n", number);
}
if (isInterrupted()) { 
·5·
System.out.printf("The Prime Generator has been Interrupted");
return;
}
number++;
}
}
//判断参数是否是一个素数
private boolean isPrime(long number) {
if (number <= 2) {
return true;
}
for (long i = 2; i < number; i++) {
if ((number % i) == 0) {
return false;
}
}
return true;
}
public static void main(String[] args) {
Thread task = new PrimeGenerator();
task.start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
task.interrupt();    //中断线程 task
}
}
在 Thread 类中,有一个属性存储一个 boolean 值,指定该线程是否被中断。当调用线程的 interrupt()
方法时,将这个属性设为 true。而 isInterrupted()方法返回这个属性的值。
21.1.4   控制线程的中断
Java 提供了一个 InterruptedException 异常用于控制线程的中断。当程序检测到线程的中断时,就抛
出一个 InterruptedException 异常,然后在 run()方法中捕获它。
【例】简单的线程中断示例。
import java.util.concurrent.TimeUnit;
public class InterruptThread implements Runnable{
@Override
public void run() {
System.out.println("假设这个线程要做很耗时的工作......");
try { 
6
TimeUnit.SECONDS.sleep(10);      //这里可改为休眠 2 秒钟,再运行并观察结果
System.out.println("工作完成");
} catch (InterruptedException ex) {
System.out.println("工作还没做完,被中断了");
}        
}
public static void main(String[] args) {
Thread thread = new Thread(new InterruptThread());
thread.start();
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException ex) {
ex.printStackTrace();
}
thread.interrupt();    //中断线程
}
}
执行程序,输出结果如下:
假设这个线程要做很耗时的工作......
工作还没做完,被中断了
【例】下面这是一个稍微复杂些的线程中断示例。程序中,给定一个目录,在其中递归查找指定的
文件。在查找过程中有可能查找线程被中断。
import java.io.File;
import java.util.concurrent.TimeUnit;
public class FileSearch implements Runnable {
private String initPath;     //初始文件夹
private String fileName;     //要查找的文件名
public FileSearch(String initPath, String fileName) {
this.initPath = initPath;
this.fileName = fileName;
}
//判断属性 fileName 是否是一个目录。如果是目录的话,调用方法 processDirectory().
//该方法会抛出一个 InterruptedException 异常,  这样我们就必须捕获该异常
@Override
public void run() {
File file = new File(initPath);
if (file.isDirectory()) {
try {
directoryProcess(file);
} catch (InterruptedException e) {
System.out.printf("%s: The search has been interrupted", Thread.currentThread().getName());

·7·
}
}
private void directoryProcess(File dir) throws InterruptedException {
File[] list = dir.listFiles();
if (list != null) {
for (int i = 0; i < list.length; i++) {
if (list[i].isDirectory()) {
directoryProcess(list[i]);
} else {
fileProcess(list[i]);
}
}
}
if (Thread.interrupted()) {
throw new InterruptedException();
}
}
private void fileProcess(File file) throws InterruptedException {
if (file.getName().equals(fileName)) {
System.out.printf("%s : %s\n", Thread.currentThread().
getName(), file.getAbsolutePath());
}
//当一个线程通过调用静态方法 Thread.interrupted()检查中断时,中断状态被清除。
//非静态的方法 isInterrupted()被一个线程使用来查询另一个线程的中断状态,而不改变中断状态标记。
if (Thread.interrupted()) {
throw new InterruptedException();
}
}
public static void main(String[] args) {
FileSearch searcher = new FileSearch("C:\\", "autoexec.bat");
Thread thread = new Thread(searcher);
thread.start();
try {
TimeUnit.SECONDS.sleep(10); //休眠 10 秒钟
} catch (InterruptedException e) {
e.printStackTrace();
}
thread.interrupt(); //中断线程 thread
}
}
8
21.1.5   暂停线程的执行
有的时候,我们需要一个线程暂时停止执行,比如,某个程序中的线程每分钟检测一次传感器的状
态。在其余的时间,该线程什么也不做,也不消耗计算机的资源。这可以通过调用 Thread 类的 sleep()
方法实现。
Thread.sleep()方法可以引起当前线程挂起执行一个指定的时期。这意味着处理时间可用于程序其他
线程,或者运行在计算机系统内的其他应用程序。sleep()方法还被用于控制步调、等待另外的线程完成
有时间要求的任务。
有两个重载的 sleep()方法。一个指定休眠的时间毫秒,另一个指定休眠的时间为纳秒(十亿分之一
秒)。不过,这些休眠时间并不保证精确,这由后台的操作系统所提供的工具所限制。另外,休眠期也
可以被中断信息号所终结。
在任何情况下,都不能假定对 sleep()方法的调用会精确地按指定的时间周期挂起线程。下面的
SleepMessagesDemo.java 示例使用 sleep()方法每 3 秒输出一个消息。
【例】使用 Thread.sleep 方法,每隔 3 秒打印输出一个消息。
public class SleepMessagesDemo{
public static void main(String[] args) throws InterruptedException {
String messages[] = {
"消息 1",
"消息 2",
"消息 3",
"消息 4"
};
for (int i = 0; i < messages.length; i++) {            
Thread.sleep(3000);          //执行到此暂停 3 秒
System.out.println(messages[i]);    //输出一个消息
}
}
}
在上面的程序代码中,main()方法声明其抛出 InterruptedException 异常。当另一个线程中断了当前
线程的 sleep 状态时, sleep()方法会抛出此异常。因为这个应用程序并没有定义另外的线程,所以不必担
心会引起中断和捕获 InterruptedException 异常。
另一种可能的方式是使用 TimeUnit 枚举类型的 sleep()方法。这个方法使用 Thread 类的 sleep()方法
将当前线程置于休眠状态,但是接收的参数可以是指定的时间单元,然后会自动将其转为毫秒。
【例】使用方法,每秒打印一个日期。
import java.util.Date;
import java.util.concurrent.TimeUnit;
public class FileClock implements Runnable {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.printf("%s\n", new Date().toLocaleString());
try {
TimeUnit.SECONDS.sleep(1); 
·9·
} catch (InterruptedException e) {
System.out.printf("The FileClock has been interrupted");
}
}
}
public static void main(String[] args) {
FileClock clock = new FileClock();
Thread thread = new Thread(clock);
thread.start();
}
}
注:java.util.concurrent.TimeUnit 是一个枚举类型,它定义了许多表示时间粒度的枚举值。
Thread.sleep(),是一个静态方法,暂停线程时它不会释放锁,该方法会抛出 InterrupttedException 异
常(如果有线程中断了当前线程)。但是因为 Thread.sleep()方法的参数是毫秒,因此可读性不强。
TimeUnit 类通过指定 DAYS、 HOURS、 MINUTES,SECONDS、 MILLISECONDS 和 NANOSECONDS
这些枚举值解决了这个问题。java.utils.concurrent  .TimeUnit  是 Java 枚举应用场景中最好的例子之一,
所有 TimeUnit 都是枚举实例
让我们来看看线程睡眠 4 分钟用 TimeUnit 是如何使用的:
TimeUnit.MINUTES.sleep(4);   //休眠 4 分钟
TimeUnit.sleep()内部调用的 Thread.sleep()也会抛出 InterruptException。
除了 sleep()的功能外,TimeUnit 还提供了许多便捷方法用于把时间转换成不同单位,例如,如果你
想把秒转换成毫秒,你可以使用下面代码:
TimeUnit.SECONDS.toMillis(44);
它将返回 44,000
21.1.6  等待一个线程结束
线程对象的 join()方法允许一个线程等待另外一个线程的完成。例如,一个当前正在执行的线程 t,
它是 Thread 对象,如果调用其 join()方法,如下所示。
t.join();
这时会引起当前的线程暂停执行,直到 t 的线程终结为止。重载 join()方法允许程序员来指定一个等
待的周期。 不过与 sleep()一样, join 指定的这个时间也依赖于操作系统,所以不应该假设 join()方法会精
确地等待指定的时间。
与 sleep()方法一样,join()方法响应中断时,会抛出一个 InterruptedException 并退出。
【例】例如,首先看下面这段代码,并判断其输出结果:
public class ThreadTest implements Runnable {
public static int a = 0;           //定义静态变量
public void run() {            //在 run 方法里,循环增加静态变量 a 的值
for (int k = 0; k < 5; k++) {
a = a + 1;
}
}
public static void main(String[] args) throws Exception { 
10
Runnable r = new ThreadTest();      //创建 ThreadTest 对象
Thread t = new Thread(r);        //创建线程 t
t.start();              //启动线程 t
System.out.println(a);          //输出静态变量 a 的值
}
}
编译并运行上述程序,输出结果是 5 吗?答案是:有可能。其实很难遇到输出 5 的时候,通常情况
下都不是 5(当然这也和机器有密切的关系)。因为当主线程 main()方法执行“System.out.println(a);”
这条语句时,线程还没有真正开始运行(或许正在为它分配资源准备运行吧)。因为为线程分配资源需
要时间,而 main()方法执行完 t.start()方法后继续往下执行“System.out.println(a);”,这个时候得到的结
果是 a 还没有被改变的值 0。
如果想让输出结果为 5,就需要调用 join()方法。当调用 join()方法时,当前的 main 线程会阻塞执行,
直到调用该方法的线程执行完毕 main 线程才会继续执行。
【例】使用 join()方法以后的代码示例:
public class ThreadTest implements Runnable {
public static int a = 0;           //声明静态变量 a
public void run() {
for (int k = 0; k < 5; k++) {
a = a + 1;
}
}
public static void main(String[] args) throws Exception {
Runnable r = new ThreadTest();
Thread t = new Thread(r);        //创建另一个线程 t
t.start();              //启动线程 t
t.join();              //当前线程阻塞执行,等待线程 t 执行完毕
System.out.println(a);
}
}
在调用线程 t 的 start()方法启动线程 t 以后,先调用 t.join()方法,等待线程 t 结束, main 线程才继续
执行后续的“System.out.println(a);”方法,输出 a 的值,此时一定会输出结果 5。
21.1.7  创建并运行一个守护线程
Java 有一个特殊类型的线程,称为“守护线程”。这种线程的优先级很低,通常只有在同一程序中
没有其它线程运行时它才被执行。
当程序中只剩下守护线程在运行时, JVM 就会结束程序,终止这些线程。比如,垃圾回收器就是一
个典型的守护线程。
【例】创建两个线程。一个用户线程向一个队列中写入事件,另一个守护线程清除该队列,将生成
时间超过 10 秒的事件移除。
1)首先创建一个事件对象 Event.java。
import java.util.Date;
public class Event {
private Date date; 
·11·
private String event;
public Date getDate() {
return date;
}
public void setDate(Date date) {
this.date = date;
}
public String getEvent() {
return event;
}
public void setEvent(String event) {
this.event = event;
}
}
2)下面创建一个用户线程。 在这个线程中,循环迭代 100 次。在每次迭代中,我们创建一个新的 Event
对象,并将它保存到队列中,然后休眠 1 秒钟。
import java.util.Date;
import java.util.Deque;
import java.util.concurrent.TimeUnit;
public class WriterTask implements Runnable {
private Deque<Event> deque;
public WriterTask(Deque<Event> deque) {
this.deque = deque;
}
@Override
public void run() {
for (int i = 1; i < 100; i++) {
Event event = new Event();
event.setDate(new Date());
event.setEvent(String.format("该线程%s  已经生成了一个事件", Thread.currentThread().getId()));
deque.addFirst(event);
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

12
3)创建守护线程。 该守护线程获取队列中最后一个 Event 对象,如果该对象创建时间超过 10 秒,就
将其删除并检查下一个事件。如果一个事件被删除了,守护线程就将该事件的信息和队列的新的大小打
印出来。
import java.util.Date;
import java.util.Deque;
public class CleanerTask extends Thread {
private Deque<Event> deque;
public CleanerTask(Deque<Event> deque) {
this.deque = deque;
setDaemon(true);  //将当前线程设为守护线程
}
@Override
public void run() {
while (true) {
Date date = new Date();
clean(date);
}
}
private void clean(Date date) {
long difference;
boolean delete;
if (deque.size() == 0) {
return;
}
delete = false;
do {
Event e = deque.getLast();
difference = date.getTime() - e.getDate().getTime();
if (difference > 10000) {
System.out.printf("Cleaner: %s\n", e.getEvent());
deque.removeLast();
delete = true;
}
} while (difference > 10000);
if (delete) {
System.out.printf("Cleaner: Size of the queue: %d\n", deque.size());
}
}
}
4)测试。
import java.util.ArrayDeque;
import java.util.Deque;
public class DaemonThread { 
·13·
public static void main(String[] args) {
Deque<Event> deque = new ArrayDeque<Event>();
WriterTask writer = new WriterTask(deque);
for (int i = 0; i < 3; i++) {
Thread thread = new Thread(writer);
thread.start();
}
CleanerTask cleaner = new CleanerTask(deque);
cleaner.start();
}
}
要注意的是,只能在调用 start()方法之前调用 setDaemon()方法。一旦线程开始运行,就不能修改其
守护状态。
可以使用 isDaemon()方法来判断一个线程是守护线程(当该方法返回 true 时)还是用户线程(当该方法
返回 false 时)。
21.1.8   处理线程中的异常
在 Java 中有两种类型的异常:
  检查异常:这类异常必须在方法的 throws 语句中指明,或者在方法内捕获。例如, IOException
或 ClassNotFoundException。
  未检查异常:这类异常不必指明或捕获。例如,NumberFormatException。
当在 Thread 对象的 run()方法中抛出一个检查异常时,我们必须捕获得处理它们,因为 run()方法不
接收 throw 语句。当在 Thread 对象的 run()方法中抛出一个未经检查的异常时,默认的行为是在控制台
输出异常栈信息并退出程序。
不过,Java 提供的有捕获和处理这种 Thread 对象中所抛出的未检查异常的机制,以避免程序结束。
通过下面的示例程序,学习和掌握这种机制。
【例】Thread 中未检查异常的处理机制。
1)首先,实现一个处理未检查异常的类。这个类必须实现 UncaughtExceptionHandler 接口,并实现
该接口中声明的 uncaughtException()方法。
import java.lang.Thread.UncaughtExceptionHandler;
public class ThreadExceptionHandler implements UncaughtExceptionHandler {
@Override
public void uncaughtException(Thread t, Throwable e) {
System.out.printf("捕获到一个异常\n");
System.out.printf("Thread: %s\n", t.getId());
System.out.printf("Exception: %s: %s\n", e.getClass().getName(), e.getMessage());
System.out.printf("Stack Trace: \n");
e.printStackTrace(System.out);
System.out.printf("Thread status: %s\n", t.getState());
}
14
}
2)接下来,实现一个能抛出一个未检查异常的类 Task.java。该类实现 Runnable 接口,在其实现的
run()方法中抛出一个未检查类型异常。
public class Task implements Runnable {
@Override
public void run() {
int numero = Integer.parseInt("KKK");
}
}
3)测试。
public class DaemonThread {
public static void main(String[] args) {
Task task = new Task();
Thread thread = new Thread(task);
thread.setUncaughtExceptionHandler(new ThreadExceptionHandler());
thread.start();
}
}
执行程序,输出结果如下:
捕获到一个异常
Thread: 8
Exception: java.lang.NumberFormatException: For input string: "KKK"
Stack Trace: 
java.lang.NumberFormatException: For input string: "KKK"
at java.lang.NumberFormatException.forInputString(NumberFormatException.java:65)
at java.lang.Integer.parseInt(Integer.java:492)
at java.lang.Integer.parseInt(Integer.java:527)
at com.derun.chapter01.Task.run(Task.java:7)
at java.lang.Thread.run(Thread.java:724)
Thread status: RUNNABLE
21.1.9   使用线程局部变量
并发应用程序最重要的方面之一是共享数据。在继承了 Thread 类或实现了 Runnable 接口的这些对
象中这特别重要。
如果创建一个实现了 Runnable 接口的类的对象,然后使用同一个 Runnable 对象启动各个 Thread 对
象,那么所有这些线程共享相同的属性。这意味着,如果在一个线程中改变了一个属性,那么所有的线
程都将会受到影响。
有时候,我们可能想要一个属性不被运行同一对象的所有线程所共享。在 Java 并发 API 中提供有
一个非常简洁的机制,称为线程局部变量,它有很好的性能。
【例】线程共享数据时出现的问题。
1)创建一个名为 UnsafeTask 的类,并指定它实现 Runnable 接口。在该类中声明一个私有属性变量。  
·15·
import java.util.Date;
import java.util.concurrent.TimeUnit;
public class UnsafeTask implements Runnable {
private Date startDate;
@Override
public void run() {
startDate = new Date();
System.out.printf("启动线程: %s : %s\n", Thread.
currentThread().getId(), startDate.toLocaleString());
try {
TimeUnit.SECONDS.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.printf("线程结束: %s : %s\n", Thread.
currentThread().getId(), startDate.toLocaleString());
}
}
2)测试。在 main()方法中,创建一个 UnsafeTask 类的对象,并使用该对象启动三个线程,每个线程
间隔 2 秒钟。
import java.util.Date;
import java.util.concurrent.TimeUnit;
public class Test {
public static void main(String[] args) {
UnsafeTask task = new UnsafeTask();
for (int i = 0; i < 3; i++) {
Thread thread = new Thread(task);
thread.start();
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
3)运行。输出结果如下:
启动线程: 8 : 2014-2-19 15:09:14
启动线程: 9 : 2014-2-19 15:09:16
启动线程: 10 : 2014-2-19 15:09:18
线程结束: 8 : 2014-2-19 15:09:18
线程结束: 9 : 2014-2-19 15:09:18
线程结束: 10 : 2014-2-19 15:09:18
可以看得出来,虽然每个线程中 startDate 属性的起始值不一样,但结束时都是一样的。因为这些线
16
程共享同一个属性变量,所以对一个线程中变量值的改变会影响到所有的线程。
可以使用线程局部变量来解决这个问题。
【例】使用线程局部变量解决线程共享数据时出现的问题。
1)创建一个名为 SafeTask 的类,并指定它实现 Runnable 接口。在该类中声明一个线程局部变量。
import java.util.Date;
import java.util.concurrent.TimeUnit;
public class SafeTask implements Runnable {
private static final ThreadLocal<Date> startDate = new ThreadLocal<Date>() {
@Override
protected Date initialValue() {
return new Date();
}
};
@Override
public void run() {
System.out.printf("启动线程: %s : %s\n", Thread.
currentThread().getId(), startDate.get().toLocaleString());
try {
TimeUnit.SECONDS.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.printf("线程结束: %s : %s\n", Thread.
currentThread().getId(), startDate.get().toLocaleString ());
}
}
2)测试。在 main()方法中,创建一个 UnsafeTask 类的对象,并使用该对象启动三个线程,每个线程
间隔 2 秒钟。
import java.util.Date;
import java.util.concurrent.TimeUnit;
public class Test {
public static void main(String[] args) {
UnsafeTask task = new UnsafeTask();
for (int i = 0; i < 3; i++) {
Thread thread = new Thread(task);
thread.start();
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

·17·
3)运行。输出结果如下:
启动线程: 8 : 2014-2-19 15:22:53
启动线程: 9 : 2014-2-19 15:22:55
启动线程: 10 : 2014-2-19 15:22:57
线程结束: 8 : 2014-2-19 15:22:53
线程结束: 9 : 2014-2-19 15:22:55
线程结束: 10 : 2014-2-19 15:22:57
可以看到,三个线程各自有自己的变量值,互不影响。
线程局部变量为每个使用该变量的 Thread 存储一个属性值。可以使用 get()方法读取该值,使用 set()
方法改变该值。当第一次在某个线程中访问线程局部变量中存储的属性值时,如果该线程中的该属性没
有值,则线程局部变量调用 initialValue()方法为该线程赋一个值,并返回初始值。
ThreadLocal 类还提供有一个 remove()方法,用来删除调用线程存储在线程局部变量中的值。
21.1.10   线程组
线程组是指可以将一组线程作为一个单独的单元来处理,可以访问其中的一个线程来对整个组进行
操作。
Java 提供有 ThreadGroup 类来处理线程组。一个 ThreadGroup 对象由 Thread 对象或其它 ThreadGroup
对象组成,生成一个线程树结构。
下面将通过 ThreadGroup 对象开发一个简单的示例。
【例】在一个随机的时间周期内(例如,模拟搜索),有 10 个线程正在休眠,当其中一个线程结束时,
同时也就终止其余的线程。
1)首先,创建一个名为 Result 的类。这个类将存储第一个结束的线程的名字。在该类中声明一个
private String name 属性。
public class Result {
private String name;     //存储第一个结束的线程
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}   
}
2)创建一个名为 SearchTask 的类,并指定其实现 Runnable 接口。 在实现的 run()方法中,调用 doTask()
方法模拟当前线程正在执行的搜索任务。当某个线程第一个结束(完成搜索任务)时,其它线程就没有必
要再搜索下去了。
package com.derun.chapter01;
import java.util.Date;
import java.util.Random;
import java.util.concurrent.TimeUnit;
18
public class SearchTask implements Runnable {
private Result result;
public SearchTask(Result result) {
this.result = result;
}
//调用 doTask()方法直到其结束或出现 InterruptedException 异常,并输出当前线程启动、结束或中断的信息
@Override
public void run() {
String name = Thread.currentThread().getName();
System.out.printf("线程%s:  启动\n", name);
try {
doTask();    //模拟当前线程所执行的搜索任务
result.setName(name);
} catch (InterruptedException e) {
System.out.printf("线程%s:  被中断了\n", name);
return;
}
System.out.printf("线程%s:  结束\n", name);
}
//下面这个方法创建一个 Random 对象,生成一个随机数,并调用 sleep()方法休眠一个随机时间
//模拟不同的搜索时间
private void doTask() throws InterruptedException {
Random random = new Random((new Date()).getTime());
int value = random.nextInt(100);     //生成[0,100]之间的整数
System.out.printf("线程%s 休眠时间(秒)-模拟搜索进行的时间: %d\n", 
Thread.currentThread().getName(), value);
TimeUnit.SECONDS.sleep(value);
}
}
3)编写 main()方法。
import java.util.concurrent.TimeUnit;
public class Test {
public static void main(String[] args) {
ThreadGroup threadGroup = new ThreadGroup("Searcher");
Result result = new Result();
SearchTask searchTask = new SearchTask(result);
//创建 5 个线程对象,指定它们属于同一个线程组
for (int i = 0; i < 5; i++) {
Thread thread = new Thread(threadGroup, searchTask);
thread.start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) { 
·19·
e.printStackTrace();
}
}
System.out.printf("线程组中线程数量: %d\n", threadGroup.activeCount());
System.out.printf("关于线程组的信息\n");
threadGroup.list(); //将有关此线程组的信息打印到标准输出
//int activeCount() :返回此线程组中活动线程的估计数
Thread[] threads = new Thread[threadGroup.activeCount()];
//enumerate()方法:把此线程组及其子组中的所有活动线程复制到指定数组中
threadGroup.enumerate(threads);
for (int i = 0; i < threadGroup.activeCount(); i++) {
System.out.printf("线程%s: %s\n", threads[i].getName(), threads[i].getState());
}
//等待线程组中某个线程结束
waitFinish(threadGroup);
threadGroup.interrupt();     //中断此线程组中的所有线程
}
private static void waitFinish(ThreadGroup threadGroup) {
//当线程数少于 5 时,说明已经有一个线程结束了
while (threadGroup.activeCount() > 4) {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
4)编译运行,并观察输出结果。
ThreadGroup 类存储 Thread 对象和其它相关联的 ThreadGroup 对象,因此它能访问到它们所有的信
息(比如,状态),并在其成员上执行操作(如中断)。
21.1.11   在线程组上处理异常
这一节,学习如何捕获由 ThreadGroup 类中的任何 Thread 所抛出的未捕获的异常。
【例】处理线程组中抛出的未捕获异常。
1)创建一个名为 MyThreadGroup 的类,它继承自 ThreadGroup,并重写 uncaughtException()方法。
public class MyThreadGroup extends ThreadGroup {
public MyThreadGroup(String name) {
super(name);

20
//当 ThreadGroup 线程组中有一个线程抛出异常时,这个方法就会被调用
@Override
public void uncaughtException(Thread t, Throwable e) {
System.out.printf("线程%s 抛出了一个异常\n", t.getId());
e.printStackTrace(System.out);
System.out.printf("终止线程的其余部分\n");
interrupt();
}
}
2)创建一个名为 Task 的类,并实现 Runnable 接口。在该接口的 run()方法实现中,有可能会产生一
个 ArithmeticException 异常(随机产生除数,有可能值为 0)。
import java.util.Random;
public class Task implements Runnable {
@Override
public void run() {
int result;
Random random = new Random(Thread.currentThread().getId());
while (true) {
result = 1000 / random.nextInt(100);    //有可能除以 0
System.out.printf("%s : %d\n", Thread.currentThread().getId(), result);
//如果当前线程被中断
if (Thread.currentThread().isInterrupted()) {
System.out.printf("%d :  被中断\n", Thread.currentThread().getId());
return;
}
}
}
}
3)测试。
import java.util.concurrent.TimeUnit;
public class Test {
public static void main(String[] args) {
MyThreadGroup threadGroup = new MyThreadGroup("MyThreadGroup");
Task task = new Task();
for (int i = 0; i < 2; i++) {
Thread t = new Thread(threadGroup, task);
t.start();
}
}
}
编译并运行该程序,会看到当线程组中某个线程对象抛出异常时,其余线程都被中断。 
·21·
当在 Thread 中抛出未捕获异常时,JVM 会查找三个用于该异常的处理器。
首先, 它会查找线程的未捕获异常处理器(handler)。如果这个处理器不存在,那么 JVM 会查找该线
程所在线程组的未捕获异常处理器(如这一节所学到的)。如果这个方法也不存在,则 JVM 会查找默认的
未捕获异常处理器。如果所有的处理器都不存在,则 JVM 会将异常栈信息写出到控制台,并退出程序。
21.1.12   使用线程工厂类
工厂模式是面向对象编程世界里最有用的设计模式之一。它是一个创建模式,其目标是开发一个对
象,该对象的任务是创建其它类的对象。然后,当我们想要创建其它类的对象时,就可以使用工厂而不
是使用 new 运算符。
使用工厂模式有如下几个好处:
  很容易改变对象创建类,或我们创建这些对象的方式。
  很容易将对象的创建限制在有限的资源。
  很容易生成创建对象的静态数据。
Java 提供一个接口 ThreadFactory,实现了一个 Thread 对象工厂。一些 Java 并发 API 的高级工具使
用线程工厂来创建线程。
在这一节,我们将学习怎样实现一个 ThreadFactory 接口来创建 Thread 线程对象,并统计所创建线
程对象的数量。
【例】使用线程工厂类创建线程。
1)创建一个实现 ThreadFactory 接口的类 MyThreadFactory,并重写 newThread(Runnable r)方法。
import java.util.*;
import java.util.concurrent.ThreadFactory;
public class MyThreadFactory implements ThreadFactory {
private int counter;         //统计创建的线程对象数量
private String name;         //所创建的每个线程的基名
private List<String> stats;      //保存所创建线程的状态数据
public MyThreadFactory(String name) {
counter = 0;
this.name = name;
stats = new ArrayList<>();
}
@Override
public Thread newThread(Runnable r) {
Thread t = new Thread(r, name + "-Thread_" + counter);
counter++;
stats.add(String.format("创建的线程 id:%d,名称:%s,创建时间:%s\n", t.getId(), 
t.getName(), new Date().toLocaleString()));
return t;
}
//获取所创建线程的统计信息 
22
public String getStats() {
StringBuffer buffer = new StringBuffer();
Iterator<String> it = stats.iterator();
while (it.hasNext()) {
buffer.append(it.next());
buffer.append("\n");
}
return buffer.toString();
}
}
2)创建一个名为 Task 的类,并指定其实现 Runnable 接口。
import java.util.concurrent.TimeUnit;
public class Task3 implements Runnable {
@Override
public void run() {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
3)测试。
public class Test {
public static void main(String[] args) {
MyThreadFactory factory = new MyThreadFactory("MyThreadFactory");
Task task = new Task();
System.out.printf("启动 10 个线程......\n");
for (int i = 0; i < 10; i++) {
Thread thread = factory.newThread(task);
thread.start();
}
System.out.printf("工厂状态:\n");
System.out.printf("%s\n", factory.getStats());
}
}
ThreadFactory 接口只有一个名为 newThread 的方法,它接口一个 Runnable 对象作为参数,并返回
一个 Thread 线程对象。当我们实现一个 ThreadFactory 接口时,就必须重写该方法。大多数基本的
ThreadFactory 只有一行代码:
return new Thread(r);
在 newThread 方法中,我们可以实现一些复杂的要求。比如:
  创建个性化的线程,如上面的示例所示,使用一个特定格式的名称,或者创建我们自己的继承
自 Thread 类的线程。 
·23·
  保存线程创建的统计信息。
  限制所创建的线程的数量。
  验证线程的创建。
使用工厂设计模式是一个很好的编程实践,但是,如果实现一个 ThreadFactory 接口来集中创建线
程,那么就必须检查代码以确保所有的线程是使用该工厂类创建的。
21.2  基本的线程同步
在并发程序中最常见的一种情况是多个执行线程共享一个资源。在一个并发应用程序中,多个线程
读写相同的数据或访问相同的文件或数据库连接是很正常的事情。 这些共享资源有可能会造成两种类型
的错误:线程冲突和数据不一致(内存一致性错误)。因此,我们必须要防止这些错误,这就需要使用同
步机制,以确保访问共享资源的代码块不能同时被多个线程执行。
当一个线程想要执行访问共享资源的代码块时,它使用同步机制来找出是否有其它线程正在执行该
代码块。如果没有,该线程就进入这个访问共享资源的代码块执行。换句话说,如果有其它线程正在执
行访问共享资源的代码块,那么当前线程就会被挂起,直到其它线程结束对访问共享资源代码块的执行。
当有超过一个的线程在等待某个线程结束其对访问共享资源代码块的执行时, JVM 会从中选择一个,而
其余的依次等待。
Java 语言提供两个基本的同步机制:
  关键字 synchronized
  Lock 接口及其实现
21.2.1   线程冲突
在理解线程冲突的含义之前,先来看一个简单的类 Counter 的定义:
class Counter {
private int c = 0;
public void increment() {
c++;
}
public void decrement() {
c--;
}
public int value() {
return c;
}
}
在类 Count 中,每当调用 increment()方法时,变量 c 的值就加 1,每当调用 decrement()方法时,变
24
量 c 的值就会减 1。这在单一的线程中没有问题,但是如果从多个不同的线程中引用 Counter 对象并调
用 increment()方法和 decrement()方法时,变量 c 的值可能就不是这样改变的。
当在不同的线程中执行两个操作进行交叉存取,但操作对象是同一数据时,就会发生冲突。例如,
在单独的线程中,对表达式 c++的操作可以分解为三步:
(1)获得变量 c 当前的值。
(2)将获得的变量 c 的值加 1。
(3)将改变(增加)以后的值再赋给变量 c。
表达式 c--的操作也可以分解为这三步。
现在假设一个线程 A 调用方法 increment(),同时另一个线程 B 也在同一时刻调用方法 decrement()。
如果变量 c 的初始值为 0,那么就可能会发生以下的交叉操作:
(1)线程 A 获得变量 c 的当前值(为 0)。
(2)线程 B 获得变量 c 的当前值(为 0)。
(3)线程 A 将获得的值增加 1,增加结果为 1。
(4)线程 B 将获得的值减少 1,减少结果为-1。
(5)线程 A 将增加后的值保存回变量 c,此时 c 的值改变为 1。
(6)线程 B 将减少后的值保存回变量 c,此时 c 的值改变为-1。
操作的结果是,线程 A 的值最后丢失了,被线程 B 的值所覆盖。在实际执行过程中,也可能是线
程 B 的值丢失而最后保留线程 A 的值,或者根本就没有错误。因为不可预期,所以线程冲突这种 bug
很难被检测和修正。
上面的问题所反映的实质是,在 Java 的多线程程序中, 当两个或多个线程同时访问同一个变量,并
且一个线程需要修改这个变量时,应对这样的问题做出处理,否则可能发生混乱。
21.2.2   内存一致性错误
当不同的线程对同一数据获得不同的值时,就会发生内存一致性错误。既两个线程对同一内存的读
写相互之间是不可见的。例如,假设定义了一个简单的 int 变量并初始化如下。
int counter = 0;
假设两个不同的线程 A 和 B 共享变量 counter。如果线程 A 执行改变变量 counter 的值的操作,如
下代码所示。
counter++;
而同时线程 B 执行输出变量 counter 值的操作,如下代码所示。
System.out.println(counter);
那么输出结果可能为 1,但是也有可能为 0,因为线程 B 在输出变量 counter 的值的时候,可能并不
知道线程 A 改变了变量 counter 的值。
要避免内存不一致性错误,关键是在线程之间建立一种 happens-before 关系,这种关系可以保证一
个语句对内存的写操作可以被另外一个指定的语句所知道。要创建这种 happens-before 关系,可以通过
同步来实现。在以下两种情况下,会自动创建 happens-before 关系:
  当一个语句调用 Thread.start()时,每一个与此语句有 happens-before 关系的语句,都会与新创建
的线程所执行的每一个语句建立 happens-before 关系。因此导致新的线程创建的代码所发生的
操作对新的线程也是可见的。
  当一个线程停止并调用另一个线程的 Thread.join()时,所有停止线程中执行的语句与 join()成功
后的所有语句都有一个 happens-before 关系。因此线程中的操作的结果对执行 join()以后的线程
·25·
都是可见的。
21.2.3   同步方法
同步方法指的是使用关键字 synchronized 控制对一个方法的并发访问。只有一个执行线程能访问一
个对象中使用 synchronized 关键字声明的方法。如果另一个线程也试图访问同一个对象中任何使用
synchronized 关键字声明的方法,它将会被挂起,直到第一个线程结束对该方法的执行。
换句话说,每个使用关键字 synchronized 关键字声明的方法都是一个访问共享资源的代码块,Java
只允许一个对象中一个该代码块的执行。静态方法有不同的行为。只有一个执行线程能访问一个对象中
使用 synchronized 关键字声明的静态方法,但是其它线程能访问这个对象中其它非静态方法。对于这一
点一定要非常小心,因为两个线程能访问两个不同的同步方法,如果一个是静态的而另一个是非静态的。
如果两个方法改变了同一个数据,就会发生数据不一致错误。
在下面的示例中,两个线程访问一个共同的对象。我们通过同步方法来保证数据的一致性。
【例】有一个银行账号和两个线程。一个线程用来向账号中存款,另一个线程从同一个账号中取款。
如果不使用同步方法,我们可能会得到不正确的结果。使用同步机制能确保最终的账户平衡。
1)  创建一个名为 Account 的类,它将代表我们的银行账号。它只有一个属性 balance,代表账户余
额。另外实现一个 addAmount()的方法,该方法为 balance 增加一定数量的值(代表存款操作)。
再实现一个名为 subtractAmount()的方法,它将 balance 的值减少一定数量(代表取款操作)。同一
时刻只能有一个线程改变该 balance 的值,所以使用 synchronized 关键字来同步这两个方法。
import java.util.concurrent.TimeUnit;
public class Account {
private double balance;    //代表账户余额
public double getBalance() {
return balance;
}
public void setBalance(double balance) {
this.balance = balance;
}
//同步方法,代表存款操作
public synchronized void addAmount(double amount) {
double tmp = balance;
try {
TimeUnit.MILLISECONDS.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
tmp += amount;
balance = tmp;
}
//同步方法,代表取款操作 
26
public synchronized void subtractAmount(double amount) {
double tmp = balance;
try {
TimeUnit.MILLISECONDS.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
tmp -= amount;
balance = tmp;
}
}
2)实现一个模拟 ATM 机的类。它将使用 subtractAmount()方法来减少一个账户的余额。这个类必须
实现 Runnable 接口,以作为线程被执行。
public class Bank implements Runnable {
private Account account;     //要操作的银行账户
public Bank(Account account) {
this.account = account;
}
@Override
public void run() {
//调用 100 次账户 account 的 subtractAmount()方法来减少账户余额
for (int i = 0; i < 100; i++) {
account.subtractAmount(1000);
}
}
}
3)实现一个类 Company,模拟一个公司,它使用 Account 类的 addAmount()方法来增加该账户的余
额(存款操作)。这个类必须实现 Runnable 接口,以作为线程被执行。
public class Company implements Runnable {
private Account account;     //要操作的银行账户
public Company(Account account) {
this.account = account;
}
@Override
public void run() {
//调用 100 次账户 account 的 addAmount()方法来增加账户余额
for (int i = 0; i < 100; i++) {
account.addAmount(1000);
}
}
·27·
}
4)编写测试类。
public class Test {
public static void main(String[] args) {
Account account = new Account();
account.setBalance(1000);    //将账户余额初始化为 1000
Company company = new Company(account);
Thread companyThread = new Thread(company);
Bank bank = new Bank(account);
Thread bankThread = new Thread(bank);
System.out.printf("账户:  初始余额: %f\n", account.getBalance());
//启动线程
companyThread.start();
bankThread.start();
//使用 join()方法等待两个线程结束,并在控制台打印最终的账户余额
try {
companyThread.join();
bankThread.join();
System.out.printf("账户:  最终余额: %f\n", account.getBalance());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
编译并执行程序,输出结果如下:
账户:  初始余额: 1000.000000
账户:  最终余额: 1000.000000
可以看到,在多个线程分别对同一个账户进行存取款操作后,账户余额仍然保持平衡。试着将
Account 类中的 synchronized 关键字取消掉,然后再运行程序,并观察输出结果。
正如我们之前提到的,只有一个线程可以访问一个对象中使用 synchronized 关键字声明的方法。如
果一个线程(A)正在执行一个同步方法,而另一个线程(B)想要执行同一对象中其它同步方法,那么线程
B 将被阻塞直到线程 A 结束。但是如果线程 B 访问了同一个类的不同对象,则不存在阻塞问题。
当同步方法退出时,它会自动地与任何对同一对象同步方法的后续调用建立 happens-before 关系。
这保证对象状态的改变对所有的线程都是可见的。
另外,不能将构造器方法声明为同步的,这没有意义,而且是一个语法错误。同步方法为防止线程
冲突和内存一致性错误提供了简单而有用的策略:如果一个对象可以被多个线程使用,那么对该对象的
变量的所有读写都要通过同步方法来实现。只有一个例外:当变量被定义为 final 时,这时是不能修改
final 类型的变量的,所以可以通过非同步方法安全地访问这一类型的变量。 
28
21.2.4   固定锁和同步
同步是构建在一个称为“固定锁”或“监视锁”的内部实体上的。固定锁在同步的各个方面都扮演
着重要的角色:增强对一个对象状态的排他性访问(互斥)以及建立 happens-before 关系。
每一个对象都有一个相关联的固定锁。按惯例,一个需要对一个对象的字段进行排他性和持续性访
问的线程,在访问之前要获得对象的固定锁,然后在访问以后释放固定锁。在获得和释放固定锁期间称
一个线程拥有固定锁。只要一个线程拥有一个固定锁,其他线程就不能获得同一固定锁。如果其他线程
试图获得锁,将会阻塞。
当一个线程释放一个固定锁时,就会在此动作和任何后续的获得同一锁的动作之间建立
happens-before 关系。
当一个线程调用一个同步方法时,它自动地获得该方法所属对象的固定锁,并且当方法返回时,线
程会释放这个锁。即使方法的返回是由一个未捕获的异常引起的,该固定锁也会被释放。
当一个静态同步方法被调用时,因为静态方法是与类相关联的,而不是与对象相关联,因此线程会
获得与该类所关联的 Class 对象的固定锁。所以控制对类的静态字段的访问的锁与用于该类的任何实例
对象的锁不同。
除了同步方法之外,另外一个创建同步代码的方式是同步语句。使用关键字保护对一个代码块的访
问而不是对整个方法同步。使用同步语句保护对共享数据的访问,而将其余的操作放在同步块之外,从
而获得更好的程序性能。这样做的目的是尽可能的使同一时刻只能被一个线程访问的代码块尽可能地
短。当使用同步语句时,必须传递一个对象引用作为参数(指定提供固定锁的对象)。只有一个线程可以
访问该对象的同步代码(块或方法)。一般情况下,我们使用 this 关键字来引用执行该方法的对象。
【例】重构上一节的银行存取款操作程序。
重构上一节银行存取款操作程序中的 Account 类,使用同步语句。其它部分保持不变。
import java.util.concurrent.TimeUnit;
public class Account2 {
private double balance;
public double getBalance() {
return balance;
}
public void setBalance(double balance) {
this.balance = balance;
}
//同步方法,代表存款操作
public void addAmount(double amount) {
synchronized (this) {
double tmp = balance;
try {
TimeUnit.MILLISECONDS.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();

·29·
tmp += amount;
balance = tmp;
}
}
//同步方法,代表取款操作
public void subtractAmount(double amount) {
synchronized (this) {
double tmp = balance;
try {
TimeUnit.MILLISECONDS.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
tmp -= amount;
balance = tmp;
}
}
}
同步语句还有助于提高细粒度的同步并发。例如,假设一个类 Lunch 有两个实例字段,m1 和 m2,
这两个字段被多个线程共享,但是相互又是独立的(m1 和 m2 永远不会一起使用)。所有对于这些字段的
更新必须被同步,但是没有理由禁止对 m1 和 m2 的交错更新,否则将会创建不必要的阻塞,从而降低
并发性。代替使用同步方法或其他使用与 this 相关联的锁,这里创建两个单一的对象来提供锁。
public class Lunch {
private long m1 = 0;
private long m2 = 0;
private Object lock1 = new Object();        //创建对象 lock1 作为一个锁
private Object lock2 = new Object();        //创建对象 lock2 作为一个锁
public void addm1() {              //更新 m1 的方法
synchronized(lock1) {            //使用同步锁 lock1,同步对 m1 的更新
m1++;
}
}
public void addm2() {              //更新 m2 的方法
synchronized(lock2) {            //使用同步锁 lock2,同步对 m2 的更新
m2++;
}
}
}
下面通过一个实例来学习这种一个类中两个独立属性的同步访问问题。
【例】模拟一个电影院。该电影院有两个放映厅和两个票务办公室。当两个票务办公室卖票时,它
们可以卖两个放映厅的座位,并接受两个电影厅的退票。但对同一个放映厅,售票和退票都必须保持同
步。但是对于两上放映厅来说,各自的座位数又是独立的。
1)创建一个名为 Cinema 的类,带有两个属性 vacanciesCinema1 和 vacanciesCinema2,分别代表两
个放映厅的空位数。另外向 Cinema 类添加两个额外的对象属性 controlCinema1 和 controlCinema2。在构
造器中初始化所有的属性。创建分别对两个放映厅售票和退票的方法,实现同步语句。
public class Cinema {
30
private long vacanciesCinema1;   //放映厅 1 空位数
private long vacanciesCinema2;   //放映厅 2 空位数
private final Object controlCinema1, controlCinema2;
public Cinema() {
controlCinema1 = new Object();
controlCinema2 = new Object();
vacanciesCinema1 = 20;
vacanciesCinema2 = 20;
}
//销售第一个电影院的票。它使用 controlCinemal 对象来控制对同步代码块的访问
public boolean sellTickets1(int number) {
synchronized (controlCinema1) {
if (number < vacanciesCinema1) {
vacanciesCinema1 -= number;
return true;
} else {
return false;
}
}
}
//销售第二个电影院的票。它使用 controlCinema2 对象来控制对同步代码块的访问
public boolean sellTickets2(int number) {
synchronized (controlCinema2) {
if (number < vacanciesCinema2) {
vacanciesCinema2 -= number;
return true;
} else {
return false;
}
}
}
//当有第一个电影院的退票时,调用此方法。它使用 controlCinema1 对象控制对同步代码块的访问
public boolean returnTickets1(int number) {
synchronized (controlCinema1) {
vacanciesCinema1 += number;
return true;
}
}
//当有第二个电影院的退票时,调用此方法。它使用 controlCinema1 对象控制对同步代码块的访问
public boolean returnTickets2(int number) {
synchronized (controlCinema2) {
vacanciesCinema2 += number;
return true;
}

·31·
public long getVacanciesCinema1() {
return vacanciesCinema1;
}
public long getVacanciesCinema2() {
return vacanciesCinema2;
}
}
2)创建一个实现 Runnable 接口的类 TicketOffice1,代表第一个票务办公室。
public class TicketOffice1 implements Runnable {
private Cinema cinema;
public TicketOffice1(Cinema cinema) {
this.cinema = cinema;
}
//票务办公室的业务:售票和退票
@Override
public void run() {
cinema.sellTickets1(3);
cinema.sellTickets1(2);
cinema.sellTickets2(2);
cinema.returnTickets1(3);
cinema.sellTickets1(5);
cinema.sellTickets2(2);
cinema.sellTickets2(2);
cinema.sellTickets2(2);
}
}
3)创建一个实现 Runnable 接口的类 TicketOffice2,代表第二个票务办公室。
public class TicketOffice2 implements Runnable {
private Cinema cinema;
public TicketOffice2(Cinema cinema) {
this.cinema = cinema;
}
//票务办公室的业务:售票和退票
@Override
public void run() {
cinema.sellTickets2(2);
cinema.sellTickets2(4);
cinema.sellTickets1(2);
cinema.sellTickets1(1);
cinema.returnTickets2(2);
cinema.sellTickets1(3); 
32
cinema.sellTickets2(2);
cinema.sellTickets1(2);
}
}
4)测试。
public class Test {
public static void main(String[] args) {
Cinema cinema = new Cinema();
TicketOffice1 ticketOffice1 = new TicketOffice1(cinema);
Thread thread1 = new Thread(ticketOffice1, "TicketOffice1");
TicketOffice2 ticketOffice2 = new TicketOffice2(cinema);
Thread thread2 = new Thread(ticketOffice2, "TicketOffice2");
thread1.start();
thread2.start();
//等待线程结束
try {
thread1.join();
thread2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
//将空座位数写入到控制台
System.out.printf("放映厅 1 空余座位数: %d\n", cinema.getVacanciesCinema1());
System.out.printf("放映厅 2 空余座位数: %d\n", cinema.getVacanciesCinema2());
}
}
21.2.5   在同步代码中使用条件
在并发程序中一个经典的问题是生产者-消费者问题。我们有一个数据缓存,一个或多个数据生产
者,将生产的数据存储在缓存中,以及一个或多个数据消费者,从缓存中取数据。
因为该缓存是一个共享的数据结构,所以我们必须使用一个同步机制来控制对它的访问,如关键字
synchronized。但是这种生产者-消费者有一些限制。如果缓存满了的话,生产者无法在该缓存中保存数
据,如果该缓存是空的,则消费都无法从该缓存中取数据。
对于这种情况,Java 在 Object 类中提供了 wait()、notify()和 notifyAll()方法的实现。一个线程可以
在一个代码同步块中调用 wait()方法。如果它在一个代码同步块外部调用 wait()方法,则 JVM 会抛出一
个 IllegalMonitorStateException 异常。当线程调用 wait()方法时,JVM 将该线程置于 sleep 状态,并释放
控制正在执行的代码同步块的对象,并允许其它线程执行受该对象保护的其它同步代码块。要唤醒该线
程,必须在一个受同一个对象保护的代码块中调用 notify()或 notifyAll()方法。
这一节,我们将学习怎样使用 synchronized 关键字和 wait()、notify()和 notifyAll()方法实现生产者-
·33·
消费者问题。
【例】生产者-消费者问题。
1)创建一个名为 EventStorage 的类。在该类中存储共享的数据。
import java.util.Date;
import java.util.LinkedList;
import java.util.Queue;
public class EventStorage {
private int maxSize;
private Queue<Date> storage;     //队列
public EventStorage() {
maxSize = 10;
storage = new LinkedList<>();
}
//在 storage 中存储一个事件
public synchronized void set() {
//首先判断该 storage 是否已满。如果已满,就调用 wait()方法直到 storage 有存储空间
while (storage.size() == maxSize) {
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
storage.offer(new Date());
System.out.printf("Set: %d", storage.size());
//唤醒所有在 wait()方法中休眠的线程
notifyAll();
}
//从 storge 中获得一个事件
public synchronized void get() {
//判断 storge 中有没有事件。如果没有事件,就调用 wait()方法直到 storage 中有事件
while (storage.size() == 0) {
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.printf("Get: %d: %s", storage.size(), storage.poll().toLocaleString());
//唤醒所有在 wait()方法中处于休眠的线程
notifyAll();
}
}
2)创建生产者线程类。 
34
public class Producer implements Runnable {
private EventStorage storage;
public Producer(EventStorage storage) {
this.storage = storage;
}
@Override
public void run() {
for (int i = 0; i < 100; i++) {
storage.set();
}
}
}
3)创建消费者线程类。
public class Consumer implements Runnable {
private EventStorage storage;
public Consumer(EventStorage storage) {
this.storage = storage;
}
@Override
public void run() {
for (int i = 0; i < 100; i++) {
storage.get();
}
}
}
4)测试。
public class Test {
public static void main(String[] args) {
EventStorage storage = new EventStorage();
Producer producer = new Producer(storage);
Thread thread1 = new Thread(producer);
Consumer consumer = new Consumer(storage);
Thread thread2 = new Thread(consumer);
thread2.start();
thread1.start();        
}
}
编译并运行程序,观察输出结果。
上述同步方法中使用了守护块。守护块以循环检测一个条件开始,只有条件为 true 时,块才能继续
·35·
进行下去。
【例】“生产者-消费者”应用程序示例。
(1)首先创建由生产者和消费者所共享的对象:Drop 类。Drop 类中定义两个同步的方法,take()
方法用来读取消息,而 put()方法用来发送消息。 Drop 类位于 chapter21 包中,并单独保存为 java 源文件。
public class Drop {
//从生产者发往消费者的消息.
private String message;
//如果消费者在等待生产者发送消息,为 true;如果生产者在等待消费者获取数据,为 false。
private boolean empty = true;
public synchronized String take() {
//消费者等待,直到有消息可用.
while (empty) {              //循环等待条件 empty 成立
try {
wait();              //等待其他线程唤醒
} catch (InterruptedException e) {}
}
empty = true;    //改变状态
notifyAll();      //通知生产者状态已经改变
return message;
}
public synchronized void put(String message) {
//等待,直到消息被取走.
while (!empty) {
try { 
wait();              //等待其他线程唤醒
} catch (InterruptedException e) {}
}
empty = false;      //改变状态
this.message = message;  //存储消息
notifyAll();    //通知消费者状态已经改变
}
}
(2)生产者线程定义在 Producer.java 中,它发送一系列的消息。如果发送的消息为字符串“over”,
说明所有的消息已经发送完毕。为了模拟真实世界应用程序的不可预知性,生产者线程会在发送消息中
间暂停一个随机的时间。Producer 类仍然位于 chapter21 包中。
import java.util.Random;
public class Producer implements Runnable {
private Drop drop;            //声明一个生产者和消费者所共享的对象
public Producer(Drop drop) {        //构造器中初始化共享对象
this.drop = drop; 
36
}
public void run() {
String importantInfo[] = {"消息 1","消息 2","消息 3","消息 4"};
Random random = new Random();    //创建一个随机对象
for (int i = 0; i < importantInfo.length; i++) {  //循环向共享对象中放入消息
drop.put(importantInfo[i]);
try {
Thread.sleep(random.nextInt(5000));  //休眠一个随机时间
} catch (InterruptedException e) {}
}
drop.put("over");            //线程结束时,发出通知消息
}
}
(3)消费者线程,定义在类 Consumer 中,简单地获取消息并打印输出。如果收到“over”字符串
就结束。这个线程也暂停一个随机的间隔时间。
import java.util.Random;
public class Consumer implements Runnable {
private Drop drop;            //声明一个生产者和消费者所共享的对象
public Consumer(Drop drop) {        //构造器中初始化共享对象
this.drop = drop;
}
public void run() {
Random random = new Random();    //创建一个随机对象
String message = drop.take();      //从共享对象中获取生产者“生产”的消息
while(! message.equals("over")){      //如果不是结束的消息,则输出
System.out.format("MESSAGE RECEIVED: %s%n", message);
try {
Thread.sleep(random.nextInt(5000));
} catch (InterruptedException e) {}
message = drop.take();      //继续从共享对象中获取“生产”的消息
}
}
}
(4)最后,在 ProducerConsumerDemo.java 文件中,定义主线程,用来启动生产者和消费者线程。
public class ProducerConsumerDemo{
public static void main(String[] args) {
Drop drop = new Drop();        //创建生产者和消费者共享的对象
(new Thread(new Producer(drop))).start();  //创建并启动第 1 个线程
(new Thread(new Consumer(drop))).start();  //创建并启动第 2 个线程
}
}
(5)编译并运行程序,输出结果如下所示:
获取的消息为:  消息 1
获取的消息为:  消息 2
获取的消息为:  消息 3 
·37·
获取的消息为:  消息 4
上面的程序使用守护块来创建一个“生产-消费者”应用程序。这类应用程序在两个线程间共享数
据:生产者创建数据,而消费者处理数据。这两个线程使用一个共享对象进行通信。一个基本的条件是,
消费者线程一定不能在生产者线程分发数据前获得数据,同样的,如果消费者线程还没有获取原来的数
据,生产者线程一定不能分发新的数据。根本上就支持并发程序,在 Java 程序设计语言和 Java 类库中有基本的并发支
持。此外, Java 平台还包括高层次的并发 API,这些高层次的并发 API 都包含在 java.util.concurrent 包中。
本章就重点讲述并发程序设计的一些基本概念及 Java 的并发程序设计。
21.1   线程管理
在计算机系统中,并发指的是一系列的任务在同一个计算机中同时运行。如果是在多个处理器的计
算机中或者多核处理器中,这种同时运行是真正地同时发生,如果是在单核处理器的计算机中,则看上
去似乎是同时发生的。
所有现代的计算机都支持执行并发任务,如阅读邮件的同时听音乐和浏览网页,这种并发称为“进
程级”并发。但是在同一个进程中,我们也能同时有多个任务发生,这种运行在一个进程中的并发任务
称为“线程”。
另外一个与并发有关的概念是“并行”。对这个概念,有许多不同的定义和理解。
21.1.1   创建并运行一个线程
这一小节学习怎样在一个 Java 应用程序中创建和运行一个线程。在 Java 中,创建一个线程有两种
方式:
  继承 Thread 类并重写 run()方法
  构建一个实现了 Runnable 接口的类,然后创建一个 Thread 类的对象,并将该 Runnable 对象作
为参数传给它
Runnable 接口定义了一个单一的方法 run(),在 run()方法中包含要在线程中执行的代码。Runnable
对象被传递给 Thread 的构造器中,如下面的 RunnableDemo.java 所示。
【例】使用 Runnable 接口创建线程。
public class RunnableDemo implements Runnable {
public void run() {    //实现 Runnable 接口,则必须实现该接口中的 run 方法
System.out.println("这是一个线程!");
}
public static void main(String[] args) {
(new Thread(new RunnableDemo())).start();  //调用 start()方法,开始执行线程
}

2
在上面的代码中,创建了两个线程:调用 main()方法创建的主线程和调用 Thread 类的 start()方法创
建的执行线程。
当一个程序中所有的线程都执行完毕后,该程序结束。更准确地讲,当该程序中所有的守护线程执
行完毕后,该程序就结束。如果主线程先结束了,那么剩下的其它线程将继续它们的执行直到结束。如
果其中一个线程使用 System.exit()结束程序的执行,则所有的线程都将结束它们的执行。
【例】在下面这个示例程序中,我们使用第二种方式,创建一个简单的程序,在程序中,创建并运
行 10 个线程,每个线程计算并打印 1 到 10 之间一个数的乘法表。
public class Calculator implements Runnable {
private int number;
public Calculator(int number){
this.number = number;
}
@Override
public void run() {
for(int i=1; i<=10; i++){
System.out.printf("%s: %d * %d = %d\n",Thread.currentThread().getName(),number,i,number*i);
}
}
public static void main(String[] args) {
for(int i=1; i<=10; i++){
Calculator calculator = new Calculator(i);
Thread thread = new Thread(calculator);
thread.start();
}
}
}
编译并执行该程序,观察输出结果(多执行几次,会观察到每次输出的顺序都是不同的)。
也可以通过继承 Thread 类来创建一个新的线程。Thread 类已经实现了 Runable 接口,不过其 run()
方法什么也不做。应用程序可以通过子类化 Thread,实现其自身的 run()方法。如下面的 ThreadDemo.java
所示。
【例】使用 Thread 的子类创建线程。
public class ThreadDemo extends Thread {
public void run() {            //重写从 Thread 继承过来的 run 方法
System.out.println("这是一个线程!");
}
public static void main(String[] args) {
(new ThreadDemo()).start();
}
}
请注意,两种创建线程的方式都通过调用 Thread.start()方法来启动新的线程。两种用法应该用哪一
·3·
种呢?对于第一种用法,使用了一个 Runnable 对象,是比较常用的一种,因为 Runnable 对象可以子类
化一个非 Thread 类。第二种方法在简单的应用程序中更容易使用,但是它限制了完成异步任务的类只
能从 Thread 类继承。
21.1.2   获取和设置线程信息
Thread 类保存有相应的属性信息,用来标识一个线程、获知其状态、控制其优先级等。这些属性是:
  ID:这个属性存储每个线程的唯一标识。
  Name:这个属性存储线程的名称。
  Priority:这个属性存储 Thread 对象的优先级。线程可以有一个 1 到 10 的优先级,其中 1 是最
低优先级,10 是最高优先级。不建议改变线程的优先级。
  Status:这个属性存储线程的状态。在 Java 中,线程有 6 种状态: new, runnable, blocked, waiting, 
time waiting,或 terminated。
【例】下面这个示例程序中,为 10 个线程指定名称和优先级,然后显示它们的状态信息直到它们
结束。这些线程将计算某个数的乘法表。
import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;
import java.lang.Thread.State;
public class ThreadManagement {
public static void main(String[] args) {
Thread[] threads = new Thread[10];  //保存 10 个线程
Thread.State[] status = new Thread.State[10];  //保存这 10 个线程的状态
for (int i = 0; i < 10; i++) {
threads[i] = new Thread(new Calculator(i));
if (i % 2 == 0) {
threads[i].setPriority(Thread.MAX_PRIORITY);     //设置为最高优先级
} else {
threads[i].setPriority(Thread.MIN_PRIORITY);     //设置为最低优先级
}
threads[i].setName("Thread" + i);
}
try (FileWriter file = new FileWriter(".\\threadlog.txt");// "threadlog.txt"
PrintWriter pw = new PrintWriter(file);) {
for (int i = 0; i < 10; i++) {
pw.println("Main: Status of Thread " + i + " : " + threads[i].getState());
status[i] = threads[i].getState();
}
//开始执行 10 个线程
for (int i = 0; i < 10; i++) {
threads[i].start();

4
boolean finish = false;
while (!finish) {
for (int i = 0; i < 10; i++) {
if (threads[i].getState() != status[i]) {
writeThreadInfo(pw, threads[i], status[i]);
status[i] = threads[i].getState();
}
}
finish = true;
for (int i = 0; i < 10; i++) {
finish = finish && (threads[i].getState() == State.TERMINATED);
}
}
} catch (IOException ex) {
ex.printStackTrace();
}
}
private static void writeThreadInfo(PrintWriter pw, Thread thread, State state) {
pw.printf("Main : Id %d - %s\n", thread.getId(), thread.getName());
pw.printf("Main : Priority: %d\n", thread.getPriority());
pw.printf("Main : Old State: %s\n", state);
pw.printf("Main : New State: %s\n", thread.getState());
pw.printf("Main : ************************************\n");
}
}
21.1.3   中断一个线程
如果一个程序有多个执行线程的话,那么只有当它所有的非守护线程结束执行,或当其中一个线程
使用 System.exit()方法时,该程序才会结束。但是有时候,我们想要终止整个应用程序,或者想要取消
一个 Thread 对象正在执行的任务,这时就需要提前结束该线程。
Java 提供了中断机制来告诉一个线程我们想要结束它。这个机制要求 Thread 必须判断它是否已经
被中断了,并且能决定是否响应中断请求。Thread 可以忽视该请求,并继续其执行。
【例】下面这个示例程序中,创建一个线程,1 秒钟以后,使用中断机制强迫它结束。
public class PrimeGenerator extends Thread {
@Override
public void run() {
long number = 1L;
while (true) {
if (isPrime(number)) {
System.out.printf("Number %d is Prime\n", number);
}
if (isInterrupted()) { 
·5·
System.out.printf("The Prime Generator has been Interrupted");
return;
}
number++;
}
}
//判断参数是否是一个素数
private boolean isPrime(long number) {
if (number <= 2) {
return true;
}
for (long i = 2; i < number; i++) {
if ((number % i) == 0) {
return false;
}
}
return true;
}
public static void main(String[] args) {
Thread task = new PrimeGenerator();
task.start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
task.interrupt();    //中断线程 task
}
}
在 Thread 类中,有一个属性存储一个 boolean 值,指定该线程是否被中断。当调用线程的 interrupt()
方法时,将这个属性设为 true。而 isInterrupted()方法返回这个属性的值。
21.1.4   控制线程的中断
Java 提供了一个 InterruptedException 异常用于控制线程的中断。当程序检测到线程的中断时,就抛
出一个 InterruptedException 异常,然后在 run()方法中捕获它。
【例】简单的线程中断示例。
import java.util.concurrent.TimeUnit;
public class InterruptThread implements Runnable{
@Override
public void run() {
System.out.println("假设这个线程要做很耗时的工作......");
try { 
6
TimeUnit.SECONDS.sleep(10);      //这里可改为休眠 2 秒钟,再运行并观察结果
System.out.println("工作完成");
} catch (InterruptedException ex) {
System.out.println("工作还没做完,被中断了");
}        
}
public static void main(String[] args) {
Thread thread = new Thread(new InterruptThread());
thread.start();
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException ex) {
ex.printStackTrace();
}
thread.interrupt();    //中断线程
}
}
执行程序,输出结果如下:
假设这个线程要做很耗时的工作......
工作还没做完,被中断了
【例】下面这是一个稍微复杂些的线程中断示例。程序中,给定一个目录,在其中递归查找指定的
文件。在查找过程中有可能查找线程被中断。
import java.io.File;
import java.util.concurrent.TimeUnit;
public class FileSearch implements Runnable {
private String initPath;     //初始文件夹
private String fileName;     //要查找的文件名
public FileSearch(String initPath, String fileName) {
this.initPath = initPath;
this.fileName = fileName;
}
//判断属性 fileName 是否是一个目录。如果是目录的话,调用方法 processDirectory().
//该方法会抛出一个 InterruptedException 异常,  这样我们就必须捕获该异常
@Override
public void run() {
File file = new File(initPath);
if (file.isDirectory()) {
try {
directoryProcess(file);
} catch (InterruptedException e) {
System.out.printf("%s: The search has been interrupted", Thread.currentThread().getName());

·7·
}
}
private void directoryProcess(File dir) throws InterruptedException {
File[] list = dir.listFiles();
if (list != null) {
for (int i = 0; i < list.length; i++) {
if (list[i].isDirectory()) {
directoryProcess(list[i]);
} else {
fileProcess(list[i]);
}
}
}
if (Thread.interrupted()) {
throw new InterruptedException();
}
}
private void fileProcess(File file) throws InterruptedException {
if (file.getName().equals(fileName)) {
System.out.printf("%s : %s\n", Thread.currentThread().
getName(), file.getAbsolutePath());
}
//当一个线程通过调用静态方法 Thread.interrupted()检查中断时,中断状态被清除。
//非静态的方法 isInterrupted()被一个线程使用来查询另一个线程的中断状态,而不改变中断状态标记。
if (Thread.interrupted()) {
throw new InterruptedException();
}
}
public static void main(String[] args) {
FileSearch searcher = new FileSearch("C:\\", "autoexec.bat");
Thread thread = new Thread(searcher);
thread.start();
try {
TimeUnit.SECONDS.sleep(10); //休眠 10 秒钟
} catch (InterruptedException e) {
e.printStackTrace();
}
thread.interrupt(); //中断线程 thread
}
}
8
21.1.5   暂停线程的执行
有的时候,我们需要一个线程暂时停止执行,比如,某个程序中的线程每分钟检测一次传感器的状
态。在其余的时间,该线程什么也不做,也不消耗计算机的资源。这可以通过调用 Thread 类的 sleep()
方法实现。
Thread.sleep()方法可以引起当前线程挂起执行一个指定的时期。这意味着处理时间可用于程序其他
线程,或者运行在计算机系统内的其他应用程序。sleep()方法还被用于控制步调、等待另外的线程完成
有时间要求的任务。
有两个重载的 sleep()方法。一个指定休眠的时间毫秒,另一个指定休眠的时间为纳秒(十亿分之一
秒)。不过,这些休眠时间并不保证精确,这由后台的操作系统所提供的工具所限制。另外,休眠期也
可以被中断信息号所终结。
在任何情况下,都不能假定对 sleep()方法的调用会精确地按指定的时间周期挂起线程。下面的
SleepMessagesDemo.java 示例使用 sleep()方法每 3 秒输出一个消息。
【例】使用 Thread.sleep 方法,每隔 3 秒打印输出一个消息。
public class SleepMessagesDemo{
public static void main(String[] args) throws InterruptedException {
String messages[] = {
"消息 1",
"消息 2",
"消息 3",
"消息 4"
};
for (int i = 0; i < messages.length; i++) {            
Thread.sleep(3000);          //执行到此暂停 3 秒
System.out.println(messages[i]);    //输出一个消息
}
}
}
在上面的程序代码中,main()方法声明其抛出 InterruptedException 异常。当另一个线程中断了当前
线程的 sleep 状态时, sleep()方法会抛出此异常。因为这个应用程序并没有定义另外的线程,所以不必担
心会引起中断和捕获 InterruptedException 异常。
另一种可能的方式是使用 TimeUnit 枚举类型的 sleep()方法。这个方法使用 Thread 类的 sleep()方法
将当前线程置于休眠状态,但是接收的参数可以是指定的时间单元,然后会自动将其转为毫秒。
【例】使用方法,每秒打印一个日期。
import java.util.Date;
import java.util.concurrent.TimeUnit;
public class FileClock implements Runnable {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.printf("%s\n", new Date().toLocaleString());
try {
TimeUnit.SECONDS.sleep(1); 
·9·
} catch (InterruptedException e) {
System.out.printf("The FileClock has been interrupted");
}
}
}
public static void main(String[] args) {
FileClock clock = new FileClock();
Thread thread = new Thread(clock);
thread.start();
}
}
注:java.util.concurrent.TimeUnit 是一个枚举类型,它定义了许多表示时间粒度的枚举值。
Thread.sleep(),是一个静态方法,暂停线程时它不会释放锁,该方法会抛出 InterrupttedException 异
常(如果有线程中断了当前线程)。但是因为 Thread.sleep()方法的参数是毫秒,因此可读性不强。
TimeUnit 类通过指定 DAYS、 HOURS、 MINUTES,SECONDS、 MILLISECONDS 和 NANOSECONDS
这些枚举值解决了这个问题。java.utils.concurrent  .TimeUnit  是 Java 枚举应用场景中最好的例子之一,
所有 TimeUnit 都是枚举实例
让我们来看看线程睡眠 4 分钟用 TimeUnit 是如何使用的:
TimeUnit.MINUTES.sleep(4);   //休眠 4 分钟
TimeUnit.sleep()内部调用的 Thread.sleep()也会抛出 InterruptException。
除了 sleep()的功能外,TimeUnit 还提供了许多便捷方法用于把时间转换成不同单位,例如,如果你
想把秒转换成毫秒,你可以使用下面代码:
TimeUnit.SECONDS.toMillis(44);
它将返回 44,000
21.1.6  等待一个线程结束
线程对象的 join()方法允许一个线程等待另外一个线程的完成。例如,一个当前正在执行的线程 t,
它是 Thread 对象,如果调用其 join()方法,如下所示。
t.join();
这时会引起当前的线程暂停执行,直到 t 的线程终结为止。重载 join()方法允许程序员来指定一个等
待的周期。 不过与 sleep()一样, join 指定的这个时间也依赖于操作系统,所以不应该假设 join()方法会精
确地等待指定的时间。
与 sleep()方法一样,join()方法响应中断时,会抛出一个 InterruptedException 并退出。
【例】例如,首先看下面这段代码,并判断其输出结果:
public class ThreadTest implements Runnable {
public static int a = 0;           //定义静态变量
public void run() {            //在 run 方法里,循环增加静态变量 a 的值
for (int k = 0; k < 5; k++) {
a = a + 1;
}
}
public static void main(String[] args) throws Exception { 
10
Runnable r = new ThreadTest();      //创建 ThreadTest 对象
Thread t = new Thread(r);        //创建线程 t
t.start();              //启动线程 t
System.out.println(a);          //输出静态变量 a 的值
}
}
编译并运行上述程序,输出结果是 5 吗?答案是:有可能。其实很难遇到输出 5 的时候,通常情况
下都不是 5(当然这也和机器有密切的关系)。因为当主线程 main()方法执行“System.out.println(a);”
这条语句时,线程还没有真正开始运行(或许正在为它分配资源准备运行吧)。因为为线程分配资源需
要时间,而 main()方法执行完 t.start()方法后继续往下执行“System.out.println(a);”,这个时候得到的结
果是 a 还没有被改变的值 0。
如果想让输出结果为 5,就需要调用 join()方法。当调用 join()方法时,当前的 main 线程会阻塞执行,
直到调用该方法的线程执行完毕 main 线程才会继续执行。
【例】使用 join()方法以后的代码示例:
public class ThreadTest implements Runnable {
public static int a = 0;           //声明静态变量 a
public void run() {
for (int k = 0; k < 5; k++) {
a = a + 1;
}
}
public static void main(String[] args) throws Exception {
Runnable r = new ThreadTest();
Thread t = new Thread(r);        //创建另一个线程 t
t.start();              //启动线程 t
t.join();              //当前线程阻塞执行,等待线程 t 执行完毕
System.out.println(a);
}
}
在调用线程 t 的 start()方法启动线程 t 以后,先调用 t.join()方法,等待线程 t 结束, main 线程才继续
执行后续的“System.out.println(a);”方法,输出 a 的值,此时一定会输出结果 5。
21.1.7  创建并运行一个守护线程
Java 有一个特殊类型的线程,称为“守护线程”。这种线程的优先级很低,通常只有在同一程序中
没有其它线程运行时它才被执行。
当程序中只剩下守护线程在运行时, JVM 就会结束程序,终止这些线程。比如,垃圾回收器就是一
个典型的守护线程。
【例】创建两个线程。一个用户线程向一个队列中写入事件,另一个守护线程清除该队列,将生成
时间超过 10 秒的事件移除。
1)首先创建一个事件对象 Event.java。
import java.util.Date;
public class Event {
private Date date; 
·11·
private String event;
public Date getDate() {
return date;
}
public void setDate(Date date) {
this.date = date;
}
public String getEvent() {
return event;
}
public void setEvent(String event) {
this.event = event;
}
}
2)下面创建一个用户线程。 在这个线程中,循环迭代 100 次。在每次迭代中,我们创建一个新的 Event
对象,并将它保存到队列中,然后休眠 1 秒钟。
import java.util.Date;
import java.util.Deque;
import java.util.concurrent.TimeUnit;
public class WriterTask implements Runnable {
private Deque<Event> deque;
public WriterTask(Deque<Event> deque) {
this.deque = deque;
}
@Override
public void run() {
for (int i = 1; i < 100; i++) {
Event event = new Event();
event.setDate(new Date());
event.setEvent(String.format("该线程%s  已经生成了一个事件", Thread.currentThread().getId()));
deque.addFirst(event);
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

12
3)创建守护线程。 该守护线程获取队列中最后一个 Event 对象,如果该对象创建时间超过 10 秒,就
将其删除并检查下一个事件。如果一个事件被删除了,守护线程就将该事件的信息和队列的新的大小打
印出来。
import java.util.Date;
import java.util.Deque;
public class CleanerTask extends Thread {
private Deque<Event> deque;
public CleanerTask(Deque<Event> deque) {
this.deque = deque;
setDaemon(true);  //将当前线程设为守护线程
}
@Override
public void run() {
while (true) {
Date date = new Date();
clean(date);
}
}
private void clean(Date date) {
long difference;
boolean delete;
if (deque.size() == 0) {
return;
}
delete = false;
do {
Event e = deque.getLast();
difference = date.getTime() - e.getDate().getTime();
if (difference > 10000) {
System.out.printf("Cleaner: %s\n", e.getEvent());
deque.removeLast();
delete = true;
}
} while (difference > 10000);
if (delete) {
System.out.printf("Cleaner: Size of the queue: %d\n", deque.size());
}
}
}
4)测试。
import java.util.ArrayDeque;
import java.util.Deque;
public class DaemonThread { 
·13·
public static void main(String[] args) {
Deque<Event> deque = new ArrayDeque<Event>();
WriterTask writer = new WriterTask(deque);
for (int i = 0; i < 3; i++) {
Thread thread = new Thread(writer);
thread.start();
}
CleanerTask cleaner = new CleanerTask(deque);
cleaner.start();
}
}
要注意的是,只能在调用 start()方法之前调用 setDaemon()方法。一旦线程开始运行,就不能修改其
守护状态。
可以使用 isDaemon()方法来判断一个线程是守护线程(当该方法返回 true 时)还是用户线程(当该方法
返回 false 时)。
21.1.8   处理线程中的异常
在 Java 中有两种类型的异常:
  检查异常:这类异常必须在方法的 throws 语句中指明,或者在方法内捕获。例如, IOException
或 ClassNotFoundException。
  未检查异常:这类异常不必指明或捕获。例如,NumberFormatException。
当在 Thread 对象的 run()方法中抛出一个检查异常时,我们必须捕获得处理它们,因为 run()方法不
接收 throw 语句。当在 Thread 对象的 run()方法中抛出一个未经检查的异常时,默认的行为是在控制台
输出异常栈信息并退出程序。
不过,Java 提供的有捕获和处理这种 Thread 对象中所抛出的未检查异常的机制,以避免程序结束。
通过下面的示例程序,学习和掌握这种机制。
【例】Thread 中未检查异常的处理机制。
1)首先,实现一个处理未检查异常的类。这个类必须实现 UncaughtExceptionHandler 接口,并实现
该接口中声明的 uncaughtException()方法。
import java.lang.Thread.UncaughtExceptionHandler;
public class ThreadExceptionHandler implements UncaughtExceptionHandler {
@Override
public void uncaughtException(Thread t, Throwable e) {
System.out.printf("捕获到一个异常\n");
System.out.printf("Thread: %s\n", t.getId());
System.out.printf("Exception: %s: %s\n", e.getClass().getName(), e.getMessage());
System.out.printf("Stack Trace: \n");
e.printStackTrace(System.out);
System.out.printf("Thread status: %s\n", t.getState());
}
14
}
2)接下来,实现一个能抛出一个未检查异常的类 Task.java。该类实现 Runnable 接口,在其实现的
run()方法中抛出一个未检查类型异常。
public class Task implements Runnable {
@Override
public void run() {
int numero = Integer.parseInt("KKK");
}
}
3)测试。
public class DaemonThread {
public static void main(String[] args) {
Task task = new Task();
Thread thread = new Thread(task);
thread.setUncaughtExceptionHandler(new ThreadExceptionHandler());
thread.start();
}
}
执行程序,输出结果如下:
捕获到一个异常
Thread: 8
Exception: java.lang.NumberFormatException: For input string: "KKK"
Stack Trace: 
java.lang.NumberFormatException: For input string: "KKK"
at java.lang.NumberFormatException.forInputString(NumberFormatException.java:65)
at java.lang.Integer.parseInt(Integer.java:492)
at java.lang.Integer.parseInt(Integer.java:527)
at com.derun.chapter01.Task.run(Task.java:7)
at java.lang.Thread.run(Thread.java:724)
Thread status: RUNNABLE
21.1.9   使用线程局部变量
并发应用程序最重要的方面之一是共享数据。在继承了 Thread 类或实现了 Runnable 接口的这些对
象中这特别重要。
如果创建一个实现了 Runnable 接口的类的对象,然后使用同一个 Runnable 对象启动各个 Thread 对
象,那么所有这些线程共享相同的属性。这意味着,如果在一个线程中改变了一个属性,那么所有的线
程都将会受到影响。
有时候,我们可能想要一个属性不被运行同一对象的所有线程所共享。在 Java 并发 API 中提供有
一个非常简洁的机制,称为线程局部变量,它有很好的性能。
【例】线程共享数据时出现的问题。
1)创建一个名为 UnsafeTask 的类,并指定它实现 Runnable 接口。在该类中声明一个私有属性变量。  
·15·
import java.util.Date;
import java.util.concurrent.TimeUnit;
public class UnsafeTask implements Runnable {
private Date startDate;
@Override
public void run() {
startDate = new Date();
System.out.printf("启动线程: %s : %s\n", Thread.
currentThread().getId(), startDate.toLocaleString());
try {
TimeUnit.SECONDS.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.printf("线程结束: %s : %s\n", Thread.
currentThread().getId(), startDate.toLocaleString());
}
}
2)测试。在 main()方法中,创建一个 UnsafeTask 类的对象,并使用该对象启动三个线程,每个线程
间隔 2 秒钟。
import java.util.Date;
import java.util.concurrent.TimeUnit;
public class Test {
public static void main(String[] args) {
UnsafeTask task = new UnsafeTask();
for (int i = 0; i < 3; i++) {
Thread thread = new Thread(task);
thread.start();
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
3)运行。输出结果如下:
启动线程: 8 : 2014-2-19 15:09:14
启动线程: 9 : 2014-2-19 15:09:16
启动线程: 10 : 2014-2-19 15:09:18
线程结束: 8 : 2014-2-19 15:09:18
线程结束: 9 : 2014-2-19 15:09:18
线程结束: 10 : 2014-2-19 15:09:18
可以看得出来,虽然每个线程中 startDate 属性的起始值不一样,但结束时都是一样的。因为这些线
16
程共享同一个属性变量,所以对一个线程中变量值的改变会影响到所有的线程。
可以使用线程局部变量来解决这个问题。
【例】使用线程局部变量解决线程共享数据时出现的问题。
1)创建一个名为 SafeTask 的类,并指定它实现 Runnable 接口。在该类中声明一个线程局部变量。
import java.util.Date;
import java.util.concurrent.TimeUnit;
public class SafeTask implements Runnable {
private static final ThreadLocal<Date> startDate = new ThreadLocal<Date>() {
@Override
protected Date initialValue() {
return new Date();
}
};
@Override
public void run() {
System.out.printf("启动线程: %s : %s\n", Thread.
currentThread().getId(), startDate.get().toLocaleString());
try {
TimeUnit.SECONDS.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.printf("线程结束: %s : %s\n", Thread.
currentThread().getId(), startDate.get().toLocaleString ());
}
}
2)测试。在 main()方法中,创建一个 UnsafeTask 类的对象,并使用该对象启动三个线程,每个线程
间隔 2 秒钟。
import java.util.Date;
import java.util.concurrent.TimeUnit;
public class Test {
public static void main(String[] args) {
UnsafeTask task = new UnsafeTask();
for (int i = 0; i < 3; i++) {
Thread thread = new Thread(task);
thread.start();
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

·17·
3)运行。输出结果如下:
启动线程: 8 : 2014-2-19 15:22:53
启动线程: 9 : 2014-2-19 15:22:55
启动线程: 10 : 2014-2-19 15:22:57
线程结束: 8 : 2014-2-19 15:22:53
线程结束: 9 : 2014-2-19 15:22:55
线程结束: 10 : 2014-2-19 15:22:57
可以看到,三个线程各自有自己的变量值,互不影响。
线程局部变量为每个使用该变量的 Thread 存储一个属性值。可以使用 get()方法读取该值,使用 set()
方法改变该值。当第一次在某个线程中访问线程局部变量中存储的属性值时,如果该线程中的该属性没
有值,则线程局部变量调用 initialValue()方法为该线程赋一个值,并返回初始值。
ThreadLocal 类还提供有一个 remove()方法,用来删除调用线程存储在线程局部变量中的值。
21.1.10   线程组
线程组是指可以将一组线程作为一个单独的单元来处理,可以访问其中的一个线程来对整个组进行
操作。
Java 提供有 ThreadGroup 类来处理线程组。一个 ThreadGroup 对象由 Thread 对象或其它 ThreadGroup
对象组成,生成一个线程树结构。
下面将通过 ThreadGroup 对象开发一个简单的示例。
【例】在一个随机的时间周期内(例如,模拟搜索),有 10 个线程正在休眠,当其中一个线程结束时,
同时也就终止其余的线程。
1)首先,创建一个名为 Result 的类。这个类将存储第一个结束的线程的名字。在该类中声明一个
private String name 属性。
public class Result {
private String name;     //存储第一个结束的线程
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}   
}
2)创建一个名为 SearchTask 的类,并指定其实现 Runnable 接口。 在实现的 run()方法中,调用 doTask()
方法模拟当前线程正在执行的搜索任务。当某个线程第一个结束(完成搜索任务)时,其它线程就没有必
要再搜索下去了。
package com.derun.chapter01;
import java.util.Date;
import java.util.Random;
import java.util.concurrent.TimeUnit;
18
public class SearchTask implements Runnable {
private Result result;
public SearchTask(Result result) {
this.result = result;
}
//调用 doTask()方法直到其结束或出现 InterruptedException 异常,并输出当前线程启动、结束或中断的信息
@Override
public void run() {
String name = Thread.currentThread().getName();
System.out.printf("线程%s:  启动\n", name);
try {
doTask();    //模拟当前线程所执行的搜索任务
result.setName(name);
} catch (InterruptedException e) {
System.out.printf("线程%s:  被中断了\n", name);
return;
}
System.out.printf("线程%s:  结束\n", name);
}
//下面这个方法创建一个 Random 对象,生成一个随机数,并调用 sleep()方法休眠一个随机时间
//模拟不同的搜索时间
private void doTask() throws InterruptedException {
Random random = new Random((new Date()).getTime());
int value = random.nextInt(100);     //生成[0,100]之间的整数
System.out.printf("线程%s 休眠时间(秒)-模拟搜索进行的时间: %d\n", 
Thread.currentThread().getName(), value);
TimeUnit.SECONDS.sleep(value);
}
}
3)编写 main()方法。
import java.util.concurrent.TimeUnit;
public class Test {
public static void main(String[] args) {
ThreadGroup threadGroup = new ThreadGroup("Searcher");
Result result = new Result();
SearchTask searchTask = new SearchTask(result);
//创建 5 个线程对象,指定它们属于同一个线程组
for (int i = 0; i < 5; i++) {
Thread thread = new Thread(threadGroup, searchTask);
thread.start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) { 
·19·
e.printStackTrace();
}
}
System.out.printf("线程组中线程数量: %d\n", threadGroup.activeCount());
System.out.printf("关于线程组的信息\n");
threadGroup.list(); //将有关此线程组的信息打印到标准输出
//int activeCount() :返回此线程组中活动线程的估计数
Thread[] threads = new Thread[threadGroup.activeCount()];
//enumerate()方法:把此线程组及其子组中的所有活动线程复制到指定数组中
threadGroup.enumerate(threads);
for (int i = 0; i < threadGroup.activeCount(); i++) {
System.out.printf("线程%s: %s\n", threads[i].getName(), threads[i].getState());
}
//等待线程组中某个线程结束
waitFinish(threadGroup);
threadGroup.interrupt();     //中断此线程组中的所有线程
}
private static void waitFinish(ThreadGroup threadGroup) {
//当线程数少于 5 时,说明已经有一个线程结束了
while (threadGroup.activeCount() > 4) {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
4)编译运行,并观察输出结果。
ThreadGroup 类存储 Thread 对象和其它相关联的 ThreadGroup 对象,因此它能访问到它们所有的信
息(比如,状态),并在其成员上执行操作(如中断)。
21.1.11   在线程组上处理异常
这一节,学习如何捕获由 ThreadGroup 类中的任何 Thread 所抛出的未捕获的异常。
【例】处理线程组中抛出的未捕获异常。
1)创建一个名为 MyThreadGroup 的类,它继承自 ThreadGroup,并重写 uncaughtException()方法。
public class MyThreadGroup extends ThreadGroup {
public MyThreadGroup(String name) {
super(name);

20
//当 ThreadGroup 线程组中有一个线程抛出异常时,这个方法就会被调用
@Override
public void uncaughtException(Thread t, Throwable e) {
System.out.printf("线程%s 抛出了一个异常\n", t.getId());
e.printStackTrace(System.out);
System.out.printf("终止线程的其余部分\n");
interrupt();
}
}
2)创建一个名为 Task 的类,并实现 Runnable 接口。在该接口的 run()方法实现中,有可能会产生一
个 ArithmeticException 异常(随机产生除数,有可能值为 0)。
import java.util.Random;
public class Task implements Runnable {
@Override
public void run() {
int result;
Random random = new Random(Thread.currentThread().getId());
while (true) {
result = 1000 / random.nextInt(100);    //有可能除以 0
System.out.printf("%s : %d\n", Thread.currentThread().getId(), result);
//如果当前线程被中断
if (Thread.currentThread().isInterrupted()) {
System.out.printf("%d :  被中断\n", Thread.currentThread().getId());
return;
}
}
}
}
3)测试。
import java.util.concurrent.TimeUnit;
public class Test {
public static void main(String[] args) {
MyThreadGroup threadGroup = new MyThreadGroup("MyThreadGroup");
Task task = new Task();
for (int i = 0; i < 2; i++) {
Thread t = new Thread(threadGroup, task);
t.start();
}
}
}
编译并运行该程序,会看到当线程组中某个线程对象抛出异常时,其余线程都被中断。 
·21·
当在 Thread 中抛出未捕获异常时,JVM 会查找三个用于该异常的处理器。
首先, 它会查找线程的未捕获异常处理器(handler)。如果这个处理器不存在,那么 JVM 会查找该线
程所在线程组的未捕获异常处理器(如这一节所学到的)。如果这个方法也不存在,则 JVM 会查找默认的
未捕获异常处理器。如果所有的处理器都不存在,则 JVM 会将异常栈信息写出到控制台,并退出程序。
21.1.12   使用线程工厂类
工厂模式是面向对象编程世界里最有用的设计模式之一。它是一个创建模式,其目标是开发一个对
象,该对象的任务是创建其它类的对象。然后,当我们想要创建其它类的对象时,就可以使用工厂而不
是使用 new 运算符。
使用工厂模式有如下几个好处:
  很容易改变对象创建类,或我们创建这些对象的方式。
  很容易将对象的创建限制在有限的资源。
  很容易生成创建对象的静态数据。
Java 提供一个接口 ThreadFactory,实现了一个 Thread 对象工厂。一些 Java 并发 API 的高级工具使
用线程工厂来创建线程。
在这一节,我们将学习怎样实现一个 ThreadFactory 接口来创建 Thread 线程对象,并统计所创建线
程对象的数量。
【例】使用线程工厂类创建线程。
1)创建一个实现 ThreadFactory 接口的类 MyThreadFactory,并重写 newThread(Runnable r)方法。
import java.util.*;
import java.util.concurrent.ThreadFactory;
public class MyThreadFactory implements ThreadFactory {
private int counter;         //统计创建的线程对象数量
private String name;         //所创建的每个线程的基名
private List<String> stats;      //保存所创建线程的状态数据
public MyThreadFactory(String name) {
counter = 0;
this.name = name;
stats = new ArrayList<>();
}
@Override
public Thread newThread(Runnable r) {
Thread t = new Thread(r, name + "-Thread_" + counter);
counter++;
stats.add(String.format("创建的线程 id:%d,名称:%s,创建时间:%s\n", t.getId(), 
t.getName(), new Date().toLocaleString()));
return t;
}
//获取所创建线程的统计信息 
22
public String getStats() {
StringBuffer buffer = new StringBuffer();
Iterator<String> it = stats.iterator();
while (it.hasNext()) {
buffer.append(it.next());
buffer.append("\n");
}
return buffer.toString();
}
}
2)创建一个名为 Task 的类,并指定其实现 Runnable 接口。
import java.util.concurrent.TimeUnit;
public class Task3 implements Runnable {
@Override
public void run() {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
3)测试。
public class Test {
public static void main(String[] args) {
MyThreadFactory factory = new MyThreadFactory("MyThreadFactory");
Task task = new Task();
System.out.printf("启动 10 个线程......\n");
for (int i = 0; i < 10; i++) {
Thread thread = factory.newThread(task);
thread.start();
}
System.out.printf("工厂状态:\n");
System.out.printf("%s\n", factory.getStats());
}
}
ThreadFactory 接口只有一个名为 newThread 的方法,它接口一个 Runnable 对象作为参数,并返回
一个 Thread 线程对象。当我们实现一个 ThreadFactory 接口时,就必须重写该方法。大多数基本的
ThreadFactory 只有一行代码:
return new Thread(r);
在 newThread 方法中,我们可以实现一些复杂的要求。比如:
  创建个性化的线程,如上面的示例所示,使用一个特定格式的名称,或者创建我们自己的继承
自 Thread 类的线程。 
·23·
  保存线程创建的统计信息。
  限制所创建的线程的数量。
  验证线程的创建。
使用工厂设计模式是一个很好的编程实践,但是,如果实现一个 ThreadFactory 接口来集中创建线
程,那么就必须检查代码以确保所有的线程是使用该工厂类创建的。
21.2  基本的线程同步
在并发程序中最常见的一种情况是多个执行线程共享一个资源。在一个并发应用程序中,多个线程
读写相同的数据或访问相同的文件或数据库连接是很正常的事情。 这些共享资源有可能会造成两种类型
的错误:线程冲突和数据不一致(内存一致性错误)。因此,我们必须要防止这些错误,这就需要使用同
步机制,以确保访问共享资源的代码块不能同时被多个线程执行。
当一个线程想要执行访问共享资源的代码块时,它使用同步机制来找出是否有其它线程正在执行该
代码块。如果没有,该线程就进入这个访问共享资源的代码块执行。换句话说,如果有其它线程正在执
行访问共享资源的代码块,那么当前线程就会被挂起,直到其它线程结束对访问共享资源代码块的执行。
当有超过一个的线程在等待某个线程结束其对访问共享资源代码块的执行时, JVM 会从中选择一个,而
其余的依次等待。
Java 语言提供两个基本的同步机制:
  关键字 synchronized
  Lock 接口及其实现
21.2.1   线程冲突
在理解线程冲突的含义之前,先来看一个简单的类 Counter 的定义:
class Counter {
private int c = 0;
public void increment() {
c++;
}
public void decrement() {
c--;
}
public int value() {
return c;
}
}
在类 Count 中,每当调用 increment()方法时,变量 c 的值就加 1,每当调用 decrement()方法时,变
24
量 c 的值就会减 1。这在单一的线程中没有问题,但是如果从多个不同的线程中引用 Counter 对象并调
用 increment()方法和 decrement()方法时,变量 c 的值可能就不是这样改变的。
当在不同的线程中执行两个操作进行交叉存取,但操作对象是同一数据时,就会发生冲突。例如,
在单独的线程中,对表达式 c++的操作可以分解为三步:
(1)获得变量 c 当前的值。
(2)将获得的变量 c 的值加 1。
(3)将改变(增加)以后的值再赋给变量 c。
表达式 c--的操作也可以分解为这三步。
现在假设一个线程 A 调用方法 increment(),同时另一个线程 B 也在同一时刻调用方法 decrement()。
如果变量 c 的初始值为 0,那么就可能会发生以下的交叉操作:
(1)线程 A 获得变量 c 的当前值(为 0)。
(2)线程 B 获得变量 c 的当前值(为 0)。
(3)线程 A 将获得的值增加 1,增加结果为 1。
(4)线程 B 将获得的值减少 1,减少结果为-1。
(5)线程 A 将增加后的值保存回变量 c,此时 c 的值改变为 1。
(6)线程 B 将减少后的值保存回变量 c,此时 c 的值改变为-1。
操作的结果是,线程 A 的值最后丢失了,被线程 B 的值所覆盖。在实际执行过程中,也可能是线
程 B 的值丢失而最后保留线程 A 的值,或者根本就没有错误。因为不可预期,所以线程冲突这种 bug
很难被检测和修正。
上面的问题所反映的实质是,在 Java 的多线程程序中, 当两个或多个线程同时访问同一个变量,并
且一个线程需要修改这个变量时,应对这样的问题做出处理,否则可能发生混乱。
21.2.2   内存一致性错误
当不同的线程对同一数据获得不同的值时,就会发生内存一致性错误。既两个线程对同一内存的读
写相互之间是不可见的。例如,假设定义了一个简单的 int 变量并初始化如下。
int counter = 0;
假设两个不同的线程 A 和 B 共享变量 counter。如果线程 A 执行改变变量 counter 的值的操作,如
下代码所示。
counter++;
而同时线程 B 执行输出变量 counter 值的操作,如下代码所示。
System.out.println(counter);
那么输出结果可能为 1,但是也有可能为 0,因为线程 B 在输出变量 counter 的值的时候,可能并不
知道线程 A 改变了变量 counter 的值。
要避免内存不一致性错误,关键是在线程之间建立一种 happens-before 关系,这种关系可以保证一
个语句对内存的写操作可以被另外一个指定的语句所知道。要创建这种 happens-before 关系,可以通过
同步来实现。在以下两种情况下,会自动创建 happens-before 关系:
  当一个语句调用 Thread.start()时,每一个与此语句有 happens-before 关系的语句,都会与新创建
的线程所执行的每一个语句建立 happens-before 关系。因此导致新的线程创建的代码所发生的
操作对新的线程也是可见的。
  当一个线程停止并调用另一个线程的 Thread.join()时,所有停止线程中执行的语句与 join()成功
后的所有语句都有一个 happens-before 关系。因此线程中的操作的结果对执行 join()以后的线程
·25·
都是可见的。
21.2.3   同步方法
同步方法指的是使用关键字 synchronized 控制对一个方法的并发访问。只有一个执行线程能访问一
个对象中使用 synchronized 关键字声明的方法。如果另一个线程也试图访问同一个对象中任何使用
synchronized 关键字声明的方法,它将会被挂起,直到第一个线程结束对该方法的执行。
换句话说,每个使用关键字 synchronized 关键字声明的方法都是一个访问共享资源的代码块,Java
只允许一个对象中一个该代码块的执行。静态方法有不同的行为。只有一个执行线程能访问一个对象中
使用 synchronized 关键字声明的静态方法,但是其它线程能访问这个对象中其它非静态方法。对于这一
点一定要非常小心,因为两个线程能访问两个不同的同步方法,如果一个是静态的而另一个是非静态的。
如果两个方法改变了同一个数据,就会发生数据不一致错误。
在下面的示例中,两个线程访问一个共同的对象。我们通过同步方法来保证数据的一致性。
【例】有一个银行账号和两个线程。一个线程用来向账号中存款,另一个线程从同一个账号中取款。
如果不使用同步方法,我们可能会得到不正确的结果。使用同步机制能确保最终的账户平衡。
1)  创建一个名为 Account 的类,它将代表我们的银行账号。它只有一个属性 balance,代表账户余
额。另外实现一个 addAmount()的方法,该方法为 balance 增加一定数量的值(代表存款操作)。
再实现一个名为 subtractAmount()的方法,它将 balance 的值减少一定数量(代表取款操作)。同一
时刻只能有一个线程改变该 balance 的值,所以使用 synchronized 关键字来同步这两个方法。
import java.util.concurrent.TimeUnit;
public class Account {
private double balance;    //代表账户余额
public double getBalance() {
return balance;
}
public void setBalance(double balance) {
this.balance = balance;
}
//同步方法,代表存款操作
public synchronized void addAmount(double amount) {
double tmp = balance;
try {
TimeUnit.MILLISECONDS.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
tmp += amount;
balance = tmp;
}
//同步方法,代表取款操作 
26
public synchronized void subtractAmount(double amount) {
double tmp = balance;
try {
TimeUnit.MILLISECONDS.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
tmp -= amount;
balance = tmp;
}
}
2)实现一个模拟 ATM 机的类。它将使用 subtractAmount()方法来减少一个账户的余额。这个类必须
实现 Runnable 接口,以作为线程被执行。
public class Bank implements Runnable {
private Account account;     //要操作的银行账户
public Bank(Account account) {
this.account = account;
}
@Override
public void run() {
//调用 100 次账户 account 的 subtractAmount()方法来减少账户余额
for (int i = 0; i < 100; i++) {
account.subtractAmount(1000);
}
}
}
3)实现一个类 Company,模拟一个公司,它使用 Account 类的 addAmount()方法来增加该账户的余
额(存款操作)。这个类必须实现 Runnable 接口,以作为线程被执行。
public class Company implements Runnable {
private Account account;     //要操作的银行账户
public Company(Account account) {
this.account = account;
}
@Override
public void run() {
//调用 100 次账户 account 的 addAmount()方法来增加账户余额
for (int i = 0; i < 100; i++) {
account.addAmount(1000);
}
}
·27·
}
4)编写测试类。
public class Test {
public static void main(String[] args) {
Account account = new Account();
account.setBalance(1000);    //将账户余额初始化为 1000
Company company = new Company(account);
Thread companyThread = new Thread(company);
Bank bank = new Bank(account);
Thread bankThread = new Thread(bank);
System.out.printf("账户:  初始余额: %f\n", account.getBalance());
//启动线程
companyThread.start();
bankThread.start();
//使用 join()方法等待两个线程结束,并在控制台打印最终的账户余额
try {
companyThread.join();
bankThread.join();
System.out.printf("账户:  最终余额: %f\n", account.getBalance());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
编译并执行程序,输出结果如下:
账户:  初始余额: 1000.000000
账户:  最终余额: 1000.000000
可以看到,在多个线程分别对同一个账户进行存取款操作后,账户余额仍然保持平衡。试着将
Account 类中的 synchronized 关键字取消掉,然后再运行程序,并观察输出结果。
正如我们之前提到的,只有一个线程可以访问一个对象中使用 synchronized 关键字声明的方法。如
果一个线程(A)正在执行一个同步方法,而另一个线程(B)想要执行同一对象中其它同步方法,那么线程
B 将被阻塞直到线程 A 结束。但是如果线程 B 访问了同一个类的不同对象,则不存在阻塞问题。
当同步方法退出时,它会自动地与任何对同一对象同步方法的后续调用建立 happens-before 关系。
这保证对象状态的改变对所有的线程都是可见的。
另外,不能将构造器方法声明为同步的,这没有意义,而且是一个语法错误。同步方法为防止线程
冲突和内存一致性错误提供了简单而有用的策略:如果一个对象可以被多个线程使用,那么对该对象的
变量的所有读写都要通过同步方法来实现。只有一个例外:当变量被定义为 final 时,这时是不能修改
final 类型的变量的,所以可以通过非同步方法安全地访问这一类型的变量。 
28
21.2.4   固定锁和同步
同步是构建在一个称为“固定锁”或“监视锁”的内部实体上的。固定锁在同步的各个方面都扮演
着重要的角色:增强对一个对象状态的排他性访问(互斥)以及建立 happens-before 关系。
每一个对象都有一个相关联的固定锁。按惯例,一个需要对一个对象的字段进行排他性和持续性访
问的线程,在访问之前要获得对象的固定锁,然后在访问以后释放固定锁。在获得和释放固定锁期间称
一个线程拥有固定锁。只要一个线程拥有一个固定锁,其他线程就不能获得同一固定锁。如果其他线程
试图获得锁,将会阻塞。
当一个线程释放一个固定锁时,就会在此动作和任何后续的获得同一锁的动作之间建立
happens-before 关系。
当一个线程调用一个同步方法时,它自动地获得该方法所属对象的固定锁,并且当方法返回时,线
程会释放这个锁。即使方法的返回是由一个未捕获的异常引起的,该固定锁也会被释放。
当一个静态同步方法被调用时,因为静态方法是与类相关联的,而不是与对象相关联,因此线程会
获得与该类所关联的 Class 对象的固定锁。所以控制对类的静态字段的访问的锁与用于该类的任何实例
对象的锁不同。
除了同步方法之外,另外一个创建同步代码的方式是同步语句。使用关键字保护对一个代码块的访
问而不是对整个方法同步。使用同步语句保护对共享数据的访问,而将其余的操作放在同步块之外,从
而获得更好的程序性能。这样做的目的是尽可能的使同一时刻只能被一个线程访问的代码块尽可能地
短。当使用同步语句时,必须传递一个对象引用作为参数(指定提供固定锁的对象)。只有一个线程可以
访问该对象的同步代码(块或方法)。一般情况下,我们使用 this 关键字来引用执行该方法的对象。
【例】重构上一节的银行存取款操作程序。
重构上一节银行存取款操作程序中的 Account 类,使用同步语句。其它部分保持不变。
import java.util.concurrent.TimeUnit;
public class Account2 {
private double balance;
public double getBalance() {
return balance;
}
public void setBalance(double balance) {
this.balance = balance;
}
//同步方法,代表存款操作
public void addAmount(double amount) {
synchronized (this) {
double tmp = balance;
try {
TimeUnit.MILLISECONDS.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();

·29·
tmp += amount;
balance = tmp;
}
}
//同步方法,代表取款操作
public void subtractAmount(double amount) {
synchronized (this) {
double tmp = balance;
try {
TimeUnit.MILLISECONDS.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
tmp -= amount;
balance = tmp;
}
}
}
同步语句还有助于提高细粒度的同步并发。例如,假设一个类 Lunch 有两个实例字段,m1 和 m2,
这两个字段被多个线程共享,但是相互又是独立的(m1 和 m2 永远不会一起使用)。所有对于这些字段的
更新必须被同步,但是没有理由禁止对 m1 和 m2 的交错更新,否则将会创建不必要的阻塞,从而降低
并发性。代替使用同步方法或其他使用与 this 相关联的锁,这里创建两个单一的对象来提供锁。
public class Lunch {
private long m1 = 0;
private long m2 = 0;
private Object lock1 = new Object();        //创建对象 lock1 作为一个锁
private Object lock2 = new Object();        //创建对象 lock2 作为一个锁
public void addm1() {              //更新 m1 的方法
synchronized(lock1) {            //使用同步锁 lock1,同步对 m1 的更新
m1++;
}
}
public void addm2() {              //更新 m2 的方法
synchronized(lock2) {            //使用同步锁 lock2,同步对 m2 的更新
m2++;
}
}
}
下面通过一个实例来学习这种一个类中两个独立属性的同步访问问题。
【例】模拟一个电影院。该电影院有两个放映厅和两个票务办公室。当两个票务办公室卖票时,它
们可以卖两个放映厅的座位,并接受两个电影厅的退票。但对同一个放映厅,售票和退票都必须保持同
步。但是对于两上放映厅来说,各自的座位数又是独立的。
1)创建一个名为 Cinema 的类,带有两个属性 vacanciesCinema1 和 vacanciesCinema2,分别代表两
个放映厅的空位数。另外向 Cinema 类添加两个额外的对象属性 controlCinema1 和 controlCinema2。在构
造器中初始化所有的属性。创建分别对两个放映厅售票和退票的方法,实现同步语句。
public class Cinema {
30
private long vacanciesCinema1;   //放映厅 1 空位数
private long vacanciesCinema2;   //放映厅 2 空位数
private final Object controlCinema1, controlCinema2;
public Cinema() {
controlCinema1 = new Object();
controlCinema2 = new Object();
vacanciesCinema1 = 20;
vacanciesCinema2 = 20;
}
//销售第一个电影院的票。它使用 controlCinemal 对象来控制对同步代码块的访问
public boolean sellTickets1(int number) {
synchronized (controlCinema1) {
if (number < vacanciesCinema1) {
vacanciesCinema1 -= number;
return true;
} else {
return false;
}
}
}
//销售第二个电影院的票。它使用 controlCinema2 对象来控制对同步代码块的访问
public boolean sellTickets2(int number) {
synchronized (controlCinema2) {
if (number < vacanciesCinema2) {
vacanciesCinema2 -= number;
return true;
} else {
return false;
}
}
}
//当有第一个电影院的退票时,调用此方法。它使用 controlCinema1 对象控制对同步代码块的访问
public boolean returnTickets1(int number) {
synchronized (controlCinema1) {
vacanciesCinema1 += number;
return true;
}
}
//当有第二个电影院的退票时,调用此方法。它使用 controlCinema1 对象控制对同步代码块的访问
public boolean returnTickets2(int number) {
synchronized (controlCinema2) {
vacanciesCinema2 += number;
return true;
}

·31·
public long getVacanciesCinema1() {
return vacanciesCinema1;
}
public long getVacanciesCinema2() {
return vacanciesCinema2;
}
}
2)创建一个实现 Runnable 接口的类 TicketOffice1,代表第一个票务办公室。
public class TicketOffice1 implements Runnable {
private Cinema cinema;
public TicketOffice1(Cinema cinema) {
this.cinema = cinema;
}
//票务办公室的业务:售票和退票
@Override
public void run() {
cinema.sellTickets1(3);
cinema.sellTickets1(2);
cinema.sellTickets2(2);
cinema.returnTickets1(3);
cinema.sellTickets1(5);
cinema.sellTickets2(2);
cinema.sellTickets2(2);
cinema.sellTickets2(2);
}
}
3)创建一个实现 Runnable 接口的类 TicketOffice2,代表第二个票务办公室。
public class TicketOffice2 implements Runnable {
private Cinema cinema;
public TicketOffice2(Cinema cinema) {
this.cinema = cinema;
}
//票务办公室的业务:售票和退票
@Override
public void run() {
cinema.sellTickets2(2);
cinema.sellTickets2(4);
cinema.sellTickets1(2);
cinema.sellTickets1(1);
cinema.returnTickets2(2);
cinema.sellTickets1(3); 
32
cinema.sellTickets2(2);
cinema.sellTickets1(2);
}
}
4)测试。
public class Test {
public static void main(String[] args) {
Cinema cinema = new Cinema();
TicketOffice1 ticketOffice1 = new TicketOffice1(cinema);
Thread thread1 = new Thread(ticketOffice1, "TicketOffice1");
TicketOffice2 ticketOffice2 = new TicketOffice2(cinema);
Thread thread2 = new Thread(ticketOffice2, "TicketOffice2");
thread1.start();
thread2.start();
//等待线程结束
try {
thread1.join();
thread2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
//将空座位数写入到控制台
System.out.printf("放映厅 1 空余座位数: %d\n", cinema.getVacanciesCinema1());
System.out.printf("放映厅 2 空余座位数: %d\n", cinema.getVacanciesCinema2());
}
}
21.2.5   在同步代码中使用条件
在并发程序中一个经典的问题是生产者-消费者问题。我们有一个数据缓存,一个或多个数据生产
者,将生产的数据存储在缓存中,以及一个或多个数据消费者,从缓存中取数据。
因为该缓存是一个共享的数据结构,所以我们必须使用一个同步机制来控制对它的访问,如关键字
synchronized。但是这种生产者-消费者有一些限制。如果缓存满了的话,生产者无法在该缓存中保存数
据,如果该缓存是空的,则消费都无法从该缓存中取数据。
对于这种情况,Java 在 Object 类中提供了 wait()、notify()和 notifyAll()方法的实现。一个线程可以
在一个代码同步块中调用 wait()方法。如果它在一个代码同步块外部调用 wait()方法,则 JVM 会抛出一
个 IllegalMonitorStateException 异常。当线程调用 wait()方法时,JVM 将该线程置于 sleep 状态,并释放
控制正在执行的代码同步块的对象,并允许其它线程执行受该对象保护的其它同步代码块。要唤醒该线
程,必须在一个受同一个对象保护的代码块中调用 notify()或 notifyAll()方法。
这一节,我们将学习怎样使用 synchronized 关键字和 wait()、notify()和 notifyAll()方法实现生产者-
·33·
消费者问题。
【例】生产者-消费者问题。
1)创建一个名为 EventStorage 的类。在该类中存储共享的数据。
import java.util.Date;
import java.util.LinkedList;
import java.util.Queue;
public class EventStorage {
private int maxSize;
private Queue<Date> storage;     //队列
public EventStorage() {
maxSize = 10;
storage = new LinkedList<>();
}
//在 storage 中存储一个事件
public synchronized void set() {
//首先判断该 storage 是否已满。如果已满,就调用 wait()方法直到 storage 有存储空间
while (storage.size() == maxSize) {
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
storage.offer(new Date());
System.out.printf("Set: %d", storage.size());
//唤醒所有在 wait()方法中休眠的线程
notifyAll();
}
//从 storge 中获得一个事件
public synchronized void get() {
//判断 storge 中有没有事件。如果没有事件,就调用 wait()方法直到 storage 中有事件
while (storage.size() == 0) {
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.printf("Get: %d: %s", storage.size(), storage.poll().toLocaleString());
//唤醒所有在 wait()方法中处于休眠的线程
notifyAll();
}
}
2)创建生产者线程类。 
34
public class Producer implements Runnable {
private EventStorage storage;
public Producer(EventStorage storage) {
this.storage = storage;
}
@Override
public void run() {
for (int i = 0; i < 100; i++) {
storage.set();
}
}
}
3)创建消费者线程类。
public class Consumer implements Runnable {
private EventStorage storage;
public Consumer(EventStorage storage) {
this.storage = storage;
}
@Override
public void run() {
for (int i = 0; i < 100; i++) {
storage.get();
}
}
}
4)测试。
public class Test {
public static void main(String[] args) {
EventStorage storage = new EventStorage();
Producer producer = new Producer(storage);
Thread thread1 = new Thread(producer);
Consumer consumer = new Consumer(storage);
Thread thread2 = new Thread(consumer);
thread2.start();
thread1.start();        
}
}
编译并运行程序,观察输出结果。
上述同步方法中使用了守护块。守护块以循环检测一个条件开始,只有条件为 true 时,块才能继续
·35·
进行下去。
【例】“生产者-消费者”应用程序示例。
(1)首先创建由生产者和消费者所共享的对象:Drop 类。Drop 类中定义两个同步的方法,take()
方法用来读取消息,而 put()方法用来发送消息。 Drop 类位于 chapter21 包中,并单独保存为 java 源文件。
public class Drop {
//从生产者发往消费者的消息.
private String message;
//如果消费者在等待生产者发送消息,为 true;如果生产者在等待消费者获取数据,为 false。
private boolean empty = true;
public synchronized String take() {
//消费者等待,直到有消息可用.
while (empty) {              //循环等待条件 empty 成立
try {
wait();              //等待其他线程唤醒
} catch (InterruptedException e) {}
}
empty = true;    //改变状态
notifyAll();      //通知生产者状态已经改变
return message;
}
public synchronized void put(String message) {
//等待,直到消息被取走.
while (!empty) {
try { 
wait();              //等待其他线程唤醒
} catch (InterruptedException e) {}
}
empty = false;      //改变状态
this.message = message;  //存储消息
notifyAll();    //通知消费者状态已经改变
}
}
(2)生产者线程定义在 Producer.java 中,它发送一系列的消息。如果发送的消息为字符串“over”,
说明所有的消息已经发送完毕。为了模拟真实世界应用程序的不可预知性,生产者线程会在发送消息中
间暂停一个随机的时间。Producer 类仍然位于 chapter21 包中。
import java.util.Random;
public class Producer implements Runnable {
private Drop drop;            //声明一个生产者和消费者所共享的对象
public Producer(Drop drop) {        //构造器中初始化共享对象
this.drop = drop; 
36
}
public void run() {
String importantInfo[] = {"消息 1","消息 2","消息 3","消息 4"};
Random random = new Random();    //创建一个随机对象
for (int i = 0; i < importantInfo.length; i++) {  //循环向共享对象中放入消息
drop.put(importantInfo[i]);
try {
Thread.sleep(random.nextInt(5000));  //休眠一个随机时间
} catch (InterruptedException e) {}
}
drop.put("over");            //线程结束时,发出通知消息
}
}
(3)消费者线程,定义在类 Consumer 中,简单地获取消息并打印输出。如果收到“over”字符串
就结束。这个线程也暂停一个随机的间隔时间。
import java.util.Random;
public class Consumer implements Runnable {
private Drop drop;            //声明一个生产者和消费者所共享的对象
public Consumer(Drop drop) {        //构造器中初始化共享对象
this.drop = drop;
}
public void run() {
Random random = new Random();    //创建一个随机对象
String message = drop.take();      //从共享对象中获取生产者“生产”的消息
while(! message.equals("over")){      //如果不是结束的消息,则输出
System.out.format("MESSAGE RECEIVED: %s%n", message);
try {
Thread.sleep(random.nextInt(5000));
} catch (InterruptedException e) {}
message = drop.take();      //继续从共享对象中获取“生产”的消息
}
}
}
(4)最后,在 ProducerConsumerDemo.java 文件中,定义主线程,用来启动生产者和消费者线程。
public class ProducerConsumerDemo{
public static void main(String[] args) {
Drop drop = new Drop();        //创建生产者和消费者共享的对象
(new Thread(new Producer(drop))).start();  //创建并启动第 1 个线程
(new Thread(new Consumer(drop))).start();  //创建并启动第 2 个线程
}
}
(5)编译并运行程序,输出结果如下所示:
获取的消息为:  消息 1
获取的消息为:  消息 2
获取的消息为:  消息 3 
·37·
获取的消息为:  消息 4
上面的程序使用守护块来创建一个“生产-消费者”应用程序。这类应用程序在两个线程间共享数
据:生产者创建数据,而消费者处理数据。这两个线程使用一个共享对象进行通信。一个基本的条件是,
消费者线程一定不能在生产者线程分发数据前获得数据,同样的,如果消费者线程还没有获取原来的数
据,生产者线程一定不能分发新的数据。
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值