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://*.com/questions/2779484/why-must-wait-always-be-in-synchronized-block
上一篇: extjs 中比较常见且好用的监听事件