“If you put out your hands, you are a laborer; if you put out your hands and mind, you are a craftsperson; if you put out your hands, mind, heart and soul, you are an artist.”
1.进程与线程
1.1 进程
进程是程序的一次执行过程,是系统运行程序的基本单位,当一个程序进入内存运行时,即变成了一个进程。进程是系统进行资源分配和调用的独立单位,每个进程都有它自己的内存空间和系统资源。
一般而言,进程包含着如下三个特征:
- 独立性:进程是系统中独立存在的实体,它可以拥有自己独立的资源,每一个进程都拥有自己私有的地址空间。在没有经过进程本身允许的情况下,一个用户进程不可以直接访问其他进程的地址空间。
- 动态性:进程与程序的区别在于,程序只是一个静态的指令集合,而进程是一个正在系统中活动的指令集合。在进程中加入了时间的概念。进程具有自己的生命周期和各种不同的状态,这些概念在程序中都是不具备的。
- 并发性:多个进程可以在单个处理器上并发执行,多个进程之间不会相互干扰。
并发性(concurrency)和并行性(parallel)是两个概念,并行在同一个时刻,有多条指令在多个处理器上同时执行;并发性是指在同一个时刻只能有一条指令执行,但是多个进程指令被快速轮换执行,使得在宏观上具有多个进程同时执行的效果。
在多任务操作系统中,表面上看是支持进程并发执行的,例如可以一边听音乐一边聊天。但实际上这些进程并不是同时运行的。在计算机中,所有的应用程序都是由CPU执行的,对于一个CPU而言,在某个时间点只能运行一个程序,也就是说只能执行一个进程。操作系统会为每一个进程分配一段有限的CPU使用时间,CPU在这段时间中执行某个进程,然后会在下一段时间切换到另一个进程中去执行。由于CPU运行速度很快,能在极短的时间内在不同的进程之间进行切换,所以给人以同时执行多个程序的感觉。
1.2 线程
每个运行的程序都是一个进程,在一个进程中还可以有多个执行单元同时运行,这些执行单元可以看成程序执行的一条条线索,被称为线程(Thread),也被称为轻量级进程。操作系统中的每一个进程中都至少存在一个线程。例如当一个Java程序启动时,就会产生一个进程,该进程中会默认创建一个线程,在这个线程上会运行main()方法中的代码。
线程是进程的组成部分,一个进程可以拥有多个线程,一个线程必须有一个父进程。与进程不同的是同类的多个线程共享进程的堆和方法区资源,但是每个线程有自己的程序计数器、虚拟机栈和本地方法栈,所以系统在产生一个线程,或是在各个线程之间切换工作时,负担要比进程小得多,也正是如此,线程被称为轻量级进程。
举个例子理解进程和线程:一个公司的运作可以比作为一个进程的执行,公司内各个部门,如财务部门,人事部门,业务部门等组成进程,他们的运作才支撑起公司的运作,各个部门则可以理解为各个线程,每个线程用于完成不同的工作,进而使得进程能正确的运转;即线程组成进程,进程囊括线程,是一个组成和被组成的关系。
用术语来讲就是,进程(Process)是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。在早期面向进程设计的计算机结构中,进程是程序的基本执行实体;在当代面向线程设计的计算机结构中,进程是线程的容器。程序是指令、数据及其组织形式的描述,进程是程序的实体。线程就是轻量级进程,是程序执行的最小单位。
使用多线程而不是用多进程去进行并发程序的设计,是因为线程间的切换和调度的成本远远小于进程。
Java 程序天生就是多线程程序,我们可以通过 JMX 来看一下一个普通的 Java 程序有哪些线程,代码如下。
1
2
3
4
5
6
7
8
9
10
11
12
13
public class MultiThread {
public static void main(String[] args) {
// 获取 Java 线程管理 MXBean
ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean();
// 不需要获取同步的 monitor 和 synchronizer 信息,仅获取线程和线程堆栈信息
ThreadInfo[] threadInfos = threadMXBean.dumpAllThreads(false, false);
// 遍历线程信息,仅打印线程 ID 和线程名称信息
for (ThreadInfo threadInfo : threadInfos) {
System.out.println("[" + threadInfo.getThreadId() + "] " + threadInfo.getThreadName());
}
}
}
上述程序输出如下(输出内容可能不同,不用太纠结下面每个线程的作用,只用知道 main 线程执行 main 方法即可):
1
2
3
4
5
6
[6] Monitor Ctrl-Break //监听中断信号
[5] Attach Listener //添加事件,该线程是负责接收到外部的命令,执行该命令,并且把结果返回给发送者。通常我们会用一些命令去要求jvm给我们一些反馈信息,如:java -version、jmap、jstack等等。如果该线程在jvm启动的时候没有初始化,那么,则会在用户第一次执行jvm命令时,得到启动
[4] Signal Dispatcher // 分发处理给 JVM 信号的线程,前面我们提到第一个Attach Listener线程的职责是接收外部jvm命令,当命令接收成功后,会交给signal dispather线程去进行分发到各个不同的模块处理命令,并且返回处理结果。signal dispather线程也是在第一次接收外部jvm命令时,进行初始化工作。
[3] Finalizer //调用对象 finalize 方法的线程, JVM在垃圾收集时会将失去引用的对象包装成Finalizer对象(Reference的实现),并放入ReferenceQueue,由Finalizer线程来处理;最后将该Finalizer对象的引用置为null,由垃圾收集器来回收。
[2] Reference Handler //清除 reference 线程, 它主要用于处理引用对象本身(软引用、弱引用、虚引用)的垃圾回收问题。
[1] main //main 线程,程序入口
从上面的输出内容可以看出:一个 Java 程序的运行是 main 线程和多个其他线程同时运行。
1.3 线程与进程的关系、区别及优缺点
1.3.1 图解进程与线程的关系
给出Java内存区域的图如下所示,通过下图我们从JVM的角度来说一下线程与进程之间的关系

