AbstractQueuedSynchronizer(一)

应该将子类定义为非公共内部帮助器类,一般并发包类用内部类Sync sync来继承并实现。为实现依赖于先进先出 (FIFO) 等待队列的阻塞锁和相关同步器(信号量、事件,等等)提供一个框架。此类的设计目标是成为依靠单个原子 int 值来表示状态的大多数同步器的一个有用基础。子类必须定义重写此状态的受保护方法,并定义哪种状态对于此对象意味着被获取或被释放。假定这些条件之后,此类中的其他方法就可以实现所有排队和阻塞机制。子类可以维护其他状态字段,但只是为了获得同步而只追踪使用
getState()、setState(int) 和compareAndSetState(int, int)
方法来操作以原子方式更新的 int 值。

简单说来,AbstractQueuedSynchronizer会把所有的请求线程构成一个CLH队列,当一个线程执行完毕(lock.unlock())时会激活自己的后继节点,但正在执行的线程并不在队列中,而那些等待执行的线程全部处于阻塞状态,线程的显式阻塞是通过调用LockSupport.park()完成,而LockSupport.park()则调用 sun.misc.Unsafe.park()本地方法,再进一步,HotSpot在Linux中中通过调用pthread_mutex_lock函数把 线程交给系统内核进行阻塞。

此类支持默认的独占
模式和共享
模式之一,或者二者都支持。处于独占模式下时,其他线程试图获取该锁将无法取得成功。在共享模式下,多个线程获取某个锁可能(但不是一定)会获得成功。此类并不"了解"这些不同,除了机械地意识到当在共享模式下成功获取某一锁时,下一个等待线程(如果存在)也必须确定自己是否可以成功获取该锁。处于不同模式下的等待线程可以共享相同的 FIFO 队列。通常,实现子类只支持其中一种模式,但两种模式都可以在(例如)ReadWriteLock
中发挥作用。只支持独占模式或者只支持共享模式的子类不必定义支持未使用模式的方法。

此类通过支持独占模式的子类定义了一个嵌套的AbstractQueuedSynchronizer.ConditionObject
类,可以将这个类用作
Condition
实现。isHeldExclusively()
方法将报告同步对于当前线程是否是独占的;使用当前getState()
值调用
release(int)
方法则可以完全释放此对象;如果给定保存的状态值,那么
acquire(int)
方法可以将此对象最终恢复为它以前获取的状态。没有别的 AbstractQueuedSynchronizer 方法创建这样的条件,因此,如果无法满足此约束,则不要使用它。AbstractQueuedSynchronizer.ConditionObject
的行为当然取决于其同步器实现的语义。

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

同步器拥有三个成员变量:sync队列的头结点head、sync队列的尾节点tail和状态state。对于锁的获取,请求形成节点,将其挂载在尾部,而锁资源的转移(释放再获取)是从头部开始向后进行。对于同步器维护的状态state,多个线程对其的获取将会产生一个链式的结构。

使用

为了将此类用作同步器的基础,需要适当地重新定义以下方法,这是通过使用getState()、setState(int)
和/或compareAndSetState(int, int)
方法来检查和/或修改同步状态来实现的:


Protected子类实现


解释


tryAcquire(int)


独占模式获取。先尝试获取,如果不成功入队,等待其它线程release信号。用于Lock的tryLock实现。


tryRelease(int)


独占模式释放状态


tryAcquireShared(int)


共享模式获取尝试。先查询对象的状态是否允许共享,然后尝试获取。获取失败,入队等待其它线程释放信号。


tryReleaseShared(int)


共享的模式下释放状态


Boolean isHeldExclusively()


如果当前调用线程独占,返回true。

这个方法被调用,在每次调用非等待conditionobject方法。

默认情况下,每个方法都抛出UnsupportedOperationException。这些方法的实现在内部必须是线程安全的,通常应该很短并且不被阻塞。子类通过实现这些方法来实现不同业务,其他所有方法都被声明为 final。

开始提到同步器内部基于一个FIFO队列,对于一个独占锁的获取和释放有以下伪码可以表示。

AbstractOwnableSynchronizer的主要方法是set/get当前独占线程。

即使此类基于内部的某个 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;

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

CLH锁对应CLH的详细介绍请参考此片论文。

