zoukankan      html  css  js  c++  java
  • 《java并发编程实战》读书笔记11--构建自定义的同步工具,条件队列,Condition,AQS

    第14章 构建自定义的同步工具

    本章将介绍实现状态依赖性的各种选择,以及在使用平台提供的状态依赖机制时需要遵守的各项规则。

    14.1 状态依赖性的管理

    对于并发对象上依赖状态的方法,虽然有时候在前提条件不满足的情况下不会失败,但通常有一种更好的选择,即等待前提条件为真。依赖状态的操作可以阻塞知道可以继续执行。内置的条件队列可以使线程一直阻塞。

    首先介绍如何通过轮询与休眠等方式来(勉强地)解决状态依赖性问题。可阻塞的状态依赖操作的形式如程序14-1所示。

    构成前提条件的状态变量必须由对象锁来保护,从而使它们在测试前提条件的同时保持不变。如果前提条件尚未满足,就必须释放锁,以便其他线程可以修改对象的状态,否则,前提条件就永远无法变成真。在再次测试前提条件之前,必须重新获得锁。

    在生产者-消费者的设计中经常会使用想ArrayBlockingQueue这样的有界缓存。在有界缓存提供的put和take中都包含一个前提条件:不能从空缓存中取出元素,也不能将元素放入已满的缓存中。当前提条件为满足时,依赖状态的操作可以抛出一个异常或返回一个错误状态,也可以保持阻塞直到对象进入正确的状态。

    接下来介绍有界缓存的几种实现,其中采用不同的方法来处理前提条件失败的问题。

     

    14.1.1 示例:将前提条件的失败传递给调用者

    程序清单14-4给出了对take的调用——并不是很漂亮,尤其是当程序中有许多地方都调用put和take方法时。(因为调用者要不停的捕获处理异常,并且在每次缓存操作时都需要重试)

    如果将状态依赖性管理交给调用者管理,那么将导致一些功能无法实现,例如维持FIFO顺序,由于迫使调用者重试,因此失去了“谁先到达”的信息。

    14.1.2 示例:通过轮询与休眠来实现简单的阻塞

    这种通过轮询与休眠方式来实现阻塞操作的过程需要付出大量的努力。如果存在某种挂起线程的方法,并且这种方法能够确保当某个条件成真时线程立即醒来,那么将极大地简化实现工作。这正是条件队列实现的功能。

    14.1.3 条件队列

    使得一组线程(称之为等待线程)能够通过某种方式来等待特定的条件变成真。正如每个java对象都可以作为一个锁,每个对象同样可以作为一个条件队列。

    Object.wait会自动释放锁,并请求操作系统挂起当前线程,从而使其他线程能够获得这个锁并修改对象的状态。当被挂起的线程醒来时,它将在返回之前重新获得锁。

     

     

    14.2 使用条件队列

    条件队列使构建高效以及高响应的状态依赖类变得更容易,但同时也很容易被不正确的使用。

    14.2.1 条件谓词

    想要正确的使用条件队列,关键是找出对象在哪个条件谓词上等待。条件谓词是使某个操作成为状态依赖的前提条件。在有界缓存中,只有当缓存不为空时,take方法才能执行,否则必须等待。对take方法说,它的条件谓词就是“缓存不为空”。

    14.2.2 过早唤醒

     wait方法的返回不一定意味着线程正在等待的条件谓词已经变成真了。

    当执行控制重新进入调用wait代码时,它已经重新获取了与条件队列相关联的锁。但现在条件谓词未必为真,或许,在发出notifyAll时,条件谓词可能已经变成真,但在重新获取锁时将再次变为假。所以,每当线程从wait中唤醒时,都必须再次测试条件谓词。

    14.2.3 丢失的信号

    14.2.4 通知

    在缓存变为非空时,为了使take解除阻塞,必须确保在每条缓存变为非空的代码路径中都发出一个通知。在BoundedBuffer中,只有一条代码路径,即在put方法之后。因此,put在成功地将一个元素添加到缓存后,将调用notifyAll。同样,take在移除一个元素后也将调用notifyAll,向任何正在等待“不为满”条件的线程发出通知:缓存已经不满了。在条件队列API中发出通知的方法,即notify和notifyAll。无论调用哪一个,都必须持有与条件队列对象相关联的锁。在调用notify时,JVM会从这个条件队列上等待的多个线程选择一个来唤醒(wait挂起的是线程,不是条件队列对象),而notifyAll则会唤醒所有在这个条件队列上等待的线程。由于多个线程可以基于不同的条件谓词在同一个条件队列(对象)上等待,因此如果使用notify而不是notifyAll,那么将是一种危险的操作,因为单一的通知很容易导致类似于信号丢失的问题。

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

    i.所有等待线程的类型都相同  ii.单进单出(在条件变量上的每次通知,最多只能唤醒一个线程来执行)

    14.2.5 示例:阀门类

    在await中使用的条件谓词比测试isOpen复杂的多。因为如果当阀门打开时有N个线程正在等待它,那么这些线程都应该被允许执行。然而,如果阀门在打开后又非常快速的关闭了,并且await方法只检查isOpen,那么所有线程都可能无法释放:当所有线程收到通知时,将重新请求锁并退出wait,而此时的阀门可能已经再次关闭了。

    14.2.6 子类的安全问题

    14.2.7 封装条件队列

    通常,我们应该把条件队列封装起来,因而除了使用条件队列的类,就不能在其他地方访问它。

    14.2.8 入口协议与出口协议

     

    14.3 显示的Condition对象

    之所以要使用显示的Condition对象是因为内置条件队列的一些缺陷:

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

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

    下面来通过Condition实现有界缓存:

     

    14.4 Synchronized剖析

    ReentrantLock和Semaphore在实现时都使用了一个共同的基类,即AbstractQueuedSynchronized(AQS),这个类也是许多其他同步类的基类。AQS是一个用于构建锁和同步器的框架,许多同步器都可以通过AQS很容易并且高效地构造出来。

     

    14.5 AbstractQueueSynchronized

    在基于AQS构建的同步容器中,最基本的操作包括各种形式的获取操作和释放操作。获取操作是一种依赖状态操作,并且通常会阻塞。当使用锁或信号量时,“获取”操作的含义就很直观,即获取的是锁或者许可,并且调用者可能会一直等待直到同步容器类处于可被获取的状态。在使用CountDownLatch时,“获取”操作意味着“等待并知道闭锁到达结束状态”,而在使用FutureTask时,则意味着“等待并直到任务已经完成”。“释放”并不是一个可阻塞的操作,当执行“释放”操作时,所有在请求时被阻塞的线程都会开始执行。AQS负责管理同步容器类中的状态,它管理了一个整数信息,可以通过getState、setState以及compareAndSetState等protected类型方法来进行操作。这个整数可以用于表示任意状态。例如,ReentrantLock用它来表示所有者线程已经重复获取该锁的次数,Semaphore用它来表示剩余的许可数量,FutureTask用它来表示任务的状态。在同步容器类中还可以自行管理一些额外的状态变量,例如,ReentrantLock保存了锁的当前所有者信息,这样就能区分某个获取操作是重入的还是竞争的。

    如果某个同步容器支持独占的获取操作,那么需要实现一些保护方法,包括tryAcquire、tryRelease和isHeldExclusively等,而对于支持共享获取的同步器(如Semaphore),则应该实现tryAcquireShared和tryReleaseShared等方法。AQS中的acquire、acquireShared、release和releaseShared等方法都将调用这些方法在子类中带有前缀try的版本来判断某个操作是否能执行。

    一个简单的闭锁

    程序清单14-14中的OneShotLatch是一个使用AQS实现的二元闭锁。它包含两个共有方法:await和signal,分别对应获取操作和释放操作。

    在OneShotLatch中,AQS状态用来表示闭锁状态——关闭(0)或者打开(1)。await方法调用AQS的acquireSharedInterruptibly,然后接着调用OneShotLatch中的tryAcquireShared方法。在tryAcquireShared的实现中必须返回一个值来表示该操作能否执行。acquireSharedInterruptibly方法处理失败的方式是把这个线程放入等待线程队列中。

     

    14.6 java.util.concurrent同步器类中的AQS

    14.6.1 ReentrantLock

    ReentrantLock只支持独占方式的获取操作,因此它实现了tryAcquire、tryRelease和isHeldExeclusively,程序14-15给出了非公平版本的tryAcquire

    ReentrantLock将同步状态用于保存锁获取操作的次数,并且还维护一个owner变量来保存当前所有者线程的标识符,只有在当前线程刚刚获取到锁,或者正要释放锁的时候,才会修改这个变量。在tryRelease中检查owner域,从而确保当前线程在执行unlock操作之前已经获取了锁:在tryAcquire中将使用这个域来区分获取操作是重入的还是竞争的。由于状态可能在检查后被立即修改,因此tryAcquire使用compareAndSetState来原子地更新状态。

    14.6.2 Semaphore与CountDownLatch

    14.6.3 FutureTask

    14.6.4 ReentrantReadWriteLock


    小结:

  • 相关阅读:
    【NOIP 2003】 加分二叉树
    【POJ 1655】 Balancing Act
    【HDU 3613】Best Reward
    【POJ 3461】 Oulipo
    【POJ 2752】 Seek the Name, Seek the Fame
    【POJ 1961】 Period
    【POJ 2406】 Power Strings
    BZOJ3028 食物(生成函数)
    BZOJ5372 PKUSC2018神仙的游戏(NTT)
    BZOJ4836 二元运算(分治FFT)
  • 原文地址:https://www.cnblogs.com/f91og/p/7001179.html
Copyright © 2011-2022 走看看