从上图可以看出:一个进程中可以有多个线程,多个线程共享进程的堆和方法区(JDK 1.8之后的元空间)资源,但是每个线程都有自己的程序计数器、虚拟机栈和本地方法栈。
总结:线程 是 进程 划分成的更小的运行单位。线程和进程最大的不同在于基本上各进程是独立的,而各线程则不一定,因为同一进程中的线程极有可能会相互影响。线程执行开销小,但不利于资源的管理和保护;而进程正相反。
拓展问题:为什么程序计数器、虚拟机栈和本地方法栈是线程私有的呢?为什么堆和方法区是线程共享的呢?
首先,对于程序技术器,它主要有以下两个作用:
- 字节码解释器通过改变程序计数器来依次读取指令,从而实现代码的流程控制,如:顺序执行、选择、循环、异常处理。
- 在多线程的情况下,程序计数器用于记录当前线程执行的位置,从而当线程被切换回来的时候能够知道该线程上次运行到哪里。
但是,需要注意的是,如果执行的是native方法,那么程序计数器记录的是undefined地址,只有执行的是Java代码时程序计数器记录的才是下一条指令的地址。所以,程序计数器私有主要是为了线程切换后能恢复到正确的执行位置。
而对于虚拟机栈和本地方法栈而言,其主要作用如下:
- 虚拟机栈:每个Java方法在执行的同时会创建一个栈帧用于存储局部变量表、操作数栈、常量池引用等信息。从方法调用直至执行完成的过程,就对应着一个栈帧在Java虚拟机中入栈和出栈的过程。
- 本地方法栈:和虚拟机栈发挥的作用类似,其区别是:虚拟机栈为虚拟机执行Java方法(字节码)服务的,而本地方法栈则为虚拟机使用到的Native方法服务。
所以,为了保证线程中的局部变量不被别的线程访问到,虚拟机栈和本地方法栈也是线程私有的。
1.3.2 多线程的优缺点
首先,使用多线程编程具有如下几个优点:
- 进程之间不能共享内存,但是线程之间共享内存非常容易;
- 系统创建进程时需要为该进程重新分配系统资源,但创建线程则代价小得多,因此使用多线程来实现多任务并发比多进程的效率高。
- Java语言内置了多线程功能支持,而不是单纯地作为底层操作系统的调度方式,从而简化了Java的多线程编程。
多线程编程的目的就是为了能提升程序运行速度,但是并发编程并不总是能提升运行速度的,其可能会遇到种种问题,例如:内存泄漏、上下文切换、死锁还有受限于硬件和软件的资源闲置问题等等。
1.4 线程的生命周期和状态
不同的书籍对于线程的生命周期状态的描述各不相同,而两种分类都有其所在的道理。为此,我也总结了两种线程状态。面试的时候可以两种线程生命周期选取其中之一。
1.4.1 源码层面的生命周期
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
public enum State {
/**
* Thread state for a thread which has not yet started.
*/
NEW,
/**
* Thread state for a runnable thread. A thread in the runnable
* state is executing in the Java virtual machine but it may
* be waiting for other resources from the operating system
* such as processor.
*/
RUNNABLE,
/**
* Thread state for a thread blocked waiting for a monitor lock.
* A thread in the blocked state is waiting for a monitor lock
* to enter a synchronized block/method or
* reenter a synchronized block/method after calling
* {@link Object#wait() Object.wait}.
*/
BLOCKED,
/**
* Thread state for a waiting thread.
* A thread is in the waiting state due to calling one of the
* following methods:
* <ul>
* <li>{@link Object#wait() Object.wait} with no timeout</li>
* <li>{@link #join() Thread.join} with no timeout</li>
* <li>{@link LockSupport#park() LockSupport.park}</li>
* </ul>
*
* <p>A thread in the waiting state is waiting for another thread to
* perform a particular action.
*
* For example, a thread that has called <tt>Object.wait()</tt>
* on an object is waiting for another thread to call
* <tt>Object.notify()</tt> or <tt>Object.notifyAll()</tt> on
* that object. A thread that has called <tt>Thread.join()</tt>
* is waiting for a specified thread to terminate.
*/
WAITING,
/**
* Thread state for a waiting thread with a specified waiting time.
* A thread is in the timed waiting state due to calling one of
* the following methods with a specified positive waiting time:
* <ul>
* <li>{@link #sleep Thread.sleep}</li>
* <li>{@link Object#wait(long) Object.wait} with timeout</li>
* <li>{@link #join(long) Thread.join} with timeout</li>
* <li>{@link LockSupport#parkNanos LockSupport.parkNanos}</li>
* <li>{@link LockSupport#parkUntil LockSupport.parkUntil}</li>
* </ul>
*/
TIMED_WAITING,
/**
* Thread state for a terminated thread.
* The thread has completed execution.
*/
TERMINATED;
}
Java线程在运行的生命周期中的指定时刻只可能处于下面6种不同状态的其中一个状态:
| 状态名称 | 说明 |
|---|---|
| NEW | 初始状态,线程被构建,但是还没有调用start()方法 |
| RUNNABLE | 运行状态,Java线程将操作系统中的就绪和运行两种状态笼统地称作“运行中”,对应于操作系统中的ready和running。也就是说,RUNNABLE状态既可以是可运行的,也可以是实际运行中的,有可能正在执行,也有可能没有正在执行。 |
| BLOCKED | 阻塞状态,表示线程阻塞于锁 |
| WAITING | 等待状态,表示线程进入等待状态,进入该状态表示当前线程需要等待其他线程做出一些特定动作(通知或中断) |
| TIME_WAITING | 超时等待状态,该状态不同于WAITING,它是可以在指定的时间自行返回的 |
| TERMINATED | 终止状态,表示当前线程已经执行完毕 |
线程在生命周期中国并不是固定处于某一个状态,而是随着代码的执行在不同状态之间切换。给出线程状态转换如下图所示:

新建(NEW)
创建后尚未启动。
可运行(Runnable)
可能正在运行,也可能正在等待CPU时间片。其包含了操作系统线程状态中的运行(Running)和就绪(Ready)。
阻塞(Blocked)
这个状态下,是在多个线程有同步操作的场景,比如正在等待另一个线程的synchronized块的执行释放,或者可重入的synchronized块里别人调用wait()方法,也就是线程在等待进入临界区。阻塞可以分为:等待阻塞、同步阻塞以及其他阻塞。
无限期等待(Waiting)
等待其他线程显示地唤醒,否则不会被分配CPU时间片。 进入方法 | 退出方法 — | — 没有设置Timeout参数的Object.wait()方法 | Object.notify() / Object.notifyAll() 没有设置Timeout参数的Thread.join()方法 | 被调用的线程执行完毕 LockSupport.park()方法 | LockSupport.unpark(Thread)
限期等待(Timed Waiting)
无需等待其他线程显示地唤醒,在一定时间之后被系统自动唤醒。
- 调用Thread.sleep()方法进入限期等待状态时,常常用“使一个线程睡眠”进行描述。
- 调用Object.wait()方法使线程进入限期等待或者无限期等待时,常常用“挂起一个线程”进行描述。
阻塞和等待的区别在于:阻塞是被动的,它是在等待获取一个排他锁。而等待是主动的,通过调用Thread.sleep()和Object.wait()方法进入。
| 进入方法 | 退出方法 |
|---|---|
| Thread.sleep()方法 | 时间结束 |
| 设置了Timeout参数的Object.wait()方法 | 时间结束/Object.notify()/Object.notifyALL() |
| 设置了Timeout参数的Thread.join()方法 | 时间结束/被调用的线程执行完毕 |
| LockSupport.parkNanos()方法 | LockSupport.unpark(Thread) |
| LockSupport.parkUntil()方法 | LockSupport.unpark(Thread) |
死亡(Terminated)
- 线程因为run方法或call()正常退出而自然死亡
- 因为一个没有捕获的异常终止了run方法而意外死亡
- 直接调用该线程的stop()方法来结束该线程——该方法容易导致死锁,通常不推荐使用。
在《Java并发编程艺术》书中,给出了Java线程状态变迁的图如下所示:

