API翻译之AbstractQueuedSynchronizer

AbstractQueuedSynchronizer

  提供一个框架,用于实现依赖先进先出(FIFO)等待队列的阻塞锁和相关同步器(信号量、事件等)。该类被设计为大多数依赖于单个原子int值来表示状态的同步器的有用基础。子类必须定义改变该状态的受保护方法,以及这些方法定义了该状态在获取或释放该对象方面的含义。考虑到这些,该类中的其他方法执行所有队列和阻塞机制。子类可以维护其他状态字段,但是只有使用方法getStatesetStatecompareAndSetState对原子更新的int值进行同步跟踪。

  子类应该被定义为非公共的内部帮助类,用于实现其封闭类的同步属性。类AbstractQueuedSynchronizer不实现任何同步接口。相反,它定义了诸如acquireInterruptibly这样的方法,具体锁和相关同步器可以根据需要调用这些方法来实现它们的公共方法。

  这个类支持默认的\color{red}{独占的}模式和\color{red}{共享的}模式。在独占模式下获取时,其他线程试图获取的操作无法成功。多个线程获取的共享模式可能(但不需要)成功。这个类不“明白”除了机械意义上的差异之外,这些差异还包括:当共享模式获取成功时,下一个等待的线程(如果存在的话)也必须确定它是否能够获取。在不同模式下等待的线程共享同一个FIFO队列。通常,实现子类只支持其中一种模式,但这两种模式都可以发挥作用,例如在ReadWriteLock中。只支持独占模式或仅支持共享模式的子类不需要定义支持未使用模式的方法。

  这个类定义了一个嵌套的ConditionObject类,可以用作Condition由子类实现支持独占模式的方法isHeldExclusively报告同步是否只对当前线程持有,release方法调用与getState获取当前的值和acquire方法完全释放这个对象,鉴于这个保存的状态值,最终将该对象恢复到以前获取的状态。AbstractQueuedSynchronizer没有方法会创建这样的条件,所以如果不能满足这个约束,就不要使用它。ConditionObject的行为当然依赖于它的同步器实现的语义。

  该类为内部队列提供了检查、检测和监视方法,跟Condition对象的方法很相似。可以使用AbstractQueuedSynchronizer将这些同步机制按照需要导出到类中。

  该类的序列化只存储底层原子整数维护状态,因此反序列化的对象具有空线程队列。需要序列化的典型子类将定义一个readObject方法,该方法在反序列化时将其恢复到已知的初始状态。

使用

  如果要使用这个类作为同步器的基础,可以使用getStatesetState和/或compareAndSetState检查和/或修改同步状态,根据需要重新定义以下方法:
--tryAcquire
--tryRelease
--tryAcquireShared
--tryReleaseShared
--isHeldExclusively

  默认情况下,每个方法都会UnsupportedOperationException。这些方法的实现必须是内部线程安全的,并且通常应该是短的,而不是阻塞的。定义这些方法是使用这个类的only支持的方法。所有其他方法都声明为final,因为它们不能独立更改。

  您还可能发现从AbstractOwnableSynchronizer继承的方法对于跟踪拥有独占同步器的线程非常有用。我们鼓励您使用它们——这使得监视和诊断工具能够帮助用户确定哪些线程持有锁。

  即使这个类基于内部FIFO队列,它也不会自动执行FIFO获取策略。独占同步的核心形式为:

Acquire:
    while (!tryAcquire(arg)) {
        //enqueue thread if it is not already queued;
        //possibly block current thread;
    }
Release:
    if (tryRelease(arg))
        //unblock the first queued thread;

(共享模式类似,但可能涉及级联信号。)

  因为签入获取是在排队之前调用的,所以一个新的获取线程可能会“插入”在其他被阻塞和排队的线程之前。但是,如果需要,您可以通过内部调用一个或多个检查方法来定义tryAcquire和/或tryAcquireShared来禁止插入,从而提供一个"公平" FIFO获取顺序。特别是,如果hasQueuedPredecessors(一个专门为公平同步器设计的方法)返回true,那么大多数公平同步器可以定义tryAcquire来返回false。其他变化是可能的。

  对于默认的barging(也称为"贪心")策略,吞吐量和可伸缩性通常是最高的。虽然不能保证这是公平的或无中断的,但允许较早排队的线程在较晚排队的线程之前重新竞争,而且每次重新竞争都有公平的机会面对传入成功的线程。而且,在获得的同时不<转动>。通常情况下,在阻塞之前,它们可以执行tryAcquire的多次调用,其间穿插着其他计算。当独占同步只被短暂地持有时,这就提供了自旋的大部分好处,而当它不被持有时,就没有了大部分的不利因素。如果需要,您可以通过前面的调用来增强这种能力,通过“fast-path”检查来获取方法,可能会预先检查hasContended和/或hasQueuedThreads,只在同步器可能不存在竞争的情况下才能这样做。

  这个类为同步提供了一个高效且可伸缩的基础,部分原因是它将同步器的使用范围专门化为可以依赖于int状态、获取和释放参数以及内部FIFO等待队列的同步器。当这还不够时,您可以使用java.util.concurrent.atomic类、自定义的java.util.Queue类和LockSupport阻塞支持,从较低的级别构建同步器。

使用举例

  这是一个不可重入互斥锁类,它使用0表示未锁状态,1表示已锁状态。虽然不可重入锁并不严格要求记录当前的持有线程,但是这个类还是这样做了,以便更容易地监视使用情况。它还支持conditions,并公开了一种检测方法:

class Mutex implements Lock, java.io.Serializable {
    // 我们的内部帮助类
    private static class Sync extends AbstractQueuedSynchronizer {
        // 报告是否处于锁定状态
        protected boolean isHeldExclusively() {
            return getState() == 1;
        }

