wait 方法注释中的标准使用模式(JDK 8)

22 篇文章 2 订阅

查看 java.lang.Object#wait(long) 方法的注释时,发现了在注释中给了一个使用范例,JDK中其他地方竟然也使用了这种范例,于是来了兴趣,记录一下。

一、原注释

在这里插入图片描述

简要说明,就是为了防止 “ 伪唤醒 ”等,需要使用条件循环来做再次判断。

好的。这里提到了两本书有详细的介绍。找了这两本书的说明如下:

1.1 、《Java并发编程实战》

说明:

过早唤醒

虽然在锁、条件谓词和条件队列之间的三元关系并不复杂,但 wait 方法的放回并不一定意味着线程正在等待的条件谓词已经变成真了。

内置条件队列可以与多个条件谓词一起使用。当一个线程由于调用 notifyAll 而醒来时,并不意味该线程正在等待的条件谓词已经变成真了。(这就像烤面包机和咖啡机共用一个铃声,当响铃后,你必须查看是哪个设备发出的铃声。)另外,wait 方法还可以 “ 假装 ” 返回,而不是由于某个线程调用了 notify。

当执行控制重新进入调用 wait 的代码时,它已经重新获取了与条件队列相关联的锁。现在条件谓词是不是已经变为真了?或许。在发出通知的线程调用 notifyAll 时,条件谓词可能已经变成真,但在重新获取锁时将再次变为假。在线程被唤醒到 wait 重新获取锁的这段时间里,可能有其他线程已经获取了这个锁,并修改了对象的状态。或者,条件谓词从调用 wait 起根本就没有变成真。你并不知道另一个线程为什么调用 notify 或 notifyAll ,也许因为与同一条件队列相关的另一个条件谓词变成了真。“ 一个条件队列与多个条件谓词相关 ” 是一种很常见的情况。

基于所有这些原因,每次线程从 wait 中唤醒时,都必须再次测试条件谓词,如果条件谓词不为真,那么就继续等待(或者失败)。由于线程在条件谓词不为真的情况下也可以反复地醒来,因此必须在一个循环中调用 wait,并在每次迭代中都测试条件谓词。条件等待的标准形式如下:

void stateDependentMethod() throws InterruptedException{
	// 必须通过一个锁来保护条件谓词
	synchronized(lock){
		while(!conditionPredicate())
			lock.wait()
		// 现在对象处于合适的状态
	}
}

1.2、《Effective Java》

说明:

并发工具优先于 wait 和 notify

虽然你始终应该优先使用并发工具,而不是使用 wait 和 notify 方法,但可能必要维护使用了 wait 和 notify 方法的遗留代码。 wait 方法被用来是线程等待某个条件。它必须在同步区域内部被使用,这个同步区域将对象锁定在了调用 wait 方法的对象上。下面是使用 wait 方法的标准模式

// The standard idiom for using the wait method 
synchronized (obj){
	while (<condition does not hold>)
		obj.wait(); // (Releases lock, and reacquires on wakeup)
	     ...// Perform action appropriate to condition
}

始终应该使用 wait 循环模式来调用 wait 方法;永远不要在循环之外调用 wait 方法。

循环会在等待之前和之后对条件经行测试。

在等待之前测试条件,当条件已经成立时就跳过等待,这对于确保活性是必要的。如果条件已经成立,并且在线程等待之前,notify (或者 notifyAll)方法已经被调用,则无法保证该线程会从等待中苏醒过来。

在等待之后测试,如果条件不成立的话继续等待,这对于确保安全性是必要的。当条件不成立的时候,如果线程继续执行,则可能会破坏被锁保护的约束条件。当条件不成立时,有下面一些理由可使一个线程苏醒过来:

  • 另一个线程可能已经得到了锁,并且从一个线程调用 notify 方法那一刻起,到等待线程苏醒过来的这段是时间中,得到锁的线程已经改变了保护的状态。
  • 条件并不成立,但是另一个线程可能意外地或恶意地调用了 notify 方法。在公有可访问的对象上等待,这些类实际上把自己暴露在了这种危险的境地中。公有可访问对象的同步方法中包含的 wait 方法都会出现这样的问题。
  • 通知线程(notifying thread)在唤醒等待线程时可能会过度 “ 大方 ”。例如,即使只有某些等待线程的条件已经被满足,但是通知线程可能仍然调用 notifyAll 方法。
  • 在没有通知的情况下,等待线程也可能(但很少)会苏醒过来。这被称为 “ 伪唤醒 ”(spurious wakeup)。

二、JDK中的使用范例

java.lang.Thread#join(long)


public final synchronized void join(long millis)
throws InterruptedException {
    long base = System.currentTimeMillis();
    long now = 0;

    if (millis < 0) {
        throw new IllegalArgumentException("timeout value is negative");
    }

    if (millis == 0) {
       // 循环判断条件,进入等待
        while (isAlive()) {
            wait(0);
        }
    } else {
        // 循环判断条件,进入等待
        while (isAlive()) {
            long delay = millis - now;
            if (delay <= 0) {
                break;
            }
            wait(delay);
            now = System.currentTimeMillis() - base;
        }
    }
}

三、等待 / 通知的经典范式

该范式分为两部分,分别针对等待方(消费者)和通知方(生产者)。

等待方遵循如下原则:

  1. 获取对象的锁。
  2. 如果条件不满足,那么调用对象的 wait() 方法,被通知后仍要检查条件。
  3. 条件满足则执行对应的逻辑。

对应的伪代码如下:

synchronized(对象){
	while(条件不满足){
	对象.wait();
	}
	对应的处理逻辑
}

通知方遵循如下原则:

  1. 获取对象的锁。
  2. 改变条件。
  3. 通知所有等待在对象上的线程。

对应的伪代码如下:

synchronized(对象){
	改变条件
	对象.notifyAll();
}

参考

《Java并发编程的艺术》
《Java并发编程实战》
《Effective Java》

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值