由上图可以看出,线程被创建之后它将处于NEW(新建)状态,调用start()方法后开始运行,线程这时候处于READY(可运行状态)。可运行状态的线程获取到了CPU时间片后就可以处于RUNNING(运行)状态。
操作系统隐藏 Java 虚拟机(JVM)中的 RUNNABLE 和 RUNNING 状态,它只能看到 RUNNABLE 状态(,所以 Java 系统一般将这两个状态统称为 RUNNABLE(运行中) 状态 。

当线程执行wait()方法之后,线程进入WAITING(等待)状态。进入等待状态的线程需要依靠其他线程的通知才能返回到运行状态,而TIMEWAITING(超时等待)状态相当于在等待状态的基础上增加了超时限制,比如通过sleep(long millis)方法或wait(long millis)方法可以将Java线程置于TIMED WAITING状态。当超时时间到达后Java线程将会返回到RUNNABLE状态。当线程调用同步方法时,在没有获取到锁的情况下,线程将会进入到BLOCKED(阻塞)状态。线程在执行Runnable的run()方法之后将会进入到TERMINATED(终止)状态。
1.4.2 另一种层面的生命周期
当线程被创建且启动之后,它既不是一开始就进入了执行状态,也不是一直处于执行状态,在线程的生命周期中,它要经过新建(NEW)、就绪(Runnable)、运行(Running)、阻塞(Blocked)和死亡(Dead)五种状态。

1.新建(New)
用new关键字和Thread类或其子类建立一个线程对象后,该线程对象就处于新生状态。处于新生状态的线程有自己的内存空间,通过调用start方法进入就绪状态(Runnable)。(注意:不能对已经启动的线程再次调用start()方法,否则会出现java.lang.IllegalThreadStateException异常。)
2.就绪(Runnable)
处于就绪状态的线程已经具备了运行条件,但还没有分配到CPU,处于线程就绪队列(尽管是采用队列形式,事实上,把它称为可运行池而不是可运行队列。因为cpu的调度不一定是按照先进先出的顺序来调度的),等待系统为其分配CPU。等待状态并不是执行状态,当系统选定一个等待执行的Thread对象后,它就会从等待执行状态进入执行状态,系统挑选的动作称之为“cpu调度”。一旦获得CPU,线程就进入运行状态并自动调用自己的run方法。(提示:如果希望子线程调用start()方法后立即执行,可以使用Thread.sleep()方式使主线程睡眠一伙儿,转去执行子线程。)
3.运行(Running)
处于运行状态的线程最为复杂,它可以变为阻塞状态、就绪状态和死亡状态。
对于处于就绪状态的线程,如果获得了CPU的调度,就会从就绪状态变为运行状态,执行run()方法中的任务。如果该线程失去了CPU资源,就会又从运行状态变为就绪状态,重新等待系统分配资源。也可以对在运行状态的线程调用yield()方法,它会让出cpu资源,再次变为就绪状态。
当发生以下情况时,线程将会进入阻塞状态:
- 线程调用sleep方法主动放弃所占用的系统资源;
- 线程调用一个阻塞式IO方法,在该方法返回之前,该线程被阻塞
- 线程试图获得一个同步监视器,但更改同步监视器正被其他线程所持有
- 线程在等待某个通知(notify)
- 程序调用了线程的suspend方法将线程挂起。不过该方法容易导致死锁,所以程序应该尽量避免使用该方法
当线程的run()方法执行完,或者被强制性地终止,例如出现异常,或者调用了stop()、desyory()方法等等,就会从运行状态转变为死亡状态。
4.阻塞(Blocked)
处于运行状态的线程在某些情况下,如执行了sleep(睡眠)方法,或等待I/O设备等资源,将让出CPU并暂时停止自己的运行,进入阻塞状态。在阻塞状态的线程不能进入就绪队列。只有当引起阻塞的原因消除时,如睡眠时间已到,或等待的I/O设备空闲下来,线程便转入就绪状态,重新到就绪队列中排队等待,被系统选中后从原来停止的位置开始继续运行。其线程重新进入就绪状态的情况如下:
- 调用sleep()方法的线程已经过了指定时间。
- 线程调用的阻塞式IO方法已经返回。
- 线程成功获得了试图取得的同步监视器。
- 线程正在等待某个通知时,其他线程发出了一个通知。
- 处于挂起状态的线程被调用了resume()恢复方法。
5.死亡状态(Dead)
- 线程因为run方法或call()正常退出而自然死亡
- 因为一个没有捕获的异常终止了run方法而意外死亡
- 直接调用该线程的stop()方法来结束该线程——该方法容易导致死锁,通常不推荐使用。
线程一旦死亡,就不能复生。 如果在一个死去的线程上调用start()方法,会抛出java.lang.IllegalThreadStateException异常。
1.5 线程死锁及死锁的避免方法
1.5.1 什么是死锁
当多个线程同时被阻塞,它们中的一个或者全部等待某个资源被释放。由于线程被无限期地阻塞,因此程序不可能正常终止。如下图所示,线程A持有资源2,线程B持有资源1,它们同时都想申请对方的资源,所以这两个线程就会相互等待,从而进入死锁状态。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
public class DeadLockDemo {
private static Object resource1 = new Object();//资源 1
private static Object resource2 = new Object();//资源 2
public static void main(String[] args) {
new Thread(() -> {
synchronized (resource1) {
System.out.println(Thread.currentThread() + "get resource1");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread() + "waiting get resource2");
synchronized (resource2) {
System.out.println(Thread.currentThread() + "get resource2");
}
}
}, "线程 1").start();
new Thread(() -> {
synchronized (resource2) {
System.out.println(Thread.currentThread() + "get resource2");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread() + "waiting get resource1");
synchronized (resource1) {
System.out.println(Thread.currentThread() + "get resource1");
}
}
}, "线程 2").start();
}
}
输出:
1
2
3
4
Thread[线程 1,5,main]get resource1
Thread[线程 2,5,main]get resource2
Thread[线程 1,5,main]waiting get resource2
Thread[线程 2,5,main]waiting get resource1
线程 A 通过 synchronized (resource1) 获得 resource1 的监视器锁,然后通过 Thread.sleep(1000);让线程 A 休眠 1s 为的是让线程 B 得到执行然后获取到 resource2 的监视器锁。线程 A 和线程 B 休眠结束了都开始企图请求获取对方的资源,然后这两个线程就会陷入互相等待的状态,这也就产生了死锁。上面的例子符合产生死锁的四个必要条件。
操作系统产生死锁必须具备以下四个条件:
- 互斥条件:该资源在任意一个时刻只由一个线程占用。
- 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
- 不剥夺条件:线程已获得的资源在未使用完之前不能被其他线程强行剥夺,只有自己使用完毕后才释放资源。
- 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。
1.5.2 如何避免死锁
我们只要破坏产生死锁的四个条件中的其中一个就可以了。
破坏互斥条件
这个条件我们没有办法破坏,因为我们用锁本来就是想让他们互斥的(临界资源需要互斥访问)。
破坏请求与保持条件
一次性申请所有的资源。
破坏不剥夺条件
占用部分资源的线程进一步申请其他资源时,如果申请不到,可以主动释放它占有的资源。
破坏循环等待条件
靠按序申请资源来预防。按某一顺序申请资源,释放资源则反序释放。破坏循环等待条件。
对于破坏循环等待条件,我们可以对线程2的代码修改成如下就不会产生死锁:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
new Thread(() -> {
synchronized (resource1) {
System.out.println(Thread.currentThread() + "get resource1");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread() + "waiting get resource2");
synchronized (resource2) {
System.out.println(Thread.currentThread() + "get resource2");
}
}
}, "线程 2").start();
输出结果:
1
2
3
4
5
6
7
8
Thread[线程 1,5,main]get resource1
Thread[线程 1,5,main]waiting get resource2
Thread[线程 1,5,main]get resource2
Thread[线程 2,5,main]get resource1
Thread[线程 2,5,main]waiting get resource2
Thread[线程 2,5,main]get resource2
Process finished with exit code 0
线程 1 首先获得到 resource1 的监视器锁,这时候线程 2 就获取不到了。然后线程 1 再去获取 resource2 的监视器锁,可以获取到。然后线程 1 释放了对 resource1、resource2 的监视器锁的占用,线程 2 获取到就可以执行了。这样就破坏了破坏循环等待条件,因此避免了死锁。
2. 多线程的实现方案
Java使用Thread类代表线程,所有的线程对象都必须是Thread类或者其子类的实例。每个线程的作用是完成一定的任务,实际上就是执行一段程序流。在Java中,使用线程执行体来代表这段程序流。在Java中有三种使用线程的办法:
- 继承Thread类;
- 实现Runnable接口;
- 实现Callable接口。
实现 Runnable 和 Callable 接口的类只能当做一个可以在线程中运行的任务,不是真正意义上的线程,因此最后还需要通过 Thread 来调用。可以说任务是通过线程驱动从而执行的。
2.1 继承Thread类,重写run()方法
通过继承Thread类来创建并启动多线程的步骤如下:
- 定义Thread类的子类,并重写该类的run()方法,该run()方法的方法体就代表了线程需要完成的任务。因此把run()方法作为线程执行体。
- 创建Thread子类的实例,即创建了线程对象。
- 调用线程对象的start()方法来启动该线程。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
//1、定义一个类继承Thread类。
class MyThread extends Thread {
private String name;
MyThread(String name) {
this.name = name;
}
// 2、覆盖Thread类中的run方法。
public void run() {
for (int x = 0; x < 5; x++) {
System.out.println(name + "...x=" + x + "...ThreadName="
+ Thread.currentThread().getName());
}
}
}
class ThreadTest {
public static void main(String[] args) {
// 3、直接创建Thread的子类对象创建线程。
MyThread d1 = new MyThread("黑马程序员");
MyThread d2 = new MyThread("中关村在线");
// 4、调用start方法开启线程并调用线程的任务run方法执行。
d1.start();
d2.start();
for (int x = 0; x < 5; x++) {
System.out.println("x = " + x + "...over..."
+ Thread.currentThread().getName());
}
}
}
输出结果:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
黑马程序员...x=0...ThreadName=Thread-0
中关村在线...x=0...ThreadName=Thread-1
x = 0...over...main
中关村在线...x=1...ThreadName=Thread-1
黑马程序员...x=1...ThreadName=Thread-0
中关村在线...x=2...ThreadName=Thread-1
x = 1...over...main
中关村在线...x=3...ThreadName=Thread-1
黑马程序员...x=2...ThreadName=Thread-0
中关村在线...x=4...ThreadName=Thread-1
x = 2...over...main
x = 3...over...main
x = 4...over...main
黑马程序员...x=3...ThreadName=Thread-0
黑马程序员...x=4...ThreadName=Thread-0
注意:start()方法的调用后并不是立即执行多线程代码,而是使得该线程变为可运行态(Runnable),什么时候运行是由操作系统决定的。 从程序运行的结果可以发现,多线程程序是乱序执行。因此,只有乱序执行的代码才有必要设计为多线程。
启动线程调用的是start()方法,而不是run()方法,run()只是封装了被线程执行的代码。如果调用run()方法的话,只是普通的方法调用,无法启动线程。(start()方法是启动了线程,再由JVM去调用了该线程的run()方法).
2.2 实现Runnable接口
实现Runnable接口来创建并启动多线程的步骤如下:
- 定义Runnable接口的实现类,并重写该接口的run()方法,该run()方法的方法体同样是该线程的线程执行体。
- 创建Runnable实现类的实例,并以此实例作为Thread的target来创建Thread对象,该Thread对象才是真正的线程对象。
- 调用线程对象的start()方法来开启线程。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 1、定义类实现Runnable接口。
class MyThread implements Runnable {
// 2、覆盖接口中的run方法,将线程的任务代码封装到run方法中。
private int i;
@Override
public void run() {
for (; i < 20; i++){
System.out.println(Thread.currentThread().getName() + " " + i);
}
}
}
class ThreadTest {
public static void main(String[] args) {
MyThread d = new MyThread();
// 3、通过Thread类创建线程对象,并将Runnable接口的子类对象作为Thread类的构造函数的参数进行传递。
Thread t1 = new Thread(d);
Thread t2 = new Thread(d);
// 4、调用线程对象的start方法开启线程。
t1.start();
t2.start();
}
}
运行结果:
1
2
3
4
5
6
7
8
9
10
11
12
13
Thread-1 0
Thread-0 0
Thread-1 1
Thread-0 2
Thread-1 3
Thread-0 4
Thread-1 5
Thread-0 6
Thread-1 7
Thread-0 8
Thread-1 9
Thread-0 10
Thread-1 11
实现接口方法的好处有:
- 可以避免由于Java单继承带来的局限性,所以创建线程的第二种方式较为常用。
- 适合多个相同程序的代码其处理同一个资源的情况,把线程同程序的代码、数据有效分离,较好的体现了面向对象的设计思想。
2.3 实现Callable接口
创建并启动有返回值的线程步骤如下:
- 创建Callable接口的实现类,并实现call()方法,该call()方法将作为线程执行体,且call()方法有返回值,再创建Callable实现类的实例。从Java 8开始,可以直接使用Lambda表达式创建Callable对象。
- 使用FutureTask类来包装Callable对象,该FutureTask对象封装了该Callable对象的call()方法的返回值;
- 使用FutureTask对象作为Thread对象的target创建并启动新线程。
- 调用FutureTask对象的get()方法来获得子线程执行结束后的返回值。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class ThreadTest {
public static void main(String[] args) {
ThreadTest test = new ThreadTest();
FutureTask<Integer> task = new FutureTask<Integer>(() ->{
int i = 0;
for (; i < 20; i++){
System.out.println(Thread.currentThread().getName() + " " + i);
}
return i;
});
new Thread(task).start();
try{
System.out.println("子线程的返回值: " + task.get());
}catch (Exception e){
e.printStackTrace();
}
}
}
输出结果:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
Thread-0 0
Thread-0 1
Thread-0 2
Thread-0 3
Thread-0 4
Thread-0 5
Thread-0 6
Thread-0 7
Thread-0 8
Thread-0 9
Thread-0 10
Thread-0 11
Thread-0 12
Thread-0 13
Thread-0 14
Thread-0 15
Thread-0 16
Thread-0 17
Thread-0 18
Thread-0 19
子线程的返回值: 20
2.4 创建线程的三种方式对比
通过继承Thread类或实现Runnable、Callable接口都可以实现多线程,不过实现Runnable接口与实现Callable接口的方式基本相同,只是Callable接口定义的方法有返回值,可以声明抛出异常而已。因为可以将实现Callable与Runnable接口归为一种方式。这种方式与集成Thread方式之间的主要差别如下:
采用实现Runnable,Callable接口创建多线程的优缺点:
- 线程类只是实现了Runnable接口或Callable接口,还可以继承其他类;
- 在这种方式下,多个线程可以共享一个target对象,所以非常适合多个相同线程来处理同一份资源的情况,从而可以将CPU,代码和数据分开,形成清晰的模型,较好地体现了面向对象的思想。
- 劣势是,编程稍微复杂,如果需要访问当前线程,则必须使用Thread.currentThread()方法。
采用继承Thread类的方式创建多线程的优缺点:
- 劣势是,因为线程类继承了Thread类,所以不能再继承其他父类。
- 优势是编写简单,如果需要访问当前线程,则无须使用Thread.currentThread()方法,直接使用this即可获得当前线程。
整体而言,推荐使用实现Runnable,Callable接口的方式来创建多线程。
3.线程调度与线程控制
程序中的多个线程是并发执行的,某个线程若想被执行必须得到CPU的使用权。Java虚拟机会按照特定的机制为程序中的每个线程分配CPU的使用权,这种机制被称作线程的调度。
在计算机中,线程调
度有两种模型,分别是分时调度模型和抢占式调度模型。所谓的分时调度模型就是让所有的线程轮流获得CPU的使用权,并且平均分配每个线程占用的CPU的时间片。抢占式调度模型是指让可运行池中优先级高的线程优先占用CPU,而对于优先级相同的线程,随机选择一个线程使其占用CPU,当它失去了CPU的使用权后,再随机选择其他线程获取CPU的使用权。Java虚拟机默认采用抢占式调度模型,通常情况下程序员不需要去关心它,但在某些特定的需求下需要改变这种模式,由程序自己来控制CPU的调度。
3.1 线程调度
Java线程有优先级,优先级高的线程会获得较多的运行机会。Java线程的优先级用整数表示,取值范围是1~10,Thread类有以下三个静态常量:
| Thread类的静态变量 | 功能描述 |
|---|---|
| static int MAX_PRIORITY | 表示线程的最高优先级,值为10 |
| static int MIN_PRIORITY | 表示线程的最低优先级,值为1 |
| static int NORM_PRIORITY | 表示线程的普通优先级,值为5 |
Thread类的setPriority()和getPriority()方法分别用来设置和获取线程的优先级。每个线程都有默认的优先级,主线程的默认优先级为Thread.NORM_PRIORITY。并且线程的优先级有继承关系,比如A线程创建了B线程,那么B将和A具有相同的优先级。JVM提供了10个线程优先级,但与常见的操作系统都不能很好的映射。如果希望程序能移植到各个操作系统中,应该仅仅使用Thread类有以下三个静态常量作为优先级,这样能保证同样的优先级采用了同样的调度方式。
3.2 线程控制
- 线程睡眠:Thread.sleep(long mills)方法,让当前线程暂停,进入休眠等待状态。mills参数设定休眠的时间,以毫秒为单位。当睡眠结束后,就转为就绪状态。sleep()方法平台移植性较好。
- 线程加入:join()方法,等待其他线程终止。在当前线程中调用另一个线程的join()方法,则当前线程则被阻塞,直到另一个线程运行结束,当前线程再由阻塞状态转为就绪状态。
- 线程等待:Object类中的wait()方法,导致当前线程等待,直到其他线程调用此对象的notify()方法或者notifyAll()唤醒方法。这两个唤醒方法也是Object类中的方法,行为等价于调用wait(0)一样。
- 线程让步: Thread.yield()方法,暂停当前正在执行的线程对象,把执行机会让给相同或者优先级更高的线程。yield()应该做的是让当前运行线程回到可运行状态,以允许具有相同优先级的其他线程获得运行机会。因此,使用yield()的目的是让相同优先级的线程之间能适当的轮转执行。但是,实际中无法保证yield()达到让步目的,因为让步的线程还有可能被线程调度程序再次选中。
- 设为守护线程:setDaemon(boolean on),将该线程标记为守护线程(后台线程)或用户线程。当正在运行的线程都是守护线程时,Java 虚拟机退出。该方法必须在启动线程前调用。
- 线程中断:interrupt(),中断线程。 把线程的状态终止,并抛出一个InterruptedException。
- 线程唤醒: Object类中的notify()方法,唤醒在此对象监视器上等待的单个线程。如果所有线程都在此对象上等待,则会选择唤醒其中一个线程。选择是任意性的,并在对实现做出决定时发生。线程通过调用其中一个 wait 方法,在对象的监视器上等待。 直到当前的线程放弃此对象上的锁定,才能继续执行被唤醒的线程。被唤醒的线程将以常规方式与在该对象上主动同步的其他所有线程进行竞争;例如,唤醒的线程在作为锁定此对象的下一个线程方面没有可靠的特权或劣势。类似的方法还有一个notifyAll(),唤醒在此对象监视器上等待的所有线程。
Attention:Thread中suspend()和resume()两个方法在JDK1.5中已经废除,不再介绍。因为有死锁倾向。
1. sleep()和yield()方法的区别
sleep()使得当前线程进入停滞状态,所以执行sleep()的线程在指定时间内肯定不会被执行;yield()只是使得当前线程重新回到可执行状态,所以执行yield()的线程有可能在进入到可执行状态之后马上又被执行。
sleep 方法允许较低优先级的线程获得运行机会,但 yield() 方法执行时,当前线程仍处在可运行状态,所以,不可能让出较低优先级的线程些时获得 CPU 占有权。在一个运行系统中,如果较高优先级的线程没有调用 sleep 方法,又没有受到 I\O 阻塞,那么,较低优先级线程只能等待所有较高优先级的线程运行结束,才有机会运行。
2.wait()方法使用
Obj.wait()与Obj.notify()必须要与synchronized(Obj)一起使用,也就是wait,与notify是针对已经获取了Obj锁进行操作,从语法角度来说就是Obj.wait(),Obj.notify必须在synchronized(Obj){…}语句块内。从功能上来说wait就是说线程在获取对象锁后,主动释放对象锁,同时本线程休眠。直到有其它线程调用对象的notify()唤醒该线程,才能继续获取对象锁,并继续执行。相应的notify()就是对对象锁的唤醒操作。但有一点需要注意的是notify()调用后,并不是马上就释放对象锁的,而是在相应的synchronized(){}语句块执行结束,自动释放锁后,JVM会在wait()对象锁的线程中随机选取一线程,赋予其对象锁,唤醒线程,继续执行。这样就提供了在线程间同步、唤醒的操作。Thread.sleep()与Object.wait()二者都可以暂停当前线程,释放CPU控制权,主要的区别在于Object.wait()在释放CPU同时,释放了对象锁的控制。
下面给出一个经典的案例,三线程打印ABC,题目要求如下:建立三个线程,A线程打印10次A,B线程打印10次B,C线程打印10次C,要求线程同时运行,交替打印ABC。这个问题用Object的wait(),notify()就可以很方便的解决。代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
public class ABC_PrintThread implements Runnable {
private String name;
private Object prev;
private Object self;
public static int index = 0;
public ABC_PrintThread(String name, Object prev, Object self) {
this.name = name;
this.prev = prev;
this.self = self;
}
@Override
public void run() {
int count = 10;
while (count > 0){
synchronized (prev){
synchronized (self){
System.out.print(name);
count--;
index++;
if(index % 3 == 0){
System.out.println();
}
self.notify();
}
try{
prev.wait();
}catch (InterruptedException e){
e.printStackTrace();
}
}
}
}
public static void main(String[] args) throws Exception {
Object a = new Object();
Object b = new Object();
Object c = new Object();
ABC_PrintThread pa = new ABC_PrintThread("A",c,a);
ABC_PrintThread pb = new ABC_PrintThread("B",a,b);
ABC_PrintThread pc = new ABC_PrintThread("C",b,c);
new Thread(pa).start();
Thread.sleep(100);
new Thread(pb).start();
Thread.sleep(100);
new Thread(pc).start();
Thread.sleep(100);
}
}
输出结果:
1
2
3
4
5
6
7
8
9
10
ABC
ABC
ABC
ABC
ABC
ABC
ABC
ABC
ABC
ABC
我们来具体分析一下整体思路:从大的方向上来讲,该问题为三线程间的同步唤醒操作,主要的目的就是ThreadA->ThreadB->ThreadC->ThreadA循环执行三个线程。为了控制线程执行的顺序,那么就必须要确定唤醒、等待的顺序,所以每一个线程必须同时持有两个对象锁,才能继续执行。一个对象锁是prev,就是前一个线程所持有的对象锁。还有一个就是自身对象锁。主要的思想就是,为了控制执行的顺序,某个必须要先持有prev锁(也就是前一个线程要释放自身对象锁),再去申请自身对象锁,两者兼备时打印,之后首先调用self.notify()释放自身对象锁,唤醒下一个等待线程,再调用prev.wait()释放prev对象锁,终止当前线程,等待循环结束后再次被唤醒。运行上述代码,可以发现三个线程循环打印ABC,共10次。程序运行的主要过程就是A线程最先运行,持有C,A对象锁,后释放A,C锁,唤醒B。线程B等待A锁,再申请B锁,后打印B,再释放B,A锁,唤醒C,线程C等待B锁,再申请C锁,后打印C,再释放C,B锁,唤醒A。看起来似乎没什么问题,但如果你仔细想一下,就会发现有问题,就是初始条件,三个线程按照A,B,C的顺序来启动,按照前面的思考,A唤醒B,B唤醒C,C再唤醒A。但是这种假设依赖于JVM中线程调度、执行的顺序。
3.wait()和sleep()区别
共同点:
- 他们都是在多线程的环境下,都可以在程序的调用处阻塞指定的毫秒数,并返回。
- wait()和sleep()都可以通过interrupt()方法 打断线程的暂停状态 ,从而使线程立刻抛出InterruptedException。如果线程A希望立即结束线程B,则可以对线程B对应的Thread实例调用interrupt方法。如果此刻线程B正在wait/sleep /join,则线程B会立刻抛出InterruptedException,在catch() {} 中直接return即可安全地结束线程。(InterruptedException是线程自己从内部抛出的,并不是interrupt()方法抛出的。对某一线程调用 interrupt()时,如果该线程正在执行普通的代码,那么该线程根本就不会抛出InterruptedException。但是,一旦该线程进入到 wait()/sleep()/join()后,就会立刻抛出InterruptedException)
- Thread.sleep和Object.wait都会暂停当前的线程,对于CPU资源来说,不管是哪一种方式暂停的线程,都表示它暂时不再需要CPU的执行时间。OS会将执行时间分配给其他的线程。区别的是调用wait后,需要别的线程执行notify/notifyAll才能重新获得CPU执行时间。
不同点:
- sleep()是Thread类的方法,而wait()是Object类的方法;
- sleep()方法没有释放锁,而wait()方法释放了锁,使得其他线程可以使用同步控制块或方法。
- wait,notify和notifyAll只能在同步控制方法或者同步控制块里面使用,而sleep可以在任何地方使用。
4.线程同步
为什么需要线程同步?
当使用多个线程同时访问一个变量或对象时,如果这些线程中既有读又有写操作时,就会导致变量值或对象的状态出现混乱,从而导致程序异常。不妨来看下面的代码例子:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
public class RunnableThread implements Runnable {
private int ticket = 30;
public RunnableThread(){}
@Override
public void run() {
Thread thd = Thread.currentThread();
while (ticket > 0){
if(thd.getName().equals("Thread-0")){
try {
Thread.sleep(10);
}catch (InterruptedException e){
e.printStackTrace();
}
}
System.out.println(thd.getName() + " leftTcnt: " + ticket--);
}
}
public static void threadRunnable(){
RunnableThread rt = new RunnableThread();
new Thread(rt).start();
new Thread(rt).start();
new Thread(rt).start();
}
public static void main(String[] args) {
threadRunnable();
}
}
运行结果:本来是100张票,但是结果是101次.这是由于程序中当线程名字是Thread-0时,线程休眠10ms,这时线程是阻塞的,且并没有将ticket减1;这时其他线程正常运行,就会导致线程同步问题.还有个经典的案例就是银行取钱。
Java提供了两种锁机制来控制多个线程对共享资源的互斥访问,第一个是JVM实现的synchronized,而另一个是JDK实现的Lock。
4.1 synchronized
synchronized关键字的详细描述见另外一篇文章。
1.同步一个代码块
1
2
3
4
5
public void func() {
synchronized (this) {
// ...
}
}
它只作用于同一个对象,如果调用两个对象上的同步代码块,就不会进行同步。
对于以下代码,使用 ExecutorService 执行了两个线程,由于调用的是同一个对象的同步代码块,因此这两个线程会进行同步,当一个线程进入同步语句块时,另一个线程就必须等待。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class SynchronizedExample {
public void func1() {
synchronized (this) {
for (int i = 0; i < 10; i++) {
System.out.print(i + " ");
}
}
}
public static void main(String[] args) {
SynchronizedExample e1 = new SynchronizedExample();
ExecutorService executorService = Executors.newCachedThreadPool();
executorService.execute(() -> e1.func1());
executorService.execute(() -> e1.func1());
}
}
1
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9
而对于下面的代码,两个线程调用了不同对象的同步代码块,因此这两个线程就不需要同步。从输出结果可以看出,两个线程交叉执行。
1
2
3
4
5
6
7
public static void main(String[] args) {
SynchronizedExample e1 = new SynchronizedExample();
SynchronizedExample e2 = new SynchronizedExample();
ExecutorService executorService = Executors.newCachedThreadPool();
executorService.execute(() -> e1.func1());
executorService.execute(() -> e2.func1());
}
1
0 0 1 1 2 2 3 3 4 4 5 5 6 6 7 7 8 8 9 9
2.同步一个方法
1
2
3
public synchronized void func () {
// ...
}
它和同步代码块一样,作用于同一个对象。
3.同步一个类
1
2
3
4
5
public void func() {
synchronized (SynchronizedExample.class) {
// ...
}
}
作用于整个类,也就是说两个线程调用同一个类的不同对象上的这种同步语句,也会进行同步。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class SynchronizedExample {
public void func2() {
synchronized (SynchronizedExample.class) {
for (int i = 0; i < 10; i++) {
System.out.print(i + " ");
}
}
}
public static void main(String[] args) {
SynchronizedExample e1 = new SynchronizedExample();
SynchronizedExample e2 = new SynchronizedExample();
ExecutorService executorService = Executors.newCachedThreadPool();
executorService.execute(() -> e1.func2());
executorService.execute(() -> e2.func2());
}
}
1
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9
4.同步一个静态方法
1
2
3
public synchronized static void fun() {
// ...
}
作用于整个类。
4.2 Lock
从Java 5开始,Java提供了一种功能更强大的线程同步机制——通过显示定义同步锁对象来实现同步,在这种机制下,同步锁由Lock对象充当。Lock提供了比synchronized方法和synchronized代码块更广泛的锁定操作,Lock允许实现更灵活的结构,可以具有差别很大的属性,并且支持多个相关的Condition对象。(具体参考另外一篇文章)
在实现线程安全的控制中,比较常用的是ReentrantLock(可重入锁).使用该Lock对象可以显示地加锁、释放锁。重入锁是一种递归无阻塞的同步机制:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class LockExample {
private Lock lock = new ReentrantLock();
public void func() {
lock.lock();
try {
for (int i = 0; i < 10; i++) {
System.out.print(i + " ");
}
} finally {
lock.unlock(); // 确保释放锁,从而避免发生死锁。
}
}
public static void main(String[] args) {
LockExample lockExample = new LockExample();
ExecutorService executorService = Executors.newCachedThreadPool();
executorService.execute(() -> lockExample.func());
executorService.execute(() -> lockExample.func());
}
}
1
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9
ReentrantLock 是 java.util.concurrent(J.U.C)包中的锁,相比于 synchronized,它多了以下高级功能:
- 等待可中断:当持有锁的线程长期不释放锁的时候,正在等待的线程可以选择放弃等待,改为处理其他事情。
- 可实现公平锁:公平锁是指多个线程在等待同一个锁时,必须按照申请锁的时间顺序来依次获得锁。synchronized 中的锁是非公平的,ReentrantLock 默认情况下也是非公平的,但可以通过带布尔值的构造函数要求使用公平锁。
- 锁绑定多个条件:一个 ReentrantLock 对象可以同时绑定多个 Condition 对象。
4.3 synchronized和ReentrantLock的比较
- 锁的实现:synchronized 是 JVM 实现的,而 ReentrantLock 是 JDK 实现的。
- 性能:新版本 Java 对 synchronized 进行了很多优化,例如自旋锁等。目前来看它和 ReentrantLock 的性能基本持平了,因此性能因素不再是选择 ReentrantLock 的理由。synchronized 有更大的性能优化空间,应该优先考虑 synchronized。
- 功能:ReentrantLock 多了一些高级功能。
- 使用选择:除非需要使用 ReentrantLock 的高级功能,否则优先使用 synchronized。这是因为 synchronized 是 JVM 实现的一种锁机制,JVM 原生地支持它,而 ReentrantLock 不是所有的 JDK 版本都支持。并且使用 synchronized 不用担心没有释放锁而导致死锁问题,因为 JVM 会确保锁的释放。
5. 线程通信
线程通信的目标是使得线程间能够相互发送信号。另一方面,线程通信使线程能够等待其他线程的信号。
例如,线程B可以等待线程A的一个信号,这个信号会通知线程B数据已经准备好了,下面将主要讲解Java中进行线程通信的几个主题。
5.1 通过共享对象通信
线程间发送信号的一个简单方式是在共享对象的变量里设置信号值。例如,线程A在一个同步块里设置boolean型成员变量hasDataToProcess为true,线程B也在同步块里读取hasDataToProcess这个成员变量。下面这个简单的例子使用了一个持有信号的对象,并提供了set和check方法。
1
2
3
4
5
6
7
8
9
10
11
12
13
public class MySignal{
protected boolean hasDataToProcess = false;
public synchronized boolean hasDataToProcess(){
return this.hasDataToProcess;
}
public synchronized void setHasDataToProcess(boolean hasData){
this.hasDataToProcess = hasData;
}
}
线程A和B必须获得一个指向MySignal共享实例的引用,以便进行通信。如果他们持有的引用指向不同的MySignal实例,那么彼此将不能检测到对方的信号。需要处理的数据可以存放在一个共享缓存区里,它和MySignal实例是分开存放的。
5.2 忙等待(Busy wait)
准备处理数据的线程B正在等待数据变为可用。换句话说,它在等待线程A的一个信号,这个信号使hasDataToProcess()返回true。线程B运行在一个循环里,以等待这个信号:
1
2
3
4
5
6
7
protected MySignal sharedSignal = ...
...
while(!sharedSignal.hasDataToProcess()){
//do nothing... busy waiting
}
5.3 wait,notify()和notifyAll()
忙等待没有对运行等待线程的CPU进行有效的利用,除非平均等待时间非常短。否则,让等待线程进入睡眠或者非运行状态更为明智,直到它接收到它通知的信号。
在Java中有一个内建的等待机制来允许线程在等待信号的时候变成非运行状态。java.lang.Object类定义了三个方法,wait()、notify()和notifyAll()来实现这个等待机制。一个线程一旦调用了任意对象的wait()方法,就会变为非运行状态,直到另一个线程调用了同一个对象的notify()方法。为了调用wait()或者notify(),线程必须先获得那个对象的锁。也就是说,线程必须在同步块里调用wait()或者notify()。
下面是MySignal的修改版本,使用了wait()和notify()的MyWaitNotify:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class MonitorObject{
}
public class MyWaitNotify{
MonitorObject myMonitorObject = new MonitorObject();
public void doWait(){
synchronized(myMonitorObject){
try{
myMonitorObject.wait();
} catch(InterruptedException e){...}
}
}
public void doNotify(){
synchronized(myMonitorObject){
myMonitorObject.notify();
}
}
}
等待线程调用doWait(),而唤醒线程将调用doNotify()。当一个线程调用一个对象的notify()方法,正在等待该对象的所有线程中将有一个线程被唤醒并允许执行(随机唤醒,不可指定唤醒哪一个线程)。同时也提供了一个notifyAll()方法来唤醒正在等待一个给定对象的所有线程。
一旦线程调用了wait()方法,它就释放了所持有的监视器对象上的锁。这将允许其他线程也可以调用wait()或者notify()。一旦一个线程被唤醒,不能立刻就退出wait()的方法调用,直到调用notify()的线程退出了它自己的同步块。换句话说:被唤醒的线程必须重新获得监视器对象的锁,才可以退出wait()的方法调用,因为wait方法调用运行在同步块里面。如果多个线程被notifyAll()唤醒,那么在同一个时刻将只有一个线程可以退出wait()方法,因为每个线程在退出wait()前必须获得监视器的锁。
5.4 丢失的信号(Missed Signals)
notify()和notifyAll()方法不会保存调用它们的方法,因为当着两个方法被调用时,有可能没有线程处于等待状态。通知信号过后便丢弃了。因此,如果一个线程先于被通知线程调用wait()前调用了wait(),等待的线程将会错过这个信号。这可能是也可能不是个问题。不过,在某些情况下,这可能使等待线程永远在等待,不再醒来,因为线程错过了唤醒信号。
为了避免丢失信号,必须把它们保存在信号类里。在MyWaitNotify的例子中,通知信号应被存储在MyWaitNotify实例的一个成员变量里。以下是MyWaitNotify的修改版本:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class MyWaitNotify2{
MonitorObject myMonitorObject = new MonitorObject();
boolean wasSignalled = false;
public void doWait(){
synchronized(myMonitorObject){
if(!wasSignalled){
try{
myMonitorObject.wait();
} catch(InterruptedException e){...}
}
//clear signal and continue running.
wasSignalled = false;
}
}
public void doNotify(){
synchronized(myMonitorObject){
wasSignalled = true;
myMonitorObject.notify();
}
}
}
留意doNotify()方法在调用notify()前把wasSignalled变量设为true。同时,留意doWait()方法在调用wait()前会检查wasSignalled变量。事实上,如果没有信号在前一次doWait()调用和这次doWait()调用之间的时间段里被接收到,它将只调用wait()。
5.5 假唤醒
由于莫名奇妙的原因,线程有可能在没有调用过notify()和notifyAll()的情况下醒过来。这就是所谓的假唤醒(spurious wakeups)。无端端地醒过来了。
如果在MyWaitNotify2的doWait()方法里发生了假唤醒,等待线程即使没有收到正确的信号,也能够执行后续的操作。这就可能会导致你的应用程序出现严重问题。
为了防止假唤醒,保存信号的成员变量将在一个while循环里接收检查,而不是在if表达式里。这样的一个while循环叫做自旋锁(这种做法要慎重,目前的JVM实现自旋会消耗CPU,如果长时间不调用doNotify方法,doWait方法会一直自旋,CPU会消耗太大)。被唤醒的线程会自旋直到自旋锁(while循环)里的条件变为false。以下MyWaitNotify2的修改版本展示了这点:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class MyWaitNotify3{
MonitorObject myMonitorObject = new MonitorObject();
boolean wasSignalled = false;
public void doWait(){
synchronized(myMonitorObject){
while(!wasSignalled){
try{
myMonitorObject.wait();
} catch(InterruptedException e){...}
}
//clear signal and continue running.
wasSignalled = false;
}
}
public void doNotify(){
synchronized(myMonitorObject){
wasSignalled = true;
myMonitorObject.notify();
}
}
}
留意wait()方法时在while循环里,而不在if表达式里。如果等待线程没有收到信号就唤醒,while循环会再执行一次,促使醒来的线程回到等待状态。
5.6 多个线程等待相同信号
如果你有多个线程在等待,被notifyAll()唤醒,但只有一个被允许继续执行,使用while循环也是个好方法。每次只有一个线程可以获得监视器对象锁,意味着只有一个线程可以退出wait()调用并清除wasSignalled标志(设为false)。一旦这个线程退出doWait()的同步块,其他线程退出wait()调用,并在while循环里检查wasSignalled变量值。但是,这个标志已经被第一个唤醒的线程清除了,所以其余醒来的线程将回到等待状态,直到下次信号到来。
5.7 不要在字符串常量或全局对象中调用wait()
给出下面这个例子:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class MyWaitNotify{
String myMonitorObject = "";
boolean wasSignalled = false;
public void doWait(){
synchronized(myMonitorObject){
while(!wasSignalled){
try{
myMonitorObject.wait();
} catch(InterruptedException e){...}
}
//clear signal and continue running.
wasSignalled = false;
}
}
public void doNotify(){
synchronized(myMonitorObject){
wasSignalled = true;
myMonitorObject.notify();
}
}
}
在空字符串作为锁的的同步块(或者其他常量字符串)里调用wait()和notify()产生的问题是,JVM/编译器内部会把常量字符串转换成同一个对象,这意味着,即使你有两个不同的MyWaitNotify实例,它们都引用了相同的空字符串实例。同时也意味着存在这样的风险:在第一个MyWaitNotify实例上调用doWait()的线程会被在第二个MyWaitNotify实例上调用doNotify()的线程唤醒。

