1、AQS介绍
AQS全称AbstractQueuedSynchronizer
,是一个同步器,用来构建锁或者其他同步组件的基础框架。内部主要使用一个volatile修饰的state变量和一个FIFO双向队列来实现的。
1 | java复制代码 /** |
ReentrantLock
、Semaphore
、CountDownLatch
等都是基于AQS实现的。
同步器的主要使用方式是继承,子类通过继承同步器并实现它的抽象方法来管理同步状 态,在抽象方法的实现过程中免不了要对同步状态进行更改,这时就需要使用同步器提供的3 个方法(getState()、setState(int newState)和compareAndSetState(int expect,int update))来进行操 作,因为它们能够保证状态的改变是安全的。
子类推荐被定义为自定义同步组件的静态内部类,同步器自身没有实现任何同步接口,它仅仅是定义了若干同步状态获取和释放的方法来 供自定义同步组件使用,同步器既可以支持独占式地获取同步状态,也可以支持共享式地获 取同步状态,这样就可以方便实现不同类型的同步组件(ReentrantLock、 ReentrantReadWriteLock和CountDownLatch等)
1 | java复制代码public class Main { |
同步器是实现锁(也可以是任意同步组件)的关键,在锁的实现中聚合同步器,利用同步器实现锁的语义。可以这样理解二者之间的关系:锁是面向使用者的,它定义了使用者与锁交 互的接口(比如可以允许两个线程并行访问),隐藏了实现细节;同步器面向的是锁的实现者,它简化了锁的实现方式,屏蔽了同步状态管理、线程的排队、等待与唤醒等底层操作。锁和同步器很好地隔离了使用者和实现者所需关注的领域。
2、同步器的接口与示例
同步器的设计是基于模板方法模式的,也就是说,使用者需要继承同步器并重写指定的方法,随后将同步器组合在自定义同步组件的实现中,并调用同步器提供的模板方法,而这些模板方法将会调用使用者重写的方法。 这句话听起来很绕,慢慢往后看就懂了。
我们在继承AQS并重写那5个方法的时候,需要调用AQS提供的几个方法去访问或修改stats变量的状态。
1 | java复制代码// 获取当前同步状态 |
我们在继承同步器时可重写的方法如下
在实现自定义同步组件时,也会调用同步器提供的模板方法,部分模板方法如下
AQS提供的模板方法主要分为三类:
(1)独占式获取和释放同步状态。
(2)共享式获取和释放同步状态。
(3)查询同步队列中等待的线程情况。
3、实现一个独占锁
独占锁,就是同一时刻只能一个线程拥有锁。基于AQS,我们可以很方便的实现。
1 | java复制代码import java.util.concurrent.TimeUnit; |
一般在实现自定义同步器时,都会把它作为静态内部类去实现。上面实现的ExclusiveLock类中就是定义了一个静态内部类Sync去继承AQS实现独占锁逻辑的。通过CAS设置同步变量state值为1表示获取锁成功,释放锁时把state设置为0即可。
4、AQS的实现分析
4.1 同步队列
AQS内部依赖同步队列(双向FIFO队列)来进行线程的管理,当线程获取锁失败时,会将线程以及等待状态信息构造为一个节点Node并将其放入同步队列队尾,然后阻塞该线程。当锁释放的时候,会把队首节点中的线程唤醒,使其再次尝试获取同步状态。
Node是AQS中的一个静态内部类,用来保存获取同步状态失败的线程引用、等待状态以及前驱和后继节点,主要字段如下
1 | java复制代码static final class Node { |
没有成功获取同步状态的线程将会加入同步队列的尾部,这个加入的过程也必须要保证线程安全,因此AQS提供了compareAndSetTail(Node expect, Node update)
方法。同步队列的结构大致如下
同步队列设置尾结点的过程大致如下
同步队列的首节点是获取锁成功的线程,首节点的线程在释放锁后,将会唤醒后继节点,后继节点如果获取锁成功的同时会把自己设置为头结点
设置头结点是通过获取同步状态成功的线程来完成的,由于只有一个线程能成功获取到同步状态,因此设置头结点的方法并不需要CAS来保证,它只需要将首节点设置为头结点的后继节点然后断开首节点即可。
4.2 独占式同步状态获取和释放
我们在实现自定义的独占式同步器时,主要重写了AQS的tryAcquire
和tryRelease
方法,通过操作同步变量state完成同步状态的获取和释放。
我们可以调用AQS对外提供的acquire
获取同步状态,该方法会调用我们重新的tryAcquire
方法。
1 | java复制代码public final void acquire(int arg) { |
如果调用tryAcquire
方法返回为false,则通过addWaiter
把线程加入同步队列队尾,并标志位独占Node.EXCLUSIVE
。通过CAS确保节点能安全的添加到队列尾。
1 | java复制代码private Node addWaiter(Node mode) { |
加入队列尾后,通过CAS不断的尝试获取同步状态。只有在前驱节点是头结点的情况下,才有可能获取到同步状态。如果获取不到则阻塞节点中的线程,而被阻塞线程的唤醒主要依靠前驱节点的出队或阻塞线程被中断来实现。
1 | java复制代码final boolean acquireQueued(final Node node, int arg) { |
在acquireQueued(final Node node,int arg)
方法中,当前线程在“死循环”中尝试获取同步状态,而只有前驱节点是头节点才能够尝试获取同步状态,这是为什么?原因有两个,如下。
(1)头节点是成功获取到同步状态的节点,而头节点的线程释放了同步状态之后,将会唤醒其后继节点,后继节点的线程被唤醒后需要检查自己的前驱节点是否是头节点。
(2)维护同步队列的FIFO原则。
独占式同步状态获取流程,也就是acquire(int arg)方法调用流程大致如下
上图中,当同步状态获取成功之后,当前线程从acquire(int arg)方法返回,如果对于锁这种并发组件而言,代表着当前线程获取了锁。
当线程获取同步状态并执行完相应的逻辑后,就需要释放同步状态,通过调用AQS提供的release
方法。该方法在释放了同步状态之后,会唤醒其后继节点(进而使后继节点重新尝试获取同步状态)。
1 | java复制代码public final boolean release(int arg) { |
该方法执行时,会唤醒头节点的后继节点线程,unparkSuccessor(Node node)方法使用LockSupport来唤醒处于等待状态的线程。
1 | java复制代码private void unparkSuccessor(Node node) { |
分析了独占式同步状态获取和释放过程后,适当做个总结:在获取同步状态时,同步器维护一个同步队列,获取状态失败的线程都会被加入到队列中并在队列中进行自旋;移出队列(或停止自旋)的条件是前驱节点为头节点且成功获取了同步状态。在释放同步状态时,同步器调用tryRelease(int arg)方法释放同步状态,然后唤醒头节点的后继节点。
4.3 共享式同步状态获取和释放
共享式获取与独占式获取最主要的区别在于同一时刻能否有多个线程同时获取到同步状态。以文件的读写为例,如果一个程序在对文件进行读操作,那么这一时刻对于该文件的写操作均被阻塞,而读操作能够同时进行。写操作要求对资源的独占式访问,而读操作可以是共享式访问。
通过调用同步器的acquireShared(int arg)方法可以共享式地获取同步状态
1 | java复制代码public final void acquireShared(int arg) { |
在acquireShared
方法中,会调用我们重写的tryAcquireShared
方法尝试获取同步状态,该方法返回值为int,返回值大于等于0时,表示获取成功。因此,在共享式获取的自旋过程中,成功获取到同步状态并退出自旋的条件就是tryAcquireShared(int arg)
方法返回值大于等于0。可以看到,在doAcquireShared(int arg)方法的自旋过程中,如果当前节点的前驱为头节点时,尝试获取同步状态,如果返回值大于等于0,表示该次获取同步状态成功并从自旋过程中退出。
与独占式一样,共享式获取也需要释放同步状态,通过调用releaseShared(int arg)
方法可以释放同步状态
1 | java复制代码public final boolean releaseShared(int arg) { |
该方法在释放同步状态之后,将会唤醒后续处于等待状态的节点。对于能够支持多个线程同时访问的并发组件(比如Semaphore),它和独占式主要区别在于tryReleaseShared(int arg)
方法必须确保同步状态(或者资源数)线程安全释放,一般是通过循环和CAS来保证的,因为释放同步状态的操作会同时来自多个线程。
4.4 超时获取同步状态
通过调用同步器的doAcquireNanos(int arg,long nanosTimeout)
和doAcquireSharedNanos(int arg, long nanosTimeout)
方法可以超时获取同步状态,即在指定的时间段内获取同步状态,如果获取到同步状态则返回true,否则,返回false。该方法提供了传统Java同步操作(比如synchronized关键字)所不具备的特性。
1 | java复制代码private boolean doAcquireNanos(int arg, long nanosTimeout) |
大致流程如下
本文转载自: 掘金