AbstractQueuedSynchronizer的实现也是部分基于"CLH"队列的思想。具体见AbstractQueuedSynchronizer的doAcquireSharedInterruptibly方法实现AbstractQueuedSynchronizer 为实现依赖于先进先出 (FIFO) 等待队列的阻塞锁定和相关同步器(信号量、事件,等等)提供一个框架。用java的人都知道synchronized能够对一个需要确保线程安全的对象,方法实现多线程并发控制,这是在java语法层次的实现,而AbstractQueuedSynchronizer 则是在应用层次而不是语法层次(更高的层次)提供了实现多线程并发控制组件的基础。

等待队列内部节点的实现

入队操作


分类


static final常量


解释


模式

Node mode


SHARED = new Node();


标志当前线程为共享模式


EXCLUSIVE = null;


独占模式


状态

Int waitStatus


CANCELLED = 1;


线程已经取消


SIGNAL = -1;


等待唤醒,也就是unpark


CONDITION = -2;


等待条件执行,也就是在condition队列中


PROPAGATE = -3;


表示当前场景下后续的acquireShared能够得以执行;


0


以上数值均无数值排列以简化使用。非负值意味着节点不需要信号。因此,大多数代码不需要检查特定的值,只是为了签名。字段为正常同步节点初始化为0,条件节点的条件为。它被修改使用CAS(或在可能的情况下,无条件写立即可见)。

状态字段,只能使用下列值:

  1. SIGNAL :这个节点的继承人(或即将)阻塞(通过park),所以当前节点必须启动它的继任者时,它释放或取消。为了避免竞争,获取方法必须首先指示它们需要一个信号,然后重试原子获取,然后,在失败,阻塞。
  2. CANCELLED :此节点由于超时或中断而被取消。节点永远不会离开这个状态。特别是,取消节点的线程永远不会再次阻塞。条件:此节点当前处于条件队列中。它将不会被用作一个同步队列节点,直到被转移,此时状态将被设置为0。(这个值什么都不做但简化操作。)
  3. PROPAGATE :一个releaseshared传播应该传播到其他节点。这是一套(头节点)在doreleaseshared确保传播下去,即使其他业务已经介入。


    Node prev


    前驱节点,比如当前节点被取消,那就需要前驱节点和后继节点来完成连接。指向当前节点的前驱,依赖于检查WaitStatus(线程状态)。入队时指定,出队时指向null(方便GC)。此外,在取消一个前任,我们一直找一个非取消的,这肯定存在,因为头节点永远不会取消:只有成功获得的节点成为头节点。取消的线程将不会成功获取,线程仅取消自身,不影响其他节点。


    Node next


    后继:当前节点因release而被唤醒后,链接到当前节点的后继节点。在调整分配入队,当旁路取消前任和清零(为了GC)当出列。入队操作并不指定next,所以看到next空并不一定意味着节点在终结队列。可以通过从tail遍历前驱做双重检查。取消节点的下一个字段设置为指向本身而非空,make life easier for isOnSyncQueue。如下:


    Node nextWaiter


    存储condition队列中的后继节点。链接到下一个节点等待条件,或共享特殊值。因为只有在独占模式下访问条件队列时,在等待条件时我们只需要一个简单的链接队列来保持节点。然后他们被转移到队列重新获得。由于条件只能是互斥的,我们通过保存特殊值来表示共享模式。


    Thread thread


    入队列时的当前线程。

    mode Node.EXCLUSIVE for exclusive, Node.SHARED for shared

    上述逻辑主要包括:
    1. 使用当前线程构造Node;
    对于一个节点需要做的是将当节点前驱节点指向尾节点(current.prev = tail),尾节点指向它(tail = current),原有的尾节点的后继节点指向它(t.next = current)而这些操作要求是原子的。上面的操作是利用尾节点的设置来保证的,也就是compareAndSetTail来完成的。
    2. 先行尝试在队尾添加;
    如果尾节点已经有了,然后做如下操作:
    (1)分配引用T指向尾节点;
    (2)将节点的前驱节点更新为尾节点(current.prev = tail);
    (3)如果尾节点是T,那么将当尾节点设置为该节点(tail = current,原子更新);
    (4)T的后继节点指向当前节点(T.next = current)。
    注意第3点是要求原子的。
    这样可以以最短路径O(1)的效果来完成线程入队,是最大化减少开销的一种方式。
    3. 如果队尾添加失败或者是第一个入队的节点。
    如果是第1个节点,也就是sync队列没有初始化,那么会进入到enq这个方法,进入的线程可能有多个,或者说在addWaiter中没有成功入队的线程都将进入enq这个方法。
    可以看到enq的逻辑是确保进入的Node都会有机会顺序的添加到sync队列中,而加入的步骤如下:
    (1)如果尾节点为空,那么原子化的分配一个头节点,并将尾节点指向头节点,这一步是初始化;
    (2)然后是重复在addWaiter中做的工作,但是在一个while(true)的循环中,直到当前节点入队为止。
    进入sync队列之后,接下来就是要进行锁的获取,或者说是访问控制了,只有一个线程能够在同一时刻继续的运行,而其他的进入等待状态。而每个线程都是一个独立的个体,它们自省的观察,当条件满足的时候(自己的前驱是头结点并且原子性的获取了状态),那么这个线程能够继续运行。

    在共享模式下获取,忽略中断。通过首先调用至少一次tryacquireshared实施成功,返回。否则,线程队列,可能重复blocking 和unblocking,调用tryacquireshared直到成功。

    参数:参数arg的获得。这个值是给tryacquireshared但另有解释,可以代表任何你喜欢的东西。

    当前线程封装为node,添加共享队列

    这里省略中断判断,

    如果失败取消

    设置队列的头,并检查是否继承人可能在共享模式等待,或者如果传播>0或PROPAGATE 状态被设置。

    尝试队列下一个节点信号:

