在 Java 多线程编程中,sleep(), interrupt(), wait(), notify() 等方法是非常基本也很常用的方法。这些方法会改变运行中的 Java 线程的状态,正确地认识这些方法是掌握 Java 并发编程的基本要求。
Java 线程的状态
先来谈一谈 Java 中线程的状态。在 Java 中,线程的状态和底层操作系统中线程的状态并不是一一对应的关系,我们所能见到的是 JVM 虚拟机层面暴露的状态。
Java 中线程的状态在 Thread 的内部枚举类 Thread.State
中定义,有 NEW
, RUNNABLE
, BLOCKED
, WAITING
, TIMED_WAITING
, TERMINATED
这六类状态。
在 Java 的官方文档中写道:“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
状态的线程在 JVM 虚拟机正在 Java 虚拟机中执行,但它可能正在等待来自于操作系统的其它资源,比如处理器。实际上,如果一个线程在等待阻塞I/O的操作时,它的状态也是 RUNNABLE
的。
如果一个线程在获取对象锁的过程中阻塞了(synchronized关键字),它就处于 BLOCKED
状态。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 Object.wait.
而如果是由于调用了下面这三类方法,则线程会处于 WAITING
状态,需要等待其他的线程将其唤醒:
Object.wait
with no timeoutThread.join
with no timeoutLockSupport.park
如果是通过 Lock.lock()
方法等待获取锁时,也会处于 WAITING
状态。因为 Lock
接口的实现基于 AQS 实现的,而 AQS 中的阻塞操作都是基于 LockSupport
工具类实现的。
TIMED_WAITING
状态和 WAITING
状态类似,只不过等待的是超时事件的发生,下面几种方法会使得线程进入该状态:
Thread.sleep
Object.wait
with timeoutThread.join
with timeoutLockSupport.parkNanos
LockSupport.parkUntil
同样,带有超时的 Lock.tryLock(long time, TimeUnit unit)
方法在等待获取锁时也会进入该状态。
通常,在操作系统这一层面,线程存在五类状态,状态的转换关系可以参考下面的这张图。
可以看到,JVM 中所说的线程状态和 OS 层面的线程状态是不太一样的。JVM 中 RUNNABLE
其实是包含了上图中的 RUNNING
, READY
, 和部分 WAITING
状态的;而 JVM 中 WAITING
, TIMED_WAITING
和 BLOCKED
其实又是对上图中 WAITING
剩余情形的一个更细致的划分。
sleep
vs wait
sleep(long)
和 wait()
方法都能让线程暂停执行,并让出当前的处理器资源。但是,这两个方法存在一些本质的区别。
sleep(long)
方法是在 Thread
类中定义的静态方法,会使得线程睡眠(即暂时停止运行)一段指定的时间,进入 TIMED_WAITING
状态;当超时后重新进入 RUNNABLE
状态。sleep()
方法会保留当前线程的运行状态,线程所持有的锁资源并不会释放。
wait()
方法是在 Object
上定义的方法,任何一个类都从 Object
类中继承了该方法。调用该方法(obj.wait()
)的前提是当前线程获取了该对象(obj
)的锁。调用该方法后,线程会进入 WAITING
状态,同时会释放持有的对象上的锁,JVM 会将该线程置于对象的等待队列中。wait()
方法需要通过 obj.notify()
或 obj.notifyAll()
来进行唤醒,notify()
和notifyAll()
会将对象的等待队列中的一个或全部线程移入对象的同步队列中来竞争对象的锁,当获取锁之后便从 wait()
方法中返回了。简单地说, wait()
方法会释放线程持有的锁,并等待 notify()
或 notifyAll()
唤醒,从 wait()
方法返回表明线程又重新获取了对象锁。
wait(long)
是 wait()
的一个重载版本,效果基本一致,只是 wait(long)
进入 TIMED_WAITING
状态,超时也可以被唤醒。
interrupt
很多人看到 interrupt()
方法,认为“中断”线程不就是让线程停止嘛。实际上, interrupt()
方法实现的根本就不是这个效果, interrupt()
方法更像是发出一个信号,这个信号会改变线程的一个标识位属性(中断标识),对于这个信号如何进行相应则是无法确定的(可以有不同的处理逻辑)。很多时候调用 interrupt()
方法非但不是为了停止线程,反而是为了让线程继续运行下去。
在 Java 的文档中对 interrupt()
的效果列了四种情形:
If this thread is blocked in an invocation of the wait(), wait(long), or wait(long, int) methods of the Object class, or of the join(), join(long), join(long, int), sleep(long), or sleep(long, int), methods of this class, then its interrupt status will be cleared and it will receive an InterruptedException.
If this thread is blocked in an I/O operation upon an InterruptibleChannel then the channel will be closed, the thread’s interrupt status will be set, and the thread will receive a ClosedByInterruptException.
If this thread is blocked in a Selector then the thread’s interrupt status will be set and it will return immediately from the selection operation, possibly with a non-zero value, just as if the selector’s wakeup method were invoked.
If none of the previous conditions hold then this thread’s interrupt status will be set.
前三种情形其实是描述了如果线程处于等待状态或是阻塞在某一种资源上,那么 interrupt()
方法会使得线程跳出这种状态继续执行下去。第四种情形则描述了如果线程正在正常执行,那么 interrupt()
的效果则是设置了线程的中断状态,至于怎么处理这种状态,可以选择忽略也可以按需处理。
可以通过 isInterrupted()
方法来测试当前线程的中断标识的状态;静态方法 Thread.interrupted()
可以判断当前线程是否处于中断状态,同时也会清除当前的中断状态。通常使用 Thread.interrupted()
来复位中断标识。在第一种情形的表示中我们可以看到,在抛出 InterruptedException
前会清除中断标识,因而在异常处理中调用 isInterrupted()
会返回 false。
如果线程调用 sleep(long)
方法睡眠了非常长的一段时间,现在想要将它唤醒,就可以调用 interrupt()
方法。注意是在 wait()
, sleep()
, join()
方法声明中的异常,可见不是调用 interrupt()
抛出异常,而是在 wait()
, sleep()
, join()
处于等待的过程中,调用 interrupt()
方法会使其从等待状态中返回,并收到 InterruptedException
异常,进而将控制逻辑交给异常处理语句。
在 wait()
方法中等待的线程被中断时,和使用 notify()
唤醒一样,必须要重新获得对象的锁才能从方法中返回,而不是立即就能返回并进入异常处理。下面这个例子简单地验证一下:
1
|
public class InterruptDemo {
|
输出的结果:
1
|
thread 1 get lock @1471189560880
|
可以看到,只有在线程2释放了锁之后(只有线程2释放了线程1才能重新获取),才能从wait()
方法中返回。
yield
yield()
方法是 Thread
类的静态方法,也用于出让当前线程占用的CPU资源。和 sleep(long)
方法不同的是, sleep(long)
会使得线程进入 WAITING
状态并且至少会等待超时时间到达后才会再次执行;而 yield()
方法则是从 RUNNING
进入 READY
状态(这里指的是操作系统层面,在 JVM 暴露出来的都是 RUNNABLE
状态),因而极有可能马上又被调度选中继续运行。
A hint to the scheduler that the current thread is willing to yield its current use of a processor. The scheduler is free to ignore this hint.
从文档中的表述来看,yield()
方法相比于 sleep(long)
方法更依赖与系统的调度。该方法并不经常用到。
-EOF-