多线程学习-基础(六)分析wait()-notify()-notifyAll()
一、理解wait()-notify()-notifyAll()
obj.wait()与obj.notify()必须要与synchronized(Obj)一起使用,也就是wait,notify是针对已经获取了Obj锁进行操作;
从语法角度上来说:Obj.wait()和Obj.notify()必须在synchronized(Obj){..}或者synchronized方法中
语句块内。
从功能上来说:wait()就是线程获取对象锁后,主动释放对象锁,同时本线程休眠,直到有其他线程调用对象的notify()唤醒该线程,才能继续获取对象锁,并继续执行。相应地notify()就是对象锁的唤醒操作。
值得注意的是:notify()调用后,并不是立马释放对象锁的,而是在相应的synchronized(Obj){...}语句块执行结束,自动释放锁后,JVM会在wait()对象锁的线程中随机选取一个线程,赋予其对象锁,唤醒线程,继续执行。这样就提供了线程间同步,唤醒操作。
Thread.sleep(millis)和Object.wait()二者都可以暂停当前线程,释放CPU的控制权,主要区别在于Object.wait()在释放CPU的控制权的同时,释放了对象锁的控制,而Thread.sleep(millis)仍然保留对象锁的控制权。
Object.notify()和Object.notifyAll()作用都是唤醒Object对象上释放了对象锁,处于等待状态的线程。区别在于:notify()是随机唤醒其中一个,而notifyAll()是全部唤醒。
根据上面的概念描述,现在我有些困惑的地方,具体罗列如下:(参考状态转换图)
(1)一个对象的锁可以同时被多个线程拿到吗?
(2)举例一个场景:当持有Obj对象锁的A线程在run()中的synchronized(Obj){...}的代码块中调用了 Obj.wait()方法,理论上来说,此时线程A就会释放Obj对象的锁,A线程进入等待队列。
问题一:A线程是立马释放Obj对象锁,还是在synchronized(Obj){...}代码块执行结束后释放。
场景补充:
当A线程释放了Obj的对象锁后,此时有一个B线程也执行到了run()方法中的synchronized(Obj){...},首先此时,B线程肯定控制着Obj的对象锁,其他的线程都不可以操作Obj对象,只有B线程可以。这个时候B线程执行了Obj.notify()方法,等到B线程执行完synchronized(Obj){...}代码块,并释放 Obj对象锁后,JVM的调度机制会随机地选择一个在Obj对象上等待的线程,将其唤醒(不一定是A线程),唤醒之后的的线程进入锁池状态,我们从上面的分析图可以看到转换流程,那么问题来了?
问题二:锁池状态是什么状态?
问题三: 根据转换图可以知道,等待队列中的线程,可以被Obj.notify()或者Obj.notifyAll()或者wait()时间结束自动唤醒,进入锁池状态,也就是说锁池状态中的线程可以有多个,继续转换图流程往下看,锁池状态中的某一条线程可以拿到对象的锁标记还是所有的线程都可以拿到所标记,是正面控制 线程拿到锁标记的?,这个时候才可以进入可运行状态(即就绪状态)
只分析理论是会有很多困惑,必须要动手实践来逐步验证剖析这些问题,找到答案,那么动手吧!
二、简单案例分析:wait()和notify()
对Object.wait(),Object.notify()的应用最经典的例子,应该是三线程打印ABC的问题了吧,这是一道比较经典的面试题,题目要求如下:
案例要求:
建立三个线程A,B,C, A线程打印10次A, B线程打印10次B, C线程打印10次C,要求线程同时运行,交替打印10次ABC。
这个问题可以用Object.wait()和Object.notify()就可以很方便解决,代码如下:
package com.jason.comfuns.wait; /** * 多线程学习 * @function wait()方法测试 * @author 小风微凉 * @time 2018-4-22 上午9:25:43 */ public class Thread_wait_Action implements Runnable { //设置属性 private String name; private Object prev; private Object self; //构造器 public Thread_wait_Action(String name,Object prev,Object self){ this.name=name; this.prev=prev; this.self=self; } //线程run() public void run() { int count=10; while(count>0){ synchronized (prev) { synchronized (self) { System.out.print(name); count--; self.notify();//唤醒此对象上的线程 } try { prev.wait();//在此对象上等待,直到被唤醒才继续循环,以此类推 } catch (InterruptedException e) { e.printStackTrace(); } } } } public static void main(String[] args) throws InterruptedException { //创建三个会对象,有三把对象锁 Object a=new Object(); Object b=new Object(); Object c=new Object(); //创建三个线程 Thread_wait_Action thread1=new Thread_wait_Action("A",c,a); Thread_wait_Action thread2=new Thread_wait_Action("B",a,b); Thread_wait_Action thread3=new Thread_wait_Action("C",b,c); //启动线程 new Thread(thread1).start(); Thread.sleep(100); //确保按顺序A、B、C执行 new Thread(thread2).start(); Thread.sleep(100); new Thread(thread3).start(); Thread.sleep(100); } }
运行结果:
ABCABCABCABCABCABCABCABCABCABC
代码思路分析:
三个线程:A B C
三个对象:a b c
//创建三个线程
Thread_wait_Action thread1=new Thread_wait_Action("A",c,a);
Thread_wait_Action thread2=new Thread_wait_Action("B",a,b);
Thread_wait_Action thread3=new Thread_wait_Action("C",b,c);
第一次执行:A线程 控制对象锁 prev=c self=a 打印输出:A 操作:a.notify()唤醒其他线程 然后c.wait()
分析此时A线程对c/a对象的操作权限:
对象 权限
a 无:a.notify()释放了
c 无:c.wait()释放了
结果:A线程唤醒了a对象上等待的线程,并且A线程开始在c对象上等待 中断循环:此时count=9
第二次执行:B或者C线程
假设1:运行B线程
B线程 控制对象锁 prev=a selef=b 打印输出:B 操作:b.notify()唤醒其他线程 然后a.wait()
分析此时B线程对a/b对象的操作权限:
对象 权限
a 无:a.wait()释放了
b 无 b.notify()释放了
结果:B线程唤醒了b对象上等待的线程,并且B线程开始在a对象上等待 中断循环:此时count=9
假设2:运行C线程,不成立
原因:Thread.sleep(100); //确保按顺序A、B、C执行
第三次执行:C线程 控制对象锁 prev=b self=c 打印输出:C 操作:c.notify()唤醒其他线程 然后b.wait()
分析此时c线程对b/c对象的操作权限:
对象 权限
b 无:b.wait()释放了
c 无 c.notify()释放了
结果:C线程唤醒了c对象上等待的线程,并且C线程开始在b对象上等待 中断循环:此时count=9
一次循环(每三次线程的执行为一个循环)之后的总结:
打印输出:ABC
A线程:在c对象上等待,渴望拿到c对象的对象锁来继续执行。 等待c 唤醒a
B线程:在a对象上等待,渴望拿到a对象的对象锁来继续执行。 等待a 唤醒b
C线程:在b对象上等待,渴望拿到b对象的对象锁来继续执行。 等待b 唤醒c
所以:A唤醒B,B唤醒C,C再唤醒A。
可以看出,一个循环之后线程会重新从A线程开始执行,直到count=0,10次循环结束!
需要注意的是:刚开始执行的时候,需要控制线程执行顺序:如下
//启动线程 new Thread(thread1).start(); Thread.sleep(100); //确保按顺序A、B、C执行 new Thread(thread2).start(); Thread.sleep(100); new Thread(thread3).start(); Thread.sleep(100);
//运行结果
ABCABCABCABCABCABCABCABCABCABC
假如:线程thread1,thread2,thread3 启动的时候,没有Thread.sleep(100)会如何呢?
//启动线程 new Thread(thread1).start(); new Thread(thread2).start(); new Thread(thread3).start();
//运行结果
CBACBACBACBACBACBACBACBACBACBA
或者
ACABCABCABCABCABCABCABCABCABC
或者
......
可以看出:如果不控制初始启动线程的顺序,那么打印输入的结果就变得不确定了!
三.简单案例分析:锁(monitor)池和等待池
在Java中,每个对象都有两个池,锁(monitor)池和等待池。
wait(),notify(),notifyAll()三个方法都是Object类的方法。
锁池:
假设A线程拥有某个对象(注意不是类)的锁,而其他的线程想要调用这个对象的某个synchronized方法(或者synchronized块),由于这些线程在进入对象的synchronized方法之前必须先获取这个对象锁的控制权,但该对象的锁目前正被线程A拥有,
所以这些线程就进入了该对象的锁池中。
等待池:
假设一个线程A调用了某个对象的wait()方法,线程A就会释放该对象的锁(因为wait()方法必须出现在synchronized中,这样自然在执行wait()之前,线程A就已经拥有了该对象的锁),同时A进入到了该对象的等待池中。如果另外一个线程调用了相同对象
的notifyAll()方法,那么处于该对象等待池中的所有线程全部进入该对象的锁池中,准备争夺锁的拥有权。如果另外一个线程调用了相同对象的notify()方法,那么仅仅让一个处于高对象的等待池中的线程(随机选取的线程)进入该对象的等待池。
下面通过一个简单的案例来说明:
package com.jason.comfuns.monitors; /** * 多线程学习 * @function 测试:Object的锁池和等待池 * @author 小风微凉 * @time 2018-4-22 上午11:48:53 */ public class Thread_ObjectMonitor_Action { public static void main(String[] args) { //对象 Target t = new Target(); //创建线程 Thread thread1 = new Increase(t); thread1.setName("+"); Thread thread2 = new Decrease(t); thread2.setName("-"); //启动线程 thread1.start(); thread2.start(); } } class Target{ private int count; public synchronized void increase(){ System.out.println(Thread.currentThread().getName()+"线程被唤醒:count="+count); if(count==2){ try { System.out.println(Thread.currentThread().getName()+"线程开始wait休眠,进入等待池"); this.wait();//当前该对象的上的线程进入等待池中 } catch (InterruptedException e) { e.printStackTrace(); } } count++; System.out.println(Thread.currentThread().getName()+"线程释放对象锁,并唤醒t对象上的其他等待线程,count="+count); this.notify();//唤醒该对象的上等待池中的随机一个线程进入锁池中 } public synchronized void decrease(){ System.out.println(Thread.currentThread().getName()+"线程被唤醒:count="+count); if(count == 0) { try { System.out.println(Thread.currentThread().getName()+"线程开始wait休眠,进入等待池"); this.wait();//当前该对象的上的线程进入等待池中 } catch (InterruptedException e) { e.printStackTrace(); } } count--; System.out.println(Thread.currentThread().getName()+"线程释放对象锁,并唤醒t对象上的其他等待线程,count="+count); this.notify(); //唤醒该对象的上等待池中的随机一个线程进入锁池中 } } class Increase extends Thread{ private Target t; public Increase(Target t) { this.t = t; } public void run() { for(int i = 0 ;i < 5; i++) { try { Thread.sleep((long)(Math.random()*500)); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName()+"第"+(i+1)+"次"); t.increase(); //调用对象t的synchronized方法 } } } class Decrease extends Thread { private Target t; public Decrease(Target t){this.t = t;} public void run() { for(int i = 0 ; i < 5 ; i++) { try { //随机睡眠0~500毫秒 //sleep方法的调用,不会释放对象t的锁 Thread.sleep((long)(Math.random()*500)); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName()+"第"+(i+1)+"次"); t.decrease(); //调用对象t的synchronized方法 } } }
运行结果:
-第1次 -线程被唤醒:count=0 -线程开始wait休眠,进入等待池 +第1次 +线程被唤醒:count=0 +线程释放对象锁,并唤醒t对象上的其他等待线程,count=1 -线程释放对象锁,并唤醒t对象上的其他等待线程,count=0 -第2次 -线程被唤醒:count=0 -线程开始wait休眠,进入等待池 +第2次 +线程被唤醒:count=0 +线程释放对象锁,并唤醒t对象上的其他等待线程,count=1 -线程释放对象锁,并唤醒t对象上的其他等待线程,count=0 +第3次 +线程被唤醒:count=0 +线程释放对象锁,并唤醒t对象上的其他等待线程,count=1 -第3次 -线程被唤醒:count=1 -线程释放对象锁,并唤醒t对象上的其他等待线程,count=0 +第4次 +线程被唤醒:count=0 +线程释放对象锁,并唤醒t对象上的其他等待线程,count=1 -第4次 -线程被唤醒:count=1 -线程释放对象锁,并唤醒t对象上的其他等待线程,count=0 +第5次 +线程被唤醒:count=0 +线程释放对象锁,并唤醒t对象上的其他等待线程,count=1 -第5次 -线程被唤醒:count=1 -线程释放对象锁,并唤醒t对象上的其他等待线程,count=0
结果分析:
(1)根据上面的代码,可以知道“+”和“-”这两个线程的执行时没有先后顺序的。
分析:(后面的结果截取均来至下面的结果部分)
-第1次 -线程被唤醒:count=0 -线程开始wait休眠,进入等待池 +第1次 +线程被唤醒:count=0 +线程释放对象锁,并唤醒t对象上的其他等待线程,count=1 -线程释放对象锁,并唤醒t对象上的其他等待线程,count=0 -第2次 -线程被唤醒:count=0 -线程开始wait休眠,进入等待池 +第2次 +线程被唤醒:count=0 +线程释放对象锁,并唤醒t对象上的其他等待线程,count=1 -线程释放对象锁,并唤醒t对象上的其他等待线程,count=0
可以看出;
"-"线程第一次执行的时候,就休眠了,“-”线程进入等待池中
-第1次
-线程被唤醒:count=0
-线程开始wait休眠,进入等待池
紧接着“+”线程在target对象的锁池中,成功抢夺了target对象的锁的控制权,开始执行
+第1次
+线程被唤醒:count=0
+线程释放对象锁,并唤醒t对象上的其他等待线程,count=1
根据代码可以知道,在“+”线程会调用:
this.notify();//唤醒该对象的上等待池中的随机一个线程进入锁池中
然后“+”线程的任务完成,并成功释放了target对象的锁,并唤醒了“-”线程,是的“-”线程从等待池中转移到了锁池中,此时“+”线程和“-”线程同时存在于锁池中,并且两个线程的优先级别是一样的(由于都没有设置优先级,所有优先界别都默认为:NORM_PRIORITY=5)
此时:“+”线程和“-”线程开始抢夺target对象的控制权。谁抢到,谁就继续开始执行。
我们继续看一下代码:(以:increase()方法为例)
if(count == 0) { try { System.out.println(Thread.currentThread().getName()+"线程开始wait休眠,进入等待池"); this.wait();//当前该对象的上的线程进入等待池中 } catch (InterruptedException e) { e.printStackTrace(); } } count--; System.out.println(Thread.currentThread().getName()+"线程释放对象锁,并唤醒t对象上的其他等待线程,count="+count); this.notify(); //唤醒该对象的上等待池中的随机一个线程进入锁池中
可以看到如果线程被notify()唤醒并且继续执行的话,会继续执行this.wait()后面的代码:
count--; System.out.println(Thread.currentThread().getName()+"线程释放对象锁,并唤醒t对象上的其他等待线程,count="+count); this.notify(); //唤醒该对象的上等待池中的随机一个线程进入锁池中
那么我们继续看一下运行结果:观察是否是这样的现象!!!!
-第1次 -线程被唤醒:count=0 -线程开始wait休眠,进入等待池 +第1次 +线程被唤醒:count=0 +线程释放对象锁,并唤醒t对象上的其他等待线程,count=1 -线程释放对象锁,并唤醒t对象上的其他等待线程,count=0 -第2次 -线程被唤醒:count=0 -线程开始wait休眠,进入等待池 +第2次 +线程被唤醒:count=0 +线程释放对象锁,并唤醒t对象上的其他等待线程,count=1 -线程释放对象锁,并唤醒t对象上的其他等待线程,count=0
可以看出,继续执行的话是:“-”线程抢夺到了target对象的锁,并且确实是继续执行this.wait()后面的代码。此时:又会执行一次this.notify(),"-"线程仍然后释放target对象的锁,然后存在于锁池中,继续和锁池中另外一个线程:“+”线程继续抢夺target对象锁的控制权。
后面的显示结果,就是这样以此类推,一次往后执行。
需要注意的是:控制线程执行个数的控制源在:
public void run() { for(int i = 0 ;i < 5; i++) { //...... } }
四、归纳总结
(1)最开始的疑惑在上面的两个简单案例中已经得到了明确的答案;(如果有不对的地方,请指正,一起交流共同进步。)
- 问:“一个对象的锁可以同时被多个线程拿到吗?”
答案:当然不可以,一个对象的锁只能被一个线程锁拥有。只有当前线程释放了该对象的对象锁,其他在该对象锁池中的线程才有机会彼此竞争抢夺该对象的对象锁。
- 问:“一个线程在run()的synchronized方法或synchronized块中,如果要释放对象锁是立马释放还是synchronized结束之后再释放?”
答案:这个要分情况了。
- this.wait()方式释放对象锁:
this.wait()这句代码执行之后,根据上面我做的案例测试得出的结果是,会立即释放对象锁。
2.this.notify()方式释放对象锁:
this.notify()的作用是释放当前线程所拥有的对象的对象锁,然后再在锁池中和该对象上的其他线程一起竞争抢夺该对象的对象锁。
注意:
this.notify()这句代码执行之后,并不会立马释放该对象的对象锁,而是继续执行this.notify()后面的代码,直到synchronized方法或synchronized块中的代码执行完毕,才会开始释放对象锁,而只有释放对象锁完毕之后,该对象的对象锁才能够被开始竞争抢夺!
3.问:“锁池状态是什么状态?”
答案:见上面的第二个简单案例,里面有分析说明。
4.问:“锁池状态中的某一条线程可以拿到对象的锁标记还是所有的线程都可以拿到所标记,是怎么控制线程拿到锁标记的?”
答案: 根据上面的案例分析和前几条问题的回答,这个问题就很明显了。锁池中的存在一条或多条线程来竞争对象锁的控制权,只会有一条线程获得控制权,不会是多条。哎?(此处回单仅限于cpu是单核的,如果是多核的话,我还没测试过,有测试过的朋友麻烦不吝告知,在此先谢过啦)。
至于如何控制线程拿到对象锁,这个不是我们手动编码控制的,JVM有自己的调度控制(目前我还不清楚,以后再慢慢研究,同样了解的朋友可以直接告知,不吝感谢)。