多线程学习-基础(六)分析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有自己的调度控制(目前我还不清楚,以后再慢慢研究,同样了解的朋友可以直接告知,不吝感谢)。
多线程学习-基础(六)分析wait()-notify()-notifyAll()的更多相关文章
- 多线程学习-基础(十二)生产者消费者模型:wait(),sleep(),notify()实现
一.多线程模型一:生产者消费者模型 (1)模型图:(从网上找的图,清晰明了) (2)生产者消费者模型原理说明: 这个模型核心是围绕着一个“仓库”的概念,生产者消费者都是围绕着:“仓库”来进行操作, ...
- 多线程学习-基础( 十)一个synchronized(){/*代码块*/}简单案例分析
一.提出疑惑 上一篇文章中,分析了synchronized关键字的用法.但是好像遗漏了一种情况. 那就是: synchronized(obj){/*同步块代码*/} 一般有以下几种情况: (1)syn ...
- 多线程学习-基础( 十一)synchronized关键字修饰方法的简单案例
一.本案例设计到的知识点 (1)Object的notify(),notifyAll(),wait()等方法 (2)Thread的sleep(),interrupt(). (3)如何终止线程. (4)如 ...
- Java多线程学习(六)Lock锁的使用
系列文章传送门: Java多线程学习(二)synchronized关键字(1) Java多线程学习(二)synchronized关键字(2) Java多线程学习(三)volatile关键字 Java多 ...
- 多线程学习-基础(一)Thread和Runnable实现多线程
很久没记录一些技术学习过程了,这周周五的时候偶尔打开“博客园”,忽然让我产生一种重拾记录学习过程的想法,记录下学习研究过程的一点一滴,我相信,慢慢地就进步了!最近想学习一下多线程高并发,但是多线程在实 ...
- 多线程学习-基础(四)常用函数说明:sleep-join-yield
一.常用函数的使用 (1)Thread.sleep(long millis):在指定的毫秒内让当前正在执行的线程休眠(暂停执行),休眠时不会释放当前所持有的对象的锁.(2)join():主线程等待子线 ...
- Java多线程系列 基础篇10 wait/notify/sleep/yield/join
1.Object类中的wait()/notify()/notifyAll() wait(): 让当前线程处于Waiting状态并释放掉持有的对象锁,直到其他线程调用此对象的线程notify()/not ...
- Java 多线程学习笔记:wait、notify、notifyAll的阻塞和恢复
前言:昨天尝试用Java自行实现生产者消费者问题(Producer-Consumer Problem),在coding时,使用到了Condition的await和signalAll方法,然后顺便想起了 ...
- 多线程学习笔记六之并发工具类CountDownLatch和CyclicBarrier
目录 简介 CountDownLatch 示例 实现分析 CountDownLatch与Thread.join() CyclicBarrier 实现分析 CountDownLatch和CyclicBa ...
随机推荐
- Java中print()、printf()、println()的区别?
区别: 1.printf主要是继承了C语言的printf的一些特性,可以进行格式化输出 2.print就是一般的标准输出,输入信息后不会换行 3.println输入信息会换行 参照JAVA API的定 ...
- SSL/TLS捕包分析
一.基本概念 SSL:(Secure Socket Layer,安全套接字层),位于可靠的面向连接的网络层协议和应用层协议之间的一种协议层.SSL通过互相认证.使用数字签名确保完整性.使用加密确保私密 ...
- Shiro-Session
概述 Shiro提供了完整的企业级会话管理功能,不依赖于底层容器(如web容器tomcat),不管JavaSE还是JavaEE环境都可以使用,提供了会话管理.会话事件监听.会话存储/持久化.容器无关的 ...
- LeetCode Output Contest Matches
原题链接在这里:https://leetcode.com/problems/output-contest-matches/description/ 题目: During the NBA playoff ...
- LeetCode Longest Uncommon Subsequence II
原题链接在这里:https://leetcode.com/problems/longest-uncommon-subsequence-ii/#/description 题目: Given a list ...
- C# XML反序列化与序列化举例:XmlSerializer
using System; using System.IO; using System.Xml.Serialization; namespace XStream { /// <summary&g ...
- 尚硅谷Java视频教程导航(学习路线图)
最近很火,上去看了看,对于入门的人还是有点作用的,做个记号,留着以后学习. Java视频教程下载导航(学习路线图) 网站地址:http://www.atguigu.com/download.shtml
- 聊聊WPF中字体的设置
1. 今天帮同事调试一个字体的bug:TextBox中的中文显示大小不一致, 比如包含"杰","热". 原因是WPF针对点阵字体需要指定特定字体才能正确渲染, ...
- 基于TCP协议 I/O多路转接(select) 的高性能回显服务器客户端模型
服务端代码: myselect.c #include <stdio.h> #include <netinet/in.h> #include <arpa/inet.h> ...
- MySQL 查询数据表里面时间字段为今天添加的计数
一: 下面这条语句查出来的count值 . 查询类型ID(category_id)为18的,今天插入的数据数, created_on: 为数据表中一字段 datetime类型, 记录此条数据添加的时 ...