多线程学习-基础(六)分析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 ...
随机推荐
- ZOJ2314 Reactor Cooling(有上下界的网络流)
The terrorist group leaded by a well known international terrorist Ben Bladen is buliding a nuclear ...
- BZOJ4861 [Beijing2017]魔法咒语
题意 Chandra 是一个魔法天才.从一岁时接受火之教会洗礼之后, Chandra 就显示出对火元素无与伦比的亲和力,轻而易举地学会种种晦涩难解的法术.这也多亏 Chandra 有着常人难以企及的语 ...
- swing之gridlayout
package gui1; import java.awt.FlowLayout; import java.awt.GridLayout; import javax.swing.JButton; im ...
- linux下安装composer
在linux下使用comoser命令,但是提示composer command not found 那么就是当前环境中没有composer 学习源头: https://blog.csdn.net/gb ...
- 蓝桥杯 Beaver's Calculator
问题描述 从万能词典来的聪明的海狸已经使我们惊讶了一次.他开发了一种新的计算器,他将此命名为"Beaver's Calculator 1.0".它非常特别,并且被计划使用在各种各样 ...
- 用java代码解决10元喝多少瓶汽水的问题
问题:汽水2元一瓶,四个盖子换一瓶,两个空瓶一瓶,问10元可以喝几瓶?(不许借别人空瓶或瓶盖,但可以先喝汽水再付空酒瓶或瓶盖) 最近同事让笔者看了一道脑筋急转弯的数学题,当然不是很难,只要会加减法应该 ...
- JS写一个简单的程序,判断年份是平年还是闰年
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title> ...
- 阿里云服务器访问github慢临时解决方法
su root vi /etc/hosts # github 204.232.175.78 http://documentcloud.github.com 207.97.227.239 http:// ...
- Web访问中的角色与协议
- MySQL存储引擎 -- MyISAM 与 InnoDB 理论对比
MySQL常用的两种存储引擎一个是MyISAM,另一个是InnoDB.两种存储引擎各有各的特点. 1. 区别:(1)事务处理:MyISAM是非事务安全型的.-----而非事务型的系统,一般也称为数据仓 ...