前言
本文主要是为了巩固一下基础知识,对多线程的锁,wait,notify,中断做一个比较全面的总结,方便以后学习参考!同时在写博客的时候,查阅了资料,也做到一个查漏补缺的作用吧!希望可以和大家一起查漏补缺!
同步
简介
- Java 提供了多种线程之间通信的机制,其中最基本的就是使用同步 (synchronization)
- 其使用监视器 (monitor) 来实现。java中的每个对象都关联了一个监视器,线程可以对其进行加锁和解锁操作。
- 在同一时间,只有一个线程可以拿到对象上的监视器锁。如果其他线程在锁被占用期间试图去获取锁,那么将会被阻塞直到成功获取到锁。同时,监视器锁可以重入,也就是说如果线程 t 拿到了锁,那么线程 t 可以在解锁之前重复获取锁,每次解锁操作会反转一次加锁产生的效果。
关于同步,大家可以了解一下Java并发编程中Synchronized这个关键字,如果有不了解的,可以先阅读下<深入了解Synchronized原理>,这篇文章说得比较详细,对于阅读下文有很大的帮助!
TIPS:对Class对象加锁和对实例对象加锁,它们之间不构成同步。对于一个类中的两个不同的static方法加锁之间是构成同步的!
等待集合和唤醒
-
我们知道,每个Java对象都与一个监视器关联,同时也关联了一个等待集合,也就是存放想要获取这个锁的线程数据结构
-
当一个对象被创建出来时,它的等待集合是空的,对于向等待集合中添加或者移除线程的操作都是原子的,以下几个操作可以操纵这个等待集合:Object.wait, Object.notify, Object.notifyAll。
概览
Thread
的sleep
,join
,interrupted
- 继承自Object的
wait
,notify
,notifyAll
- Java的中断(重要)
wait
说明
等待一般由这几个方法触发,wait()
,wait(long millisecs)
,wait(long millisecs, int nanosecs)
,我们着重讲下wait
。
如果调用 wait
方法时没有抛出InterruptedException
异常,则表示正常返回。
假设有线程t,对象object,object的对象头的设置了线程的ID为n,目前还没解锁,现在执行object.wait()
,会有以下几种情况发生
- 假如object的对象头没有设置线程t的ID,那么说明该线程并没有拥有该锁,如果用
wait
会抛出IllegalMonitorStateException
的异常。 - 如果线程t被中断,那么中断标志被设置为true,并且抛出
InterruptedException
的异常,并且会重新设置中断标志位false - 假如1和2都没有发生,那么说明没有抛出异常,之前就已经拿到了锁。那么说明调用
wait
方法正常,就正常挂起然后释放对象锁
分析
-
假如线程 t 在调用
wait
方法之后(我们假设是正常的情况),那么该线程t就会进入object
对象的waitSet
中,并且释放该对象对应的锁,下面这段代码表示正常的获取锁,解锁操作!public Object object = new Object(); void thread1() { synchronized (object) { // 1. 获取监视器锁 try { object.wait(); // 2. 解锁 } catch (InterruptedException e) { // do somethings } } }
-
此时线程t不会有任何操作啦,直到从
object
的waitSet
中出来,这里的出来,有以下几种情况object
调用了notify
方法,并且线程 t 在waitSet
中被选中然后移出object
调用了notifyAll
方法,并且线程 t 在waitSet
中被移出- 线程 t 被
Interrupt
- 如果线程 t 是调用
wait(millisecs)
或者wait(millisecs, nanosecs)
方法进入waitSet
的,那么过了millisecs 毫秒或者 (millisecs*1000000+nanosecs) 纳秒后,线程 t 也会从waitSet
中移出。
这里线程t从
waitSet
中出来,并不是马上就往下执行,需要获取锁才行,那么怎么才能获取锁呢?在Synchronized中只有另外一个线程释放掉锁才行!这是需要注意的! -
线程 t 正常被
notify/notifyAll
,获取锁,然后继续从之前wait
的地方往下执行啦! -
线程 t 在
wait
的时候被中断从而导致从waitSet
中出来,那么线程 t 的中断状态会被重新设置为false,并且抛出InterruptedException
的异常
notify
说明
顾名思义,就是通知 在 对象object的waitSet
的线程 t 出来呗!让线程 t 自己去重新获取锁,然后执行它自己接下来的操作。
分析
在其他线程调用了object的notify/notifyAll
。可能会发生下面某一种情况
notify
操作,那么object的waitSet
不为空,那么就让在waitSet
的某一个线程出来。假如说 线程t 在object的waitSet
,其他线程调用object的notify
,移除的不一定是线程 t,对于哪个线程会被选中而被移出,虚拟机没有提供任何保证。这里需要注意的是,恢复之后的线程 t 如果对 object 进行加锁操作将不会成功,直到拥有锁的线程完全释放锁。notifyAll
操作,那么等待集合中的所有线程都将从等待集合中移出,然后恢复,这些线程恢复后,只有一个线程可以锁住监视器。其他没有拿到锁的线程就继续进入object的waitSet
呗。
Interruptions
说明
上面我们提到了中断,现在我们来说一下,
在线程 t 调用线程 u 上的方法u.interrupt()
,这个操作会将 u 的中断状态设置为 true。
在u的中断状态设置为true之后,下面几个方法会感知到,也就是说只要u的中断状态设置为true,那么下面几个方法会做出反应
-
wait(), wait(long), wait(long, int), join(), join(long), join(long, int), sleep(long), sleep(long, int)
这些方法的方法签名上都有
throws InterruptedException
,这个就是用来响应中断状态修改的。 -
如果线程阻塞在 InterruptibleChannel 类的 IO 操作中,那么这个 channel 会被关闭。
-
如果线程阻塞在一个 Selector 中,那么 select 方法会立即返回。
如果是上面的情况中当u 的中断状态设置为 true之后(也就是调用
u.interrupt()
),会将中断状态重新设置为 false,重新设置为 false,重新设置为 false,然后执行相应的操作(通常就是跳到 catch 异常处)。如果不是以上的情况,那么,线程的
interrupt()
方法被调用,会将线程的中断状态设置为 true。(注意是这里不同)当然,除了这几个方法, LockSupport 中的
park
方法也能自动感知到线程被中断,当然,它不会重置中断状态为 false。再说一遍,只有上面的几种情况会在感知到中断后先重置中断状态为 false,然后再继续执行。
这里我们再啰嗦一下,如果有一个对象 o,而且线程 t 此时在 o 的等待集合中,那么 t 将会从 o 的等待集合中移出。这会让 t 从 wait 操作中恢复过来,t 此时需要获取 o 的监视器锁,获取完锁以后,发现线程 t处于中断状态,此时会抛出 InterruptedException
异常。
实例方法 thread.isInterrupted()
可以知道线程的中断状态。
调用静态方法 Thread.interrupted()
可以返回当前线程的中断状态,同时将中断状态设置为false。
这个方法调用两次,那么第二次一定会返回 false,因为第一次会重置状态。前提是两次调用的中间没有发生设置线程中断状态的其他语句。
等待,通知,中断的交互
说明
如果一个线程在wait
期间,同时发生了通知和中断,它将发生:
- 线程被
notify
唤醒,从wait
中正常返回,然后被中断,那么只是设置一下中断标志为true,后面也不会改变中断状态,又不会抛出异常 - 先被中断,再
notify
,那么中断这个操作已经把线程从对象锁的waitSet
中取出了,所以notify
就不会唤醒这个线程了,那么如之前所说的,这个被中断的线程的中断状态是先被设置为true,被wait
感受到中断后,再重新设置为false,那么以后想查看这个线程的状态肯定是false啦!后面就是正常的抛出异常了!
考虑是否有这个场景:x 被设置了中断状态,notify 选中了集合中的线程 x,那么这次 notify 将唤醒线程 x,其他线程(我们假设还有其他线程在等待)不会有变化。
答案:存在这种场景。因为这种场景是满足上述条件的,而且此时 x 的中断状态是 true。
例子
-
我们先来验证下线程被从
wait
到被notify
之后,需要重新获得锁才能从之前wait的地方往下走的package common; public class WaitNotify { public static void main(String[] args) { Object object = new Object(); new Thread(new Runnable() { @Override public void run() { synchronized (object){ System.out.println("t1获取object的锁"); try { System.out.println("t1开始休眠啦!并且交出object的锁!"); object.wait(); System.out.println("t1过了很久终于恢复啦。为什么t2通知我还没恢复呢?因为t2通知我的时候没拿到锁呀!"); } catch (InterruptedException e) { System.out.println("t1在wait的时候发生了异常啦!"); e.printStackTrace(); } } } },"t1").start(); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } new Thread(new Runnable() { @Override public void run() { synchronized (object){ System.out.println("t2获取到t1释放的锁啦!"); System.out.println("t2 notify一下,但是被通知的t1不能拿到锁,因为我还没释放哈哈哈"); object.notify(); System.out.println("t2 notify完了,那我先sleep 1s 吧,但是和wait不一样,我sleep的时候是不会交出锁的!"); try { Thread.sleep(1000); } catch (InterruptedException e) { } System.out.println("t2休息够了,结束我的操作了!就让出锁吧"); } } },"t2").start(); } }
运行结果
t1获取object的锁
t1开始休眠啦!并且交出object的锁!
t2获取到t1释放的锁啦!
t2 notify一下,但是被通知的t1不能拿到锁,因为我还没释放哈哈哈
t2 notify完了,那我先sleep 1s 吧,但是和wait不一样,我sleep的时候是不会交出锁的!
t2休息够了,结束我的操作了!就让出锁吧
t1过了很久终于恢复啦。为什么t2通知我还没恢复呢?因为t2通知我的时候没拿到锁呀! -
我们做点修改,让t1线程被中断,然后被需要重新获取锁才能继续往下走,也就是捕获异常
package common; public class WaitNotify { public static void main(String[] args) { Object object = new Object(); Thread t1 = new Thread(new Runnable() { @Override public void run() { synchronized (object){ System.out.println("t1获取object的锁"); try { System.out.println("t1开始休眠啦!并且交出object的锁!"); object.wait(); System.out.println("t1过了很久终于恢复啦。为什么t2通知我还没恢复呢?因为t2通知我的时候没拿到锁呀!"); } catch (InterruptedException e) { System.out.println("t1在wait过了很久终于发生了异常啦!因为t2开始中断的时候我还没拿到锁,等到t2释放锁我才到这里!"); // e.printStackTrace(); } } } },"t1"); t1.start(); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } Thread t2 = new Thread(new Runnable() { @Override public void run() { synchronized (object){ System.out.println("t2获取到t1释放的锁啦!"); System.out.println("t2 notify一下,但是被通知的t1不能拿到锁,因为我还没释放哈哈哈"); t1.interrupt(); System.out.println("t2 notify完了,那我先sleep 1s 吧,但是和wait不一样,我sleep的时候是不会交出锁的!"); try { Thread.sleep(1000); } catch (InterruptedException e) { } System.out.println("t2休息够了,结束我的操作了!就让出锁吧"); } } },"t2"); t2.start(); } }
运行结果
t1获取object的锁
t1开始休眠啦!并且交出object的锁!
t2获取到t1释放的锁啦!
t2 notify一下,但是被通知的t1不能拿到锁,因为我还没释放哈哈哈
t2 notify完了,那我先sleep 1s 吧,但是和wait不一样,我sleep的时候是不会交出锁的!
t2休息够了,结束我的操作了!就让出锁吧
t1在wait过了很久终于发生了异常啦!因为t2开始中断的时候我还没拿到锁,等到t2释放锁我才到这里! -
中断和notify同时发生的情况,如果是先被中断,再notify,那么就如之前所说的啦,直接看代码!
package common; public class WaitNotify { volatile int a = 0; public static void main(String[] args) { Object object = new Object(); WaitNotify waitNotify = new WaitNotify(); Thread t1 = new Thread(new Runnable() { @Override public void run() { synchronized (object){ System.out.println("t1获取object的锁"); try { System.out.println("t1开始wait啦!并且交出object的锁!"); object.wait(); System.out.println("t1在被t3 notify 之后拿到锁才执行这条语句"); } catch (InterruptedException e) { //先被中断,由于t1已经从waitSet中出来了,所以notify就不会有t1了! System.out.println("t1在wait的时候被中断啦!虽然被中断了,但是t1只能在获取锁之后才可以执行这条语句,由于t1已经从waitSet中出来了,所以notify就不会有t1了!" + Thread.currentThread().isInterrupted()); // e.printStackTrace(); }finally { if(Thread.interrupted()){ //t1先被notify然后再被中断的情况,此时Thread.interrupted()是为true的 System.out.println("t1在被中断之前就被notify啦,但是t2是不会被唤醒的!t1并且不抛出中断异常,只是简单的设置中断标志位true" + " 然后重置为 " + Thread.interrupted()); } } } } },"t1"); t1.start(); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } Thread t2 = new Thread(new Runnable() { @Override public void run() { synchronized (object){ System.out.println("t2获取到t1释放的锁啦!"); try { System.out.println("t2开始wait啦!并且交出object的锁!"); object.wait(); } catch (InterruptedException e) { } System.out.println("t2重新拿到锁啦"); } } },"t2"); t2.start(); try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } Thread t3 = new Thread(new Runnable() { @Override public void run() { synchronized (object){ System.out.println("t3获取到t2释放的锁啦!"); System.out.println("t3开始中断t1"); t1.interrupt();//这里测试是先中断,然后再notify的! waitNotify.a = 1;//volatile 属性赋值主要是为了防止指令重排序 System.out.println("t3 执行notify,但是不知道后面唤醒的是哪一个!"); object.notify(); System.out.println("t3 调用完notify后,休息一会"); try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("t3休息够啦!,结束同步代码块!"); } } },"t3"); t3.start(); } }
运行结果
-
中断和
notify
同时发生的情况,如果是先被notify
,再中断的情况,之前也说过啦,那么直接看代码!package common; public class WaitNotify { volatile int a = 0; public static void main(String[] args) { Object object = new Object(); WaitNotify waitNotify = new WaitNotify(); Thread t1 = new Thread(new Runnable() { @Override public void run() { synchronized (object){ System.out.println("t1获取object的锁"); try { System.out.println("t1开始wait啦!并且交出object的锁!"); object.wait(); System.out.println("t1在被t3 notify 之后拿到锁才执行这条语句"); } catch (InterruptedException e) { //先被中断,由于t1已经从waitSet中出来了,所以notify就不会有t1了! System.out.println("t1在wait的时候被中断啦!虽然被中断了,但是t1只能在获取锁之后才可以执行这条语句,由于t1已经从waitSet中出来了,所以notify就不会有t1了!" + Thread.currentThread().isInterrupted()); // e.printStackTrace(); }finally { if(Thread.interrupted()){ //t1先被notify然后再被中断的情况,此时Thread.interrupted()是为true的 System.out.println("t1在被中断之前就被notify啦,但是t2是不会被唤醒的!t1并且不抛出中断异常,只是简单的设置中断标志位true" + " 然后重置为 " + Thread.interrupted()); } } } } },"t1"); t1.start(); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } Thread t2 = new Thread(new Runnable() { @Override public void run() { synchronized (object){ System.out.println("t2获取到t1释放的锁啦!"); try { System.out.println("t2开始wait啦!并且交出object的锁!"); object.wait(); } catch (InterruptedException e) { } System.out.println("t2重新拿到锁啦"); } } },"t2"); t2.start(); try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } Thread t3 = new Thread(new Runnable() { @Override public void run() { synchronized (object){ System.out.println("t3获取到t2释放的锁啦!"); System.out.println("t3开始中断t1"); object.notify(); //这里特意先把notify放在前面是为了测试先被t1 notify,然后t1 再被中断的情况 waitNotify.a = 1;//volatile 属性赋值主要是为了防止指令重排序 System.out.println("t3 执行notify,但是不知道后面唤醒的是哪一个!"); t1.interrupt(); // t1 中断 System.out.println("t3 调用完notify后,休息一会"); try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("t3休息够啦!,结束同步代码块!"); } } },"t3"); t3.start(); } }
运行结果