十一、构建自定义的同步工具

状态依赖性的管理

  • 条件队列

条件队列就好像烤面包机中通知“面包已烤好”的铃声。如果你注意听着铃声,那么当面包烤好后可以立刻得到通知,然后放下手头的事情(或者先把手头的事情做完,例如先看完报纸)开始品尝面包。如果没有听见铃声(可能出去拿报纸了),那么会错过通知信息,但回到厨房时还可以观察烤面包机的状态,如果已经烤好,那么就取出面包,如果还未烤好,就再次留意铃声。

“条件队列”这个名字来源于:它使得一组线程(称之为等待线程集合)能够通过某种方式来等待特定的条件变成真。传统队列的元素是一个个数据,而与之不同的是,条件队列中的元素是一个个正在等待相关条件的线程。

正如每个Java对象都可以作为一个锁,每个对象同样可以作为一个条件队列,并且Object中的wait、notify和notifyAll方法就构成了内部条件队列的API。对象的内置锁与其内部条件队列是相互关联的,要调用对象X中条件队列的任何一个方法,必须持有对象X上的锁。这是因为“等待由状态构成的条件”与“维护状态一致性”这两种机制必须被紧密地绑定在一起: 只有能对状态进行检查时,才能在某个条件上等待,并且只有能修改状态时,才能从条件等待中释放另一个线程。

Obiect.wait会自动释放锁,并请求操作系统挂起当前线程,从而使其他线程能够获得这个锁并修改对象的状态。当被挂起的线程醒来时,它将在返回之前重新获取锁。从直观上来理解,调用wait意味着“我要去休息了,但当发生特定的事情时唤醒我”,而调用通知方法(notify)就意味着“特定的事情发了”。

二、使用条件队列

  • 条件谓词

条件谓词是使某个操作成为状态依赖操作的前提条件。在有界缓存中,只有当缓存不为空时,take方法才能执行,否则必须等待。对take方法来说,它的条件谓词就是“缓存不为空”, take方法在执行之前必须首先测试该条件谓词。同样,put方法的条件谓词是“缓存不满”。条件谓词是由类中各个状态变量构成的表达式。BaseBoundedBuffer在测试“缓存不为空”时将把count与0进行比较,在测试“缓存不满”时将把count与缓存的大小进行比较。

将与条件队列相关联的条件谓词以及在这些条件谓词上等待的操作都写入文档是一个良好的习惯。
在条件等待中存在一种重要的三元关系,包括加锁、wait方法和一个条件谓词。在条件谓 词中包含多个状态变量,而状态变量由一个锁来保护,因此在测试条件谓词之前必须先持有这 个锁。锁对象与条件队列对象(即调用wait和notify等方法所在的对象)必须是同一个对象。

每一次wait调用都会隐士地与特定的掉件谓词关联起来。当调用某个特定条件谓词的wait时,调用者必须已经持有与条件队列相关的锁,并且这个锁必须保护着构成条件谓词的状态变量。

  • 过早唤醒

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

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

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

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

扫描二维码关注公众号,回复: 6415670 查看本文章
void stateDependentMethod() throws InterruptedException {
    //必须通过一个锁来保护条件谓词 
    synchronized (lock) {
        while (!conditionPredicate())
            lock.wait();
        //现在对象处于合适的状态
    }
}

当使用条件等待时(例如Object.wait或Condition.await)

  • 通常都有一个条件谓词–包括一些对象状态的测试,线程在执行前必须首先通过这些测试。
  • 在调用wait之前测试条件谓词,并且从wait中返回时再次进行测试。
  • 在一个循环中调用wait。
  • 确保使用与条件队列相关的锁来保护构成条件谓词的各个状态变量。
  • 当调用wait、notify或notifyAU等方法时,一定要持有与条件队列相关的锁。
  • 在检查条件谓词之后以及开始执行相应的操作之前,不要释放锁。
  • 丢失的信号

