wait() 和 notify(),notifyAll()
这三个方法用于协调多个线程对共享数据的存取, 所以必须在 Synchronized 语句块内使用这三个方法. 前面说过 Synchronized 这个关键字用于保护共享数据, 阻止其他线程对共享数据的存取. 但是这样程序的流程就很不灵活了, 如何才能在当前线程还没退出 Synchronized 数据块时让其他线程也有机会访问共享数据呢? 此时就用这三个方法来灵活控制.
wait() 方法使当前线程被阻塞挂起暂停执行并释放对象锁标志, 让其他线程可以进入 Synchronized 数据块, 当前线程被放入对象等待池中. 当调用共享对象的 notify() 或者 notifyAll() 方法才会返回, 此时返回的线程会加入到锁标志等待池中, 只有锁标志等待池中的线程能够获取锁标志, 如果锁标志等待池中没有线程, 则 notify() 不起作用.
notifyAll() 则从对象等待池中移走所有等待那个对象的线程并放到锁标志等待池中.
需要注意的是, 当线程调用共享对象的 wait() 方法时, 当前线程只会释放当前共享对象的锁, 当前线程持有的其他共享对象的监视器锁并不会释放.
线程中断
为啥需要中断呢? 下面简单的举例情况:
比如我们会启动多个线程做同一件事, 比如抢 12306 的火车票, 我们可能开启多个线程从多个渠道买火车票, 只要有一个渠道买到了, 我们会通知取消其他渠道. 这个时候需要关闭其他线程;
很多线程的运行模式是死循环, 比如在生产者 / 消费者模式中, 消费者主体就是一个死循环, 它不停的从队列中接受任务, 执行任务, 在停止程序时, 我们需要一种 "优雅" 的方法以关闭该线程;
在一些场景中, 比如从第三方服务器查询一个结果, 我们希望在限定的时间内得到结果, 如果得不到, 我们会希望取消该任务;
上面这几个例子线程已经在运行了, 并不好去干涉, 但是可以通过中断, 告诉这个线程, 你应该中断了. 比如上面的例子中的线程再收到中断后, 可以通过中断标志来结束线程的运行. 当然, 你也可以收到后, 不做任何处理, 这也是可以的.
在 Java 中, 停止一个线程的主要机制是中断, 中断并不是强迫终止一个线程, 它是一种协作机制, 是给线程传递一个取消信号, 但是由线程来决定如何以及何时退出.
需要注意的是: 在停止线程的时候, 不要调用 stop 方法, 该方法已经被废弃了, 并且会带来不可预测的影响.
线程对中断的反应
RUNNABLE: 线程在运行或具备运行条件只是在等待操作系统调度
WAITING/TIMED_WAITING: 线程在等待某个条件或超时
BLOCKED: 线程在等待锁, 试图进入同步块
NEW/TERMINATED: 线程还未启动或已结束
RUNNABLE 状态
如果线程在运行中, interrupt() 只是会设置线程的中断标志位, 没有任何其它作用. 线程应该在运行过程中合适的位置检查中断标志位, 比如说, 如果主体代码是一个循环, 可以在循环开始处进行检查, 如下所示:
- public class InterruptRunnableDemo extends Thread {
- @Override
- public void run() {
- while (!Thread.currentThread().isInterrupted()) {
- // ... 单次循环代码
- }
- System.out.println("done");
- }
- public static void main(String[] args) throws InterruptedException {
- Thread thread = new InterruptRunnableDemo();
- thread.start();
- Thread.sleep(1000);
- thread.interrupt();
- }
- }
- WAITING/TIMED_WAITING
线程执行如下方法会进入 WAITING 状态:
- public final void join() throws InterruptedException
- public final void wait() throws InterruptedException
执行如下方法会进入 TIMED_WAITING 状态:
- public final native void wait(long timeout) throws InterruptedException;
- public static native void sleep(long millis) throws InterruptedException;
- public final synchronized void join(long millis) throws InterruptedException
在这些状态时, 对线程对象调用 interrupt() 会使得该线程抛出 InterruptedException, 需要注意的是, 抛出异常后, 中断标志位会被清空(线程的中断标志位会由 true 重置为 false, 因为线程为了处理异常已经重新处于就绪状态), 而不是被设置. 比如说, 执行如下代码:
- Thread t = new Thread (){
- @Override
- public void run() {
- try {
- Thread.sleep(1000);
- } catch (InterruptedException e) {
- //exception 被捕获, 但是为输出为 false 因为标志位会被清空
- System.out.println(isInterrupted());
- }
- }
- };
- t.start();
- try {
- Thread.sleep(100);
- } catch (InterruptedException e) {
- }
- t.interrupt();// 置为 true
InterruptedException 是一个受检异常, 线程必须进行处理. 我们在异常处理中介绍过, 处理异常的基本思路是, 如果你知道怎么处理, 就进行处理, 如果不知道, 就应该向上传递, 通常情况下, 你不应该做的是, 捕获异常然后忽略.
捕获到 InterruptedException, 通常表示希望结束该线程, 线程大概有两种处理方式:
向上传递该异常, 这使得该方法也变成了一个可中断的方法, 需要调用者进行处理
有些情况, 不能向上传递异常, 比如 Thread 的 run 方法, 它的声明是固定的, 不能抛出任何受检异常, 这时, 应该捕获异常, 进行合适的清理操作, 清理后, 一般应该调用 Thread 的 interrupt 方法设置中断标志位, 使得其他代码有办法知道它发生了中断
第一种方式的示例代码如下:
- // 抛出中断异常, 由调用者捕获
- public void interruptibleMethod() throws InterruptedException{
- // ... 包含 wait, join 或 sleep 方法
- Thread.sleep(1000);
- }
第二种方式的示例代码如下:
- public class InterruptWaitingDemo extends Thread {
- @Override
- public void run() {
- while (!Thread.currentThread().isInterrupted()) {
- try {
- // 模拟任务代码
- Thread.sleep(2000);
- } catch (InterruptedException e) {
- // ... 清理操作
- System.out.println(isInterrupted());//false
- // 重设中断标志位为 true
- Thread.currentThread().interrupt();
- }
- }
- System.out.println(isInterrupted());//true
- }
- public static void main(String[] args) {
- InterruptWaitingDemo thread = new InterruptWaitingDemo();
- thread.start();
- try {
- Thread.sleep(100);
- } catch (InterruptedException e) {
- }
- thread.interrupt();
- }
- }
- BLOCKED
如果线程在等待锁, 对线程对象调用 interrupt()只是会设置线程的中断标志位, 线程依然会处于 BLOCKED 状态, 也就是说, interrupt()并不能使一个在等待锁的线程真正 "中断". 我们看段代码:
- public class InterruptWaitingDemo extends Thread {
- @Override
- public void run() {
- while (!Thread.currentThread().isInterrupted()) {
- try {
- // 模拟任务代码
- Thread.sleep(2000);
- } catch (InterruptedException e) {
- // ... 清理操作
- // 重设中断标志位
- Thread.currentThread().interrupt();
- }
- }
- System.out.println(isInterrupted());
- }
- public static void main(String[] args) {
- InterruptWaitingDemo thread = new InterruptWaitingDemo();
- thread.start();
- try {
- Thread.sleep(100);
- } catch (InterruptedException e) {
- }
- thread.interrupt();
- }
- }
BLOCKED 如果线程在等待锁, 对线程对象调用 interrupt() 只是会设置线程的中断标志位, 线程依然会处于 BLOCKED 状态, 也就是说, interrupt() 并不能使一个在等待锁的线程真正 "中断". 我们看段代码:
- public class InterruptSynchronizedDemo {
- private static Object lock = new Object();//monitor
- private static class A extends Thread {
- @Override
- public void run() {
- // 等待 lock 锁
- synchronized (lock) {
- // 等待标志位被置为 true
- while (!Thread.currentThread().isInterrupted()) {
- }
- }
- System.out.println("exit");
- }
- }
- public static void test() throws InterruptedException {
- synchronized (lock) {// 获取锁
- A a = new A();
- a.start();
- Thread.sleep(1000);
- //a 在等待 lock 锁, interrupt 无法中断
- a.interrupt();
- //a 线程加入当前线程, 等待执行完毕
- a.join();
- }
- }
- public static void main(String[] args) throws InterruptedException {
- test();
- }
- }
test 方法在持有锁 lock 的情况下启动线程 a, 而线程 a 也去尝试获得锁 lock, 所以会进入锁等待队列, 随后 test 调用线程 a 的 interrupt 方法并等待线程线程 a 结束, 线程 a 会结束吗? 不会, interrupt 方法只会设置线程的中断标志, 而并不会使它从锁等待队列中出来. 线程 a 会一直尝试获取锁, 但是主线程也在等待 a 结束才会释放锁, 所以相互之间互为等待, 不能结束.
我们稍微修改下代码, 去掉 test 方法中的最后一行 a.join(), 即变为:
- public static void test() throws InterruptedException {
- synchronized (lock) {
- A a = new A();
- a.start();
- Thread.sleep(1000);
- a.interrupt();
- }
- //lock 锁释放后 A 线程重队列中出来
- }
这时, 程序就会退出. 为什么呢? 因为主线程不再等待线程 a 结束, 释放锁 lock 后, 线程 a 会获得锁, 然后检测到发生了中断, 所以会退出.
在使用 synchronized 关键字获取锁的过程中不响应中断请求, 这是 synchronized 的局限性. 如果这对程序是一个问题, 应该使用显式锁, java 中的 Lock 接口, 它支持以响应中断的方式获取锁. 对于 Lock.lock(), 可以改用 Lock.lockInterruptibly(), 可被中断的加锁操作, 它可以抛出中断异常. 等同于等待时间无限长的 Lock.tryLock(long time, TimeUnit unit).
NEW/TERMINATE
如果线程尚未启动 (NEW), 或者已经结束 (TERMINATED), 则调用 interrupt() 对它没有任何效果, 中断标志位也不会被设置. 比如说, 以下代码的输出都是 false.
- public class InterruptNotAliveDemo {
- private static class A extends Thread {
- @Override
- public void run() {
- }
- }
- public static void test() throws InterruptedException {
- A a = new A();
- a.interrupt();
- System.out.println(a.isInterrupted());
- a.start();
- Thread.sleep(100);
- a.interrupt();
- System.out.println(a.isInterrupted());
- }
- public static void main(String[] args) throws InterruptedException {
- test();
- }
- }
IO 操作
如果线程在等待 IO 操作, 尤其是网络 IO, 则会有一些特殊的处理, 我们没有介绍过网络, 这里只是简单介绍下.
实现此 InterruptibleChannel 接口的通道是可中断的: 如果某个线程在可中断通道上因调用某个阻塞的 I/O 操作 (常见的操作一般有这些: serverSocketChannel. accept(),socketChannel.connect,socketChannel.open,socketChannel.read,socketChannel.write,fileChannel.read,fileChannel.write) 而进入阻塞状态, 而另一个线程又调用了该阻塞线程的 interrupt 方法, 这将导致该通道被关闭, 并且已阻塞线程接将会收到 ClosedByInterruptException, 并且设置已阻塞线程的中断状态. 另外, 如果已设置某个线程的中断状态并且它在通道上调用某个阻塞的 I/O 操作, 则该通道将关闭并且该线程立即接收到 ClosedByInterruptException; 并仍然设置其中断状态.
如果线程阻塞于 Selector 调用, 则线程的中断标志位会被设置, 同时, 阻塞的调用会立即返回.
我们重点介绍另一种情况, InputStream 的 read 调用, 该操作是不可中断的, 如果流中没有数据, read 会阻塞 (但线程状态依然是 RUNNABLE ), 且不响应 interrupt(), 与 synchronized 类似, 调用 interrupt() 只会设置线程的中断标志, 而不会真正 "中断" 它, 我们看段代码
- public class InterruptReadDemo {
- private static class A extends Thread {
- @Override
- public void run() {
- while(!Thread.currentThread().isInterrupted()){
- try {
- System.out.println(System.in.read())//wait input
- } catch (IOException e) {
- e.printStackTrace();
- }
- }
- System.out.println("exit");
- }
- }
- public static void main(String[] args) throws InterruptedException {
- A t = new A();
- t.start();
- Thread.sleep(100);
- t.interrupt();
- }
- }
线程 t 启动后调用 System.in.read() 从标准输入读入一个字符, 不要输入任何字符, 我们会看到, 调用 interrupt() 不会中断 read(), 线程会一直运行.
不过, 有一个办法可以中断 read() 调用, 那就是调用流的 close 方法, 我们将代码改为:
- public class InterruptReadDemo {
- private static class A extends Thread {
- @Override
- public void run() {
- while (!Thread.currentThread().isInterrupted()) {
- try {
- System.out.println(System.in.read());
- } catch (IOException e) {
- e.printStackTrace();
- }
- }
- System.out.println("exit");
- }
- public void cancel() {
- try {
- System.in.close();
- } catch (IOException e) {
- }
- interrupt();
- }
- }
- public static void main(String[] args) throws InterruptedException {
- A t = new A();
- t.start();
- Thread.sleep(100);
- t.cancel();
- }
- }
我们给线程定义了一个 cancel 方法, 在该方法中, 调用了流的 close 方法, 同时调用了 interrupt 方法, 这次, 程序会输出:
-1 exit
也就是说, 调用 close 方法后, read 方法会返回, 返回值为 - 1, 表示流结束.
如何正确地取消 / 关闭线程
1. 以上, 我们可以看出, interrupt 方法不一定会真正 "中断" 线程, 它只是一种协作机制, 如果 不明白线程在做什么, 不应该贸然的调用线程的 interrupt 方法, 以为这样就能取消线程.
2. 对于以线程提供服务的程序模块而言, 它应该封装取消 / 关闭操作, 提供单独的取消 / 关闭方法给调用者, 类似于 InterruptReadDemo 中演示的 cancel 方法, 外部调用者应该调用这些方法而不是直接调用 interrupt.
3. Java 并发库的一些代码就提供了单独的取消 / 关闭方法, 比如说, Future 接口提供了如下方法以取消任务: boolean cancel(boolean mayInterruptIfRunning);
4. 再比如, ExecutorService 提供了如下两个关闭方法:
- void shutdown();
- List<Runnable> shutdownNow();
5. Future 和 ExecutorService 的 API 文档对这些方法都进行了详细说明, 这是我们应该学习的方式.
参考文章:
线程的中断 (interrupt) 机制 https://zhuanlan.zhihu.com/p/27857336
来源: https://www.cnblogs.com/huansky/p/12397063.html