Java并发之等待/通知机制
1 前言
本篇文章默认大家对synchronized跟ReentrantLock有一定了解。
1.1 先来段代码放松一下
下面一段简单的代码,主要是通过3个线程对count进行累计来进行模拟多线程的场景。
/**
* zhongxianyao
*/
public class Test {
private static final int N = 3;
private int count = 0;
public void doSomething() {
// 实际业务中,这里可能是远程获取数据之类的耗时操作
for (int i=0; i<1000_000; i++) {
synchronized (this) {
count ++;
}
}
}
public static void main(String[] args) throws Exception {
Test test = new Test();
for (int i=0; i<N; i++) {
Runnable runnable = () -> test.doSomething();
new Thread(runnable).start();
}
Thread.sleep(1000);
System.out.println(test.count);
}
}
在多线程编程中,一旦调用start()后,什么时候真正分配CPU时间片运行是不确定的,运行多久也是不确定的,所以有时候可能根据经验,预估一下程序的运行时间,然后进行sleep,最后获取结果。但这种方式太low了,有没有那么一种方式,当程序获取到结果后进行通知呢?下面将引出今天要讲的等待/通知机制。
2 Object wait()/notify()
2.1 一段入门代码
先来一段代码看一下wait()/notify()的基本用法
/**
* zhongxianyao
*/
public class Test {
private static final int N = 3;
private int count = 0;
private int finishCount = 0;
public void doSomething() {
for (int i=0; i<1000_000; i++) {
synchronized (this) {
count ++;
}
}
synchronized (this) {
finishCount ++;
notify();
}
}
public static void main(String[] args) {
Test test = new Test();
for (int i=0; i<N; i++) {
Runnable runnable = () -> test.doSomething();
new Thread(runnable).start();
}
synchronized (test) {
try {
while (test.finishCount != N) {
test.wait();
}
} catch (Exception e) {
e.printStackTrace();
}
}
System.out.println(test.count);
}
}
结果输出3000000,结果是正确,是自己想要的。
2.2 问题三连击
a.为什么官方说wait() 要放在while里面?
接口描述如下
As in the one argument version, interrupts and spurious wakeups are possible, and this method should always be used in a loop:
synchronized (obj) {
while (<condition does not hold>)
obj.wait();
... // Perform action appropriate to condition
}
翻译一下:在一个论点版本中,中断跟虚假唤醒是可能,所以这个方法应始终放在一个循环中。
加上一句自己的解释:一般在项目中,一个线程不可能无缘无故等待,总是需要在某种条件下进行等待,而且其他线程唤醒这个线程的时候,可能用的是notifyAll(),数据被其他线程消费了,这里需要在判断一下是否满足特定的条件再继续运行。
b.为什么wait()必须在同步方法/代码块中调用?
解释1:wait()本身设计的逻辑就是在释放锁进行等待,如果没有获取锁,谈何释放。
解释2:通常在wait()的方法前面都会有while语句的判断,在这两条语句中会有时间间隔,可能会破坏程序,需要加上synchronized同步代码块来保证原子操作。
c.为什么wait(), notify() 和 notifyAll()是定义在Object里面而不是在Thread里面?
因为wait()等方法都是锁级别操作,再者Java提供的锁都是对象级别的而不是线程级别的,每个对象都有锁。如果wait()方法定义在Thread类中,线程正在等待的是哪个锁就不明显了。
2.3 wait(long timeout)
在上面的例子中,如果notify();那行代码删除,wait()改为wait(100),如下,那么程序是否可以获取到正确的结果呢?
/**
* zhongxianyao
*/
public class Test {
private static final int N = 3;
private int count = 0;
private int finishCount = 0;
public void doSomething() {
for (int i=0; i<1000_000; i++) {
synchronized (this) {
count ++;
}
}
synchronized (this) {
finishCount ++;
//notify();
}
}
public static void main(String[] args) {
Test test = new Test();
for (int i=0; i<N; i++) {
Runnable runnable = () -> test.doSomething();
new Thread(runnable).start();
}
synchronized (test) {
try {
while (test.finishCount != N) {
test.wait(100);
}
} catch (Exception e) {
e.printStackTrace();
}
}
System.out.println(test.count);
}
}
运行结果是3000000,是正确的结果,看了一下文档,发现这个字段跟直觉理解的不一样,直觉告诉我,这个是最长等多久,等太久了就InterruptedException,结果不是。这个方法设置的时间,是说等待多久就唤醒自己。
3 Condition await()/signal()
3.1 用Condition进行替换
下面的代码,把前一个例子中的synchronized代码块,换成了lock()/unlock,notify()换成了condition.signal(),wait()换成了condition.await()。运行结果也是正确的。
/**
* zhongxianyao
*/
public class Test {
private static final int N = 3;
private int count = 0;
private int finishCount = 0;
private Lock lock = new ReentrantLock();
private Condition condition = lock.newCondition();
public void doSomething() {
for (int i=0; i<1000_000; i++) {
synchronized (this) {
count ++;
}
}
lock.lock();
finishCount ++;
if (finishCount == N) {
condition.signal();
}
lock.unlock();
}
public static void main(String[] args) {
Test test = new Test();
for (int i=0; i<N; i++) {
Runnable runnable = () -> test.doSomething();
new Thread(runnable).start();
}
test.lock.lock();
try {
test.condition.await();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
test.lock.unlock();
}
System.out.println(test.count);
}
}
3.2 signal()方法后不建议添加逻辑
public class ConditionTest {
public static void main(String[] args) {
ReentrantLock lock = new ReentrantLock();
Condition condition = lock.newCondition();
new Thread(() -> {
try {
long time = System.currentTimeMillis();
lock.lock();
System.out.println("await start");
condition.await();
System.out.println("await end " + (System.currentTimeMillis() - time) + "ms");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}, "Thread-await").start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(() -> {
try {
lock.lock();
System.out.println("signal start");
TimeUnit.SECONDS.sleep(5);
condition.signal();
System.out.println("signal end");
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
System.out.println("signal unlock");
}
}, "Thread-signal").start();
}
}
多次运行,结果都是一样的,如下:
await start
signal start
signal end
signal unlock
await end 5005ms
从运行结果可以看出,await()后,锁就释放了,但signal()后,锁不释放,一定要在unlock()之后,锁才释放,await()才会往下执行。
既然唤醒了其他线程,又不释放锁,可以调整唤醒的时机。一般在实际代码中,也是不建议signal()方法后添加逻辑,应该直接释放锁。
同理,上面的notify()也是在synchronized代码块结束后,wait()后面的语句才能真正执行。
3.3 boolean await(long time, TimeUnit unit)
把上面的condition.await()改为condition.await(1, TimeUnit.SECONDS),然后获取返回值,运行结果返回的是false。
这个时候,如果把TimeUnit.SECONDS.sleep(5),condition.signal()这两行代码顺序调换一下,那么await的返回值就是true。
再看到官方文档对这个返回值的描述,如下
{@code false} if the waiting time detectably elapsed
before return from the method, else {@code true}
翻译过来,大致意思就是“如果等待时间可以在方法返回之前检测到返回false,否则返回true”。但实际测试结果却是await()被唤醒的时候,而不是方法返回的时候。
4 区别
- Object wait() notify() 搭配synchronized使用
- Condition await() signal() 搭配Lock使用
- Object notify() 是随机唤醒一个
- Condition signal() 是唤醒第一个await()的线程
- Object wait()有虚假唤醒,而Condition await() 没有
5 参考文档
- https://docs.oracle.com/javase/8/docs/api/java/lang/Object.html
- https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/locks/Condition.html
- https://stackoverflow.com/questions/2779484/why-must-wait-always-be-in-synchronized-block
Java并发之等待/通知机制的更多相关文章
- 【Java并发基础】使用“等待—通知”机制优化死锁中占用且等待解决方案
前言 在前篇介绍死锁的文章中,我们破坏等待占用且等待条件时,用了一个死循环来获取两个账本对象. // 一次性申请转出账户和转入账户,直到成功 while(!actr.apply(this, targe ...
- 二 Java利用等待/通知机制实现一个线程池
接着上一篇博客的 一Java线程的等待/通知模型 ,没有看过的建议先看一下.下面我们用等待通知机制来实现一个线程池 线程的任务就以打印一行文本来模拟耗时的任务.主要代码如下: 1 定义一个任务的接口 ...
- Java Concurrency - wait & notify, 等待通知机制
生产者消费者问题是一个常见的多线程同步案例:一组生产者线程和一组消费者线程共享一个初始状态为空.大小为 N 的缓冲区.只有当缓冲区没满的时候,生产者才能把消息放入缓冲区,否则必须等待:只有缓冲区不空的 ...
- java多线程系列(三)---等待通知机制
等待通知机制 前言:本系列将从零开始讲解java多线程相关的技术,内容参考于<java多线程核心技术>与<java并发编程实战>等相关资料,希望站在巨人的肩膀上,再通过我的理解 ...
- Java多线程之三volatile与等待通知机制示例
原子性,可见性与有序性 在多线程中,线程同步的时候一般需要考虑原子性,可见性与有序性 原子性 原子性定义:一个操作或者多个操作在执行过程中要么全部执行完成,要么全部都不执行,不存在执行一部分的情况. ...
- Java并发编程,Condition的await和signal等待通知机制
Condition简介 Object类是Java中所有类的父类, 在线程间实现通信的往往会应用到Object的几个方法: wait(),wait(long timeout),wait(long tim ...
- 《java多线程编程核心技术》不使用等待通知机制 实现线程间通信的 疑问分析
不使用等待通知机制 实现线程间通信的 疑问分析 2018年04月03日 17:15:08 ayf 阅读数:33 编辑 <java多线程编程核心技术>一书第三章开头,有如下案例: ...
- Java并发读书笔记:线程通信之等待通知机制
目录 synchronized 与 volatile 等待/通知机制 等待 通知 面试常问的几个问题 sleep方法和wait方法的区别 关于放弃对象监视器 在并发编程中,保证线程同步,从而实现线程之 ...
- Java并发编程(04):线程间通信,等待/通知机制
本文源码:GitHub·点这里 || GitEE·点这里 一.概念简介 1.线程通信 在操作系统中,线程是个独立的个体,但是在线程执行过程中,如果处理同一个业务逻辑,可能会产生资源争抢,导致并发问题, ...
随机推荐
- 阶段性项目 ATM+购物车项目
ATM + 购物车https://www.cnblogs.com/kermitjam/articles/10687180.html readme 内容前戏: 一个项目是如何从无到有的. 一 需求分析 ...
- 命令行的方式启动和关闭Mysql
命令行的方式启动和关闭Mysql mysql -h 主机名 -P 端口号 -u 用户名 -p 密码 例如:mysql -h localhost -P 3306 -u root -p11111(p和密码 ...
- spi调试步骤,mcp2515调试整理
1.先查看也硬件电路,一般都是单片机作为master,其他芯片作为slave.单片机的MOSI对应从机的SI,单片机的MISO对应从机的SO,如果两个引脚接反了,那是怎么调试都没有效果的. 2.单片机 ...
- 细说Unicode(一) Unicode初认识
https://segmentfault.com/a/1190000007992346 细说Unicode(一) Unicode初认识 网站开发中经常会被乱码问题困扰.知道文件编码错误会导致乱码,但对 ...
- mui 等待动画loading mui.showLoading
显示加载框:mui.showLoading("正在加载..","div"); //加载文字和类型,plus环境中类型为div时强制以div方式显示隐藏加载框:m ...
- Game of Cards Gym - 101128G (SG函数)
Problem G: Game of Cards \[ Time Limit: 1 s \quad Memory Limit: 256 MiB \] 题意 题意就是给出\(n\)堆扑克牌,然后给出一个 ...
- linux学习11 Linux基础命令及命令历史
一.Linux系统上的文件类型 1.- :常规文件:在其它程序中用f表示.比如我们用ls -l命令查看的第一个内容 [root@localhost ~]# ls -l total -rw------- ...
- podium podlets 说明
podlets 提供了一个页面片段服务,podlets 包含了一些元数据信息,通过json 暴露, 主要包含以下内容 一个 http endpoint 提供主要内容 一个 http endpoint ...
- 20181107 模拟赛T1:快乐传递政治正确版
问题描述 David 有很多好朋友.有些期末季刚结束,有些人很快乐,但有些不太快乐,David 想把快乐传递给每个人,作为心理学大师,他准备了如下计划:David 的朋友中有 n 个男生和 m 个女生 ...
- Map、Set、List是否有序
首先我们应该清楚这个概念:这里的有序和无序不是指集合中的排序,而是是否按照元素添加的顺序来存储对象. list是按照元素的添加顺序来存储对象的,因此是有序的.他的实现类ArrayList.Linked ...