另一种形式的活跃性故障是丢失的信号。丢失的信号是指:线程必须等待一个已经为真的条件,但在开始等待之前没有检査条件谓词。现在,线程将等待一个已经发过的事件。这就好比在启动了烤面包机后出去拿报纸,当你还在屋外时烤面包机的铃声响了,但你没有听到,因此还会坐在厨房的桌子前等着烤面包机的铃声。你可能会等待很长的时间。通知并不像你涂在面包上的果酱,它没有“黏附性”。如果线程A通知了一个条件队列,而线程B随后在这个条件队列上等待,那么线程B将不会立即醒来,而是需要另一个通知来唤醒它。像上述程序清单中警示之类的编码错误(例如,没有在调用wait之前检测条件谓词)就会导致信号的丢失。

  • 通知

每当在等待一个条件时,一定要确保在条件谓词为真十通过某种方式发出通知。
在条件队列API中有两个发出通知的方法,即notify和notifyAll。无论调用哪一个,都必须持有与条件队列对象相关联的锁。在调用notify时,JVM会从这个条件队列上等待的多个线程中选择一个来唤醒,而调用notifyAll则会唤醒所有在这个条件队列上等待的线程。由于在调用notify或notifyAll时必须持有条件队列对象的锁,而如果这些等待中线程此时不能重新获得锁,那么无法从wait返回,因此发出通知的线程应该尽快地释放锁,从而确保正在等待的线程尽可能快地解除咀塞。

由于多个线程可以基于不同的条件谓词在同一个条件队列上等待,因此如果使用notify 而不是notifyAll,那么将是一种危险的操作,因为单一的通知很容易导致类似于信号丢失的问题。

只有同时满足一下两个条件时,才能用单一的notify而不是notifyAll:

  • 所有等待线程的类型相同。只有一个条件谓词与条件队列相关,并且每个线程在从wait返回后将执行相同的操作。
  • 单进单出。在条件变量上的每次通知,最多只能唤醒一个线程来执行。

三、显示的Condition对象

Condition是一种广义的内置条件队列。

public interface Condition {
    void await() throws InterruptedException;

    void awaitUninterruptibly();

    long awaitNanos(long var1) throws InterruptedException;

    boolean await(long var1, TimeUnit var3) throws InterruptedException;

    boolean awaitUntil(Date var1) throws InterruptedException;

    void signal();

    void signalAll();
}

内置条件队列存在一些缺陷。每个内置锁都只能有一个相关联的条件队列。这些因素都使得无法满足在使用notifyAIl时所有等待线程为同一类型的需求。如果想编写一个带有多个条件谓词的并发对象,或者想获得除了条件队列可见性之外的更多控制权,就可以使用显式的Lock和Condition而不是内置锁和条件队列,这是一种更灵活的选择。

一个Condition和一个Lock关联在一起,就像一个条件队列和一个内置锁相关联一样,要创建一个Condition,可以在相关联的Lock上调用Lock.newCondition方法。正如Lock比内置加锁提供了更为丰富的功能,Condition同样比内置条件队列提供了更丰富的功能:在每个锁上可存在多个等待、条件等待可以是可中断的或不可中断的、基于时限的等待,以及公平的或非公平的队列操作。

与内置条件队列不同的是,对于每个Lock,可以有任意数量的Condition对象。Condition对象继承了相关的Lock对象的公平性,对于公平的锁,线程会依照FIFO顺序从Condition.await中释放。

特别注意:在Condition对象中,与wait、notify和notifyAll方法对应的分别是await、signal和signalAll。但是,Condition对Object进行了扩展,因而它也包含wait和notify方法。一定要确保使用正确的版本–wait和signal。

4、AbstractQueuedSynchronizer

http://ifeve.com/introduce-abstractqueuedsynchronizer/

5、原子变量类

共有12个原子变量类,可分为4组:标量类(Scalar)、更新器类、数组类以及复合变量类。最常用的原子变量类就是标量类:AtomicInteger、AtomicLong、AtomicBoolean以及AtomicReference。

猜你喜欢

转载自blog.csdn.net/qq_27870421/article/details/90583736