java 等待_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

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

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 ()

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

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

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 参考文档

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值