这两项检查中的保守主义可能会导致不必要的唤醒,但只有当有多个获得/释放,所以大多数需要信号,现在或很快无论如何。

上述逻辑主要包括:
1. 尝试获取共享状态;
调用tryAcquireShared来获取共享状态,该方法是非阻塞的,如果获取成功则立刻返回,也就表示获取共享锁成功。
2. 获取失败进入sync队列;
在获取共享状态失败后,当前时刻有可能是独占锁被其他线程所把持,那么将当前线程构造成为节点(共享模式)加入到sync队列中。
3. 循环内判断退出队列条件;
如果当前节点的前驱节点是头结点并且获取共享状态成功,这里和独占锁acquire的退出队列条件类似。
4. 获取共享状态成功;
在退出队列的条件上,和独占锁之间的主要区别在于获取共享状态成功之后的行为,而如果共享状态获取成功之后会判断后继节点是否是共享模式,如果是共享模式,那么就直接对其进行唤醒操作,也就是同时激发多个线程并发的运行。
5. 获取共享状态失败。
通过使用LockSupport将当前线程从线程调度器上摘下,进入休眠状态。
对于上述逻辑中,节点之间的通知过程如下图所示:

上图中,绿色表示共享节点,它们之间的通知和唤醒操作是在前驱节点获取状态时就进行的,红色表示独占节点,它的被唤醒必须取决于前驱节点的释放,也就是release操作,可以看出来图中的独占节点如果要运行,必须等待前面的共享节点均释放了状态才可以。而独占节点如果获取了状态,那么后续的独占式获取和共享式获取均被阻塞。

时间: 2024-10-03 07:57:21

AbstractQueuedSynchronizer(一)的相关文章

再谈AbstractQueuedSynchronizer:共享模式与基于Condition的等待/通知机制实现

共享模式acquire实现流程 上文我们讲解了AbstractQueuedSynchronizer独占模式的acquire实现流程,本文趁热打铁继续看一下AbstractQueuedSynchronizer共享模式acquire的实现流程.连续两篇文章的学习,也可以对比独占模式acquire和共享模式acquire的区别,加深对于AbstractQueuedSynchronizer的理解. 先看一下共享模式acquire的实现,方法为acquireShared和acquireSharedInte

4.从AbstractQueuedSynchronizer(AQS)说起(3)——AQS结语

前两节的内容<2.从AbstractQueuedSynchronizer(AQS)说起(1)——独占模式的锁获取与释放> .<3.从AbstractQueuedSynchronizer(AQS)说起(2)——共享模式的锁获取与释放>对AQS同步器已经有了一个大致的了解,从独占模式和共享模式来解析了AQS的两个基本方面,一个是同步状态的获取,另外一个是同步状态的释放,这是AQS最基本的特性,前面两节都是以阻塞的形式获取同步状态,但实际上AQS还能超时等待获取同步状态,或者非阻塞的方式

Java显式锁学习总结之二:使用AbstractQueuedSynchronizer构建同步组件