起初这可能不像个大问题。毕竟,如果doNotify()在第二个MyWaitNotify实例上调用,真正发生的事不外乎线程A和B被错误的唤醒了。这个被唤醒的线程(A或者B)将在while循环里检查信号值,然后回到等待状态,因为doNotify()并没有在第一个MyWaitNotify实例上调用,而这个正是它要等待的实例。这种情况相当于引发了一次假唤醒。线程A或者B在信号值没有更新的情况下唤醒,但是代码处理了这种情况,所以线程回到了等待状态。记住,即使4个线程在相同的共享字符串实例上调用wait()和notify(),doWait()和doNotify()里的信号还会被2个MyWaitNotify实例分别保存。在MyWaitNotify1上的一次doNotify()调用可能唤醒MyWaitNotify2的线程,但是信号值只会保存在MyWaitNotify1里。
问题在于,由于doNotify()仅调用了notify()而不是notifyAll(),即使有4个线程在相同的字符串(空字符串)实例上等待,只能有一个线程被唤醒。所以,如果线程A或B被发给C或D的信号唤醒,它会检查自己的信号值,看看有没有信号被接收到,然后回到等待状态。而C和D都没被唤醒来检查它们实际上接收到的信号值,这样信号便丢失了。这种情况相当于前面所说的丢失信号的问题。C和D被发送过信号,只是都不能对信号作出回应。
如果doNotify()方法调用notifyAll(),而非notify(),所有等待线程都会被唤醒并依次检查信号值。线程A和B将回到等待状态,但是C或D只有一个线程注意到信号,并退出doWait()方法调用。C或D中的另一个将回到等待状态,因为获得信号的线程在退出doWait()的过程中清除了信号值(置为false)。
看过上面这段后,你可能会设法使用notifyAll()来代替notify(),但是这在性能上是个坏主意。在只有一个线程能对信号进行响应的情况下,没有理由每次都去唤醒所有线程。
所以:在wait()/notify()机制中,不要使用全局对象,字符串常量等。应该使用对应唯一的对象。