    // 如果状态为0,则获取锁
    public boolean tryAcquire(int acquires) {
        assert acquires == 1; // Otherwise unused
        if (compareAndSetState(0, 1)) {
            setExclusiveOwnerThread(Thread.currentThread());
            return true;
        }
        return false;
    }

    // 通过把状态设为0来释放锁
    protected boolean tryRelease(int releases) {
        assert releases == 1; // Otherwise unused
        if (getState() == 0) throw new IllegalMonitorStateException();
            setExclusiveOwnerThread(null);
            setState(0);
            return true;
        }
 
    // 提供一个 Condition
    Condition newCondition() { return new ConditionObject(); }
 
    // 反序列化
    private void readObject(ObjectInputStream s) throws IOException, ClassNotFoundException {
        s.defaultReadObject();
        setState(0); // reset to unlocked state
    }

 
    // sync对象完成所有的工作。我们只需要用它转发功能就行。
    private final Sync sync = new Sync();
 
    public void lock()                { sync.acquire(1); }
    public boolean tryLock()          { return sync.tryAcquire(1); }
    public void unlock()              { sync.release(1); }
    public Condition newCondition()   { return sync.newCondition(); }
    public boolean isLocked()         { return sync.isHeldExclusively(); }
    public boolean hasQueuedThreads() { return sync.hasQueuedThreads(); }
    public void lockInterruptibly() throws InterruptedException {
        sync.acquireInterruptibly(1);
    }
    public boolean tryLock(long timeout, TimeUnit unit)
        throws InterruptedException {
        return sync.tryAcquireNanos(1, unit.toNanos(timeout));
    }
}

  这是一个类似于java.util.concurrent.CountDownLatch的锁存器类。区别是它只需要一个signal来触发。因为锁存器是非独占的,它使用shared获取和释放方法。

class BooleanLatch {
    private static class Sync extends AbstractQueuedSynchronizer {
        boolean isSignalled() { return getState() != 0; }
        protected int tryAcquireShared(int ignore) {
            return isSignalled() ? 1 : -1;
        }
        protected boolean tryReleaseShared(int ignore) {
            setState(1);
            return true;
        }
    }
    private final Sync sync = new Sync();
    public boolean isSignalled() { return sync.isSignalled(); }
    public void signal()         { sync.releaseShared(1); }
    public void await() throws InterruptedException {
        sync.acquireSharedInterruptibly(1);
    }
}

AbstractQueuedSynchronizer.Node

  等待队列节点类。

  等待队列是CLH(Craig、Landin和Hagersten)锁队列的变体。CLH锁通常用于自旋锁。相反,我们使用它们来阻塞同步器,但使用相同的基本策略,即在其节点的前身中保存关于线程的一些控制信息。每个节点中的status字段跟踪线程是否应该阻塞。一个节点在其前驱节点释放时发出信号。否则,队列的每个节点都充当持有单个等待线程的特定通知样式的监视器。状态字段不控制线程是否被授予锁等。如果线程是队列中的第一个线程,它可以尝试获取。但第一个也并不能保证成功,它只给你竞争的权利。因此,当前发布的竞争者线程可能需要重新等待。

  要入队一个CLH锁,您需要将其原子拼接为新尾部。 要出列,您只需设置head字段。

      +------+  prev +-----+       +-----+
 head |      | <---- |     | <---- |     |  tail
      +------+       +-----+       +-----+

  插入CLH队列只需要对“尾部”进行单个原子操作,因此存在从未排队到排队的简单原子点划分。 同样,出列只涉及更新“头部”。 但是,节点需要更多的工作来确定他们的后驱是谁,部分是为了处理由于超时和中断而可能的取消。

  处理取消主要需要“prev”链接(未在原始CLH锁中使用)。 如果节点被取消,则其后驱(通常)重新链接到未取消的前驱。 有关自旋锁的类似机制的解释,请参阅Scott和Scherer的论文
http://www.cs.rochester.edu/u/scott/synchronization/

  我们还使用next链接来实现阻塞机制。 每个节点保存了自己所在线程ID,因此前驱通过遍历下一个链接来通知下一个节点以确定它是哪个线程。 后驱的确定必须避免使用新排队节点的竞争来设置其前驱的next字段。 必要时,当节点的后驱看起来为空时,通过从原子更新的tail向后检查来解决这个问题。 (或者,换句话说,下一个链接是一个优化,因此我们通常不需要向后扫描。)

  消除为基本算法引入了一些保守性。 由于我们必须轮询消除其他节点,我们可能会忽略已消除的节点是在我们前面还是在我们后面。 这一点通过消除时始终取消停车的继承人来处理,允许他们稳定在新的前驱,除非我们能够确定一位将承担此责任的未经解除的前驱。

  CLH队列需要一个虚拟标头节点才能开始。 但是我们不会在构造函数里创建它们,因为如果没有调用就会浪费资源。 相反,节点在第一次调用时被构造,并且设置头尾指针。

  Conditions等待的线程使用相同的节点,但使用不同链接。Conditions只需要链接简单(非并发)链接队列中的节点,因为它们仅在完全持有时才被访问。 等待时,将节点插入条件队列。 根据信号,节点被转移到主队列。 状态字段的特殊值用于标记节点所在的队列。

  感谢Dave Dice,Mark Moir,Victor Luchangco,Bill Scherer和Michael Scott以及JSR-166专家组成员对本课程设计的有益想法,讨论和批评。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,098评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,213评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,960评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,519评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,512评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,533评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,914评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,574评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,804评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,563评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,644评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,350评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,933评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,908评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,146评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,847评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,361评论 2 342

推荐阅读更多精彩内容