Jdk1.5中包含了并发大神Doug Lea写的并发工具包java.util.concurrent,这个工具包中包含了显示锁和其他的实用同步组件.Doug Lea在构建锁和组件的时候,大多是以队列同步器(AbstractQueuedSynchronizer)为基础的,因此AbstractQueuedSynchronizer可以看作是并发包的基础框架.因此掌握了AbstractQueuedSynchronizer的实现原理,也就掌握了大多数并发组件的实现原理. AbstractQueuedSync

AbstractQueuedSynchronizer源码分析

/* * ORACLE PROPRIETARY/CONFIDENTIAL. Use is subject to license terms. * * * * * * * * * * * * * * * * * * * * */ /* * * * * * * Written by Doug Lea with assistance from members of JCP JSR-166 * Expert Group and released to the public domain, as expl

AbstractQueuedSynchronizer的介绍和原理分析(转)

简介 提供了一个基于FIFO队列,可以用于构建锁或者其他相关同步装置的基础框架.该同步器(以下简称同步器)利用了一个int来表示状态,期望它能够成为实现大部分同步需求的基础.使用的方法是继承,子类通过继承同步器并需要实现它的方法来管理其状态,管理的方式就是通过类似acquire和release的方式来操纵状态.然而多线程环境中对状态的操纵必须确保原子性,因此子类对于状态的把握,需要使用这个同步器提供的以下三个方法对状态进行操作: java.util.concurrent.locks.Abstra

通过ReentrantLock源代码分析AbstractQueuedSynchronizer独占模式

1. 重入锁的概念与作用       reentrant 锁意味着什么呢?简单来说,它有一个与获取锁相关的计数器,如果已占有锁的某个线程再次获取锁,那么lock方法中将计数器就加1后就会立刻返回.当释放锁时计数器减1,若计数器不为0,说明线程仍然占有锁:若计数器值为0,线程才会真正释放锁. 可重入锁可以避免同一个线程嵌套(或者说递归)获取锁时的死锁现象. 考虑下面这样一种情况 public class LockAnalysis { private Lock l = new ReentrantLo

读AbstractQueuedSynchronizer类源码

感受 这个类的代码除去注释差不多有千多行,要想把所有代码都读完,然后按照作者的思路给理解完,是不容易的.这里我仔仔细细读了差不多一半的代码,说难倒不是很难. 虽然没有完全看完,但是基本上理解了作者代码的意图..说得简单些,就是操作一个双向链表.而链表中的每个节点有多种状态.AQS就是要保证整个双向链表和节点的状态的正确性. 连续看了好几天的JUC相关的源码,现在脑袋真有点晕乎乎的.加上这个类的代码确实有点多,这个就不在整理这个类的分析结果,就直接把代码copy了,并附上一张图.(这里只贴出分析过

AbstractQueuedSynchronizer AQS框架源码剖析

目录 1.引子 2.AQS架构设计原理 3.AQS源码实现 4.简单应用 5.总结 ====正文分割线============ 一.引子 Doug Lea在JSR166中建立了一个小框架,AbstractQueuedSynchronizer同步器框架(AQS).这个框架为构造同步器提供一种通用的机制,并且被j.u.c包中大部分类使用. 包结构如下图,其中AbstractOwnableSynchronizer是其父类,而AbstractQueuedLongSynchronizer是其32位状态的升

再谈AbstractQueuedSynchronizer:独占模式

关于AbstractQueuedSynchronizer JDK1.5之后引入了并发包java.util.concurrent,大大提高了Java程序的并发性能.关于java.util.concurrent包我总结如下: AbstractQueuedSynchronizer是并发类诸如ReentrantLock.CountDownLatch.Semphore的核心 CAS算法是AbstractQueuedSynchronizer的核心 可以说AbstractQueuedSynchronizer是

Java并发基础框架AbstractQueuedSynchronizer初探(ReentrantLock的实现分析)

AbstractQueuedSynchronizer是实现Java并发类库的一个基础框架,Java中的各种锁(RenentrantLock, ReentrantReadWriteLock)以及同步工具类(Semaphore, CountDownLatch)等很多都是基于AbstractQueuedSynchronizer实现的.AbstractQueuedSynchronizer 一般简称AQS,Abstract表示他是一个抽象类,Queued表示他是基于先进先出 FIFO 等待队列实现的,Sy