AbstractQueuedSynchronizer(AQS)
Java并发包(JUC)中提供了很多并发工具,这其中,很多我们耳熟能详的并发工具,譬如ReentrangLock、Semaphore,它们的实现都用到了一个共同的基类–AbstractQueuedSynchronizer,简称AQS。AQS是一个用来构建锁和同步器的框架,使用AQS能简单且高效地构造出应用广泛的大量的同步器,比如我们提到的ReentrantLock,Semaphore,其他的诸如ReentrantReadWriteLock,SynchronousQueue,FutureTask等等皆是基于AQS的。当然,我们自己也能利用AQS非常轻松容易地构造出符合我们自己需求的同步器。
基本原理
AQS使用一个int成员变量来表示同步状态,通过内置的FIFO队列来完成获取资源线程的排队工作。
1 | private volatile int state;//共享变量,使用volatile修饰保证线程可见性 |
状态信息通过protected类型的getState,setState,compareAndSetState进行操作
1 | protected final int getState() |
AQS支持两种同步方式:
- 独占式
- 共享式
这样方便使用者实现不同类型的同步组件,独占式如ReentrantLock,共享式如Semaphore,CountDownLatch,组合式的如ReentrantReadWriteLock。总之,AQS为使用提供了底层支撑,如何组装实现,使用者可以自由发挥。
同步器的设计是基于模板方法模式的,一般的使用方式是这样:
1.使用者继承AbstractQueuedSynchronizer并重写指定的方法。(这些重写方法很简单,无非是对于共享资源state的获取和释放)
2.将AQS组合在自定义同步组件的实现中,并调用其模板方法,而这些模板方法会调用使用者重写的方法。
可能需要重新定义的protected method
1 | //独占模式 |
子类可以维护其他状态字段,但是只有使用方法getState、setState和compareAndSetState操纵的原子更新的{int state}值在同步方面被跟踪。AbstractQueuedSynchronizer的子类应该定义为非公共的内部助手类,用于实现其封闭类的同步属性。
思想
对于使用者来讲,我们无需关心获取资源失败,线程排队,线程阻塞/唤醒等一系列复杂的实现,这些都在AQS中为我们处理好了。我们只需要负责好自己的那个环节就好,也就是获取/释放共享资源state的姿势T_T。很经典的模板方法设计模式的应用,AQS为我们定义好顶级逻辑的骨架,并提取出公用的线程入队列/出队列,阻塞/唤醒等一系列复杂逻辑的实现,将部分简单的可由使用者决定的操作逻辑延迟到子类中去实现即可。
使用
首先,我们需要去继承AbstractQueuedSynchronizer这个类,然后我们根据我们的需求去重写相应的方法,比如要实现一个独占锁,那就去重写tryAcquire,tryRelease方法,要实现共享锁,就去重写tryAcquireShared,tryReleaseShared;最后,在我们的组件中调用AQS中的模板方法就可以了,而这些模板方法是会调用到我们之前重写的那些方法的。也就是说,我们只需要很小的工作量就可以实现自己的同步组件,重写的那些方法,仅仅是一些简单的对于共享资源state的获取和释放操作,至于像是获取资源失败,线程需要阻塞之类的操作,自然是AQS帮我们完成了。
源码
AQS维护了一个共享资源state,通过内置的FIFO队列完成排队的工作。这个队列是一个由Node节点组成的双向量表。AQS分别维护了它的一头一尾两个指针。
独占式
Node节点
Node节点是AQS里面的一个静态内部类
1 | static final class Node { |
里面这几个字段是比较重要的
字段 | 类型 | 含义 |
---|---|---|
CANCELLED | waitStatus取值范围之一 | 因为超时或者中断,节点会被设置为取消状态,被取消状态的节点不应该去竞争锁,只能保持取消状态不变,不能转为其他状态。处于这种状态的节点会被踢出队列,被GC回收。 |
SIGNAL | waitStatus取值范围之一 | 表示这个节点的继任节点被阻塞了,到时候需要通知它。 |
CONDITION | waitStatus取值范围之一 | 表示这个节点因为等待某个条件而被阻塞 |
PROPAGATE | waitStatus取值范围之一 | 使用在共享模式头结点有可能处于这种状态,表示锁的下一次获取可以无条件传播; |
waitStatus | int | 初始值为0,新节点的默认状态 |
prev | Node | 当前节点的前一个节点 |
next | Node | 当前节点的继任节点 |
thread | Thread | 与节点关联的排队中的线程 |
获取锁
接下来我们看看,acquire方法是怎么获取锁的
1 | /** |
上述方法主要做以下操作:
- 调用使用者重写的tryAcquire方法,如果返回true,表示获取同步状态成功,直接返回
- 如果获取同步状态失败,就构造独占式同步节点,通过addWaiter方法将此节点添加到同步队列的尾部
- 该节点在队列中尝试获取同步状态,如果获取不到,则阻塞节点线程,知道被前驱节点唤醒或者被中断
看一下addWaiter方法
1 | /** |
以CAS方式将当前节点加入到队列的尾部,如果失败了,就进入enq方法。
enq方法
1 | /** |
enq中有一个死循环,在这个循环中,一直用CAS方法取设置节点。直到成功为止。
接着我们在看看acquireQueued方法
1 | /** |
acquireQueued内部也是一个死循环,只有前驱结点是头结点的结点,也就是老二结点,才有机会去tryAcquire;若tryAcquire成功,表示获取同步状态成功,将此结点设置为头结点;若是非老二结点,或者tryAcquire失败,则进入shouldParkAfterFailedAcquire去判断判断当前线程是否应该阻塞,若可以,调用parkAndCheckInterrupt阻塞当前线程,直到被中断或者被前驱结点唤醒。若还不能休息,继续循环。
shouldParkAfterFailedAcquire
1 | /** |
若shouldParkAfterFailedAcquire返回true,也就是当前结点的前驱结点为SIGNAL状态,则意味着当前结点可以放心休息,进入parking状态了。parkAncCheckInterrupt阻塞线程并处理中断。
parkAndCheckInterrupt
1 | /** |
总的来说就是:
- 首先tryAcquire获取同步状态,成功则直接返回;否则,进入下一环节;
- 线程获取同步状态失败,就构造一个结点,加入同步队列中,这个过程要保证线程安全;
- 加入队列中的结点线程进入自旋状态,若是老二结点(即前驱结点为头结点),才有机会尝试去获取同步状态;否则,当其前驱结点的状态为SIGNAL,线程便可安心休息,进入阻塞状态,直到被中断或者被前驱结点唤醒。
释放锁
release方法
1 | /** |
调用实现类的tryRelease方法,如果成功了,就唤醒继任节点。
1 | /** |
如果继任节点为空或者状态为CANCEL,则从尾部往前遍历找到一个处于正常阻塞状态的节点,进行唤醒。
共享式
共享式:共享式地获取同步状态。对于独占式同步组件来讲,同一时刻只有一个线程能获取到同步状态,其他线程都得去排队等待,其待重写的尝试获取同步状态的方法tryAcquire返回值为boolean,这很容易理解;对于共享式同步组件来讲,同一时刻可以有多个线程同时获取到同步状态,这也是“共享”的意义所在。其待重写的尝试获取同步状态的方法tryAcquireShared返回值为int。
获取锁
1 | /** |
1.当返回值大于0时,表示获取同步状态成功,同时还有剩余同步状态可供其他线程获取;
2.当返回值等于0时,表示获取同步状态成功,但没有可用同步状态了;
3.当返回值小于0时,表示获取同步状态失败。
如果获取同步状态失败,则调用doAcquireShared方法
1 | /** |
大体逻辑与独占式的acquireQueued差距不大,只不过由于是共享式,会有多个线程同时获取到线程,也可能同时释放线程,空出很多同步状态,所以当排队中的老二获取到同步状态,如果还有可用资源,会继续传播下去。
setHeadAndPropagate
1 | /** |
释放锁
releaseShared
1 | /** |
1 | /** |
总结
AQS是JUC中很多同步组件的构建基础,简单来讲,它内部实现主要是状态变量state和一个FIFO队列来完成,同步队列的头结点是当前获取到同步状态的结点,获取同步状态state失败的线程,会被构造成一个结点(或共享式或独占式)加入到同步队列尾部(采用自旋CAS来保证此操作的线程安全),随后线程会阻塞;释放时唤醒头结点的后继结点,使其加入对同步状态的争夺中。
AQS为我们定义好了顶层的处理实现逻辑,我们在使用AQS构建符合我们需求的同步组件时,只需重写tryAcquire,tryAcquireShared,tryRelease,tryReleaseShared几个方法,来决定同步状态的释放和获取即可,至于背后复杂的线程排队,线程阻塞/唤醒,如何保证线程安全,都由AQS为我们完成了,这也是非常典型的模板方法的应用。AQS定义好顶级逻辑的骨架,并提取出公用的线程入队列/出队列,阻塞/唤醒等一系列复杂逻辑的实现,将部分简单的可由使用者决定的操作逻辑延迟到子类中去实现。