并发包学习(三)-AbstractQueuedSynchronizer总结
J.U.C学习的第二篇AQS。AQS在Java并发包中的重要性,毋庸置疑,所以单独拿出来理一理。本文参考总结自《Java并发编程的艺术》第五章第二节队列同步器。
什么是AbstractQueuedSynchronizer?
AbstractQueuedSynchronizer是JUC并发包中锁的底层支持,AbstractQueuedSynchronizer是抽象同步队列,简称AQS,是实现同步器的基础组件,并发包中锁的实现底层就是使用AQS实现,当然大多数人不会直接用到AQS,但是学习这个类对并发包的底层理解还是有莫大的帮助的。
AQS中维持了一个单一的状态信息state,可以通过getState,setState,compareAndSetState 函数修改其值,AQS内部维持一个FIFO队列来完成资源获取线程的排队工作。对于ReentrantLock 的实现来说,state 可以用来表示当前线程获取锁的可重入次数;
对应读写锁ReentrantReadWriteLock 来说state 的高16位表示读状态,也就是获取该读锁的次数,低 16位 表示获取到写锁的线程的可重入次数;对于FuterTask 来说,state用来表示任务状态(例如,还没开始,运行,完成,取消);
对应CountDownlatch 和CyclicBarrie 来说,state用来表示计数器当前的值。
AQS有个内部类ConditionObject 是用来结合锁实现线程同步,ConditionObject可以直接访问AQS对象内部的变量,比如 state 状态值 和AQS队列;
ConditionObject是条件变量,每个条件变量对应一个条件队列(单向链表队列),用来存放调用条件变量的await()方法后被阻塞的线程。
对于AQS 来说,线程同步的关键是对状态值state进行操作,根据state是否属于一个线程,操作state的方式分为独占模式和共享模式。
独占模式下获取和释放资源使用方法的源码如下:
void acquire(int arg) void acquireInterruptibly(int arg) boolean release(int arg)
共享模式下获取和释放资源方法的源码如下:
void acquireShared(int arg) void acquireSharedInterruptibly(int arg) boolean releaseShared(int arg)
另外还有个查询同步队列等待线程情况的方法如下:
Collection<Thread> getQueuedThreads()
对于独占锁方式获取的资源是与具体线程绑定的,也就是说如果一个线程获取到了资源,就会标记是那个线程获取到的,其他线程尝试操作state获取资源时候发现当前该资源不是自己持有,就会获取失败后被阻塞;
比如独占锁ReentrantLock的实现,当一个线程获取了ReentrantLock的锁后,AQS内部会首先使用CAS操作把state状态从0 变成 1,然后设置当前锁的持有者为当前线程,当该线程再次获取锁的时候,发现当前线程就是锁的持有者,则会把state状态值从1变成2,
也就是设置可重入次数,当另外一个线程获取锁的时候发现自己并不是该锁的持有者就会被放入AQS阻塞队列后挂起。
对于共享操作方式资源是与具体线程不相关的,多个线程去请求资源时候是通过CAS方式竞争获取资源,当一个线程获取到了资源后,另外一个线程再次获取时候,如果 当前资源还能满足它的需要,则当前线程只需要使用CAS方式进行获取即可,
共享模式下并不需要记录哪个线程获取了资源;比如 Semaphore 信号量,当一个线程通过acquire()方法获取一个信号量时候,会首先看当前信号两个数是否满足需要,不满足则把当前线程放入阻塞队列,如果满足则通过自旋CAS获取信号量。
队列同步器的实现分析
1、同步队列
同步队列即当线程获取同步状态失败,同步器会将当前线程以及等待信息构成节点Node加入同步队列,同时阻塞当前线程,当同步状态释放,会把首节点的线程唤醒,使其再次尝试获取同步状态。
节点是构成同步队列的基础,同步器拥有首节点head和尾节点tail,获取状态失败的线程会加入队列的尾部,结构如图:
同步队列遵循FIFO,首节点是获取同步状态成功的节点,首节点的线程在释放同步状态时,将会唤醒后继节点,后继节点将会在获取同步状态成功时将自己设置为首节点,如下图
2、独占式同步状态获取与释放
调用acquire(int arg)方法获取同步状态,当获取失败进入同步队列,后续线程中断,也不会从同步队列移出。
public final void acquire(int arg) { if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt(); }
首先我们调用acquire(int arg)方法后,然后调用自定义同步器实现的tryAcquire方法 尝试获取同步状态,具体是设置状态变量state的值,成功则直接返回。失败则将当前线程封装为类型Node.EXCLUSIVE 的Node节点,并调用addWaiter(Node node)方法将该节点插入到AQS阻塞队列尾部,最后调用acquireQueued(Node node,int arg)方法,使得该节点以“死循环”的方式获取同步状态,如果获取不到则阻塞节点线程,而被阻塞的线程的唤醒主要依靠前驱节点的出队或被中断来实现。
private Node addWaiter(Node mode) { Node node = new Node(Thread.currentThread(), mode); // Try the fast path of enq; backup to full enq on failure Node pred = tail; if (pred != null) { node.prev = pred; if (compareAndSetTail(pred, node)) { pred.next = node; return node; } } enq(node); return node; }
private Node enq(final Node node) { for (;;) { Node t = tail; if (t == null) { // Must initialize if (compareAndSetHead(new Node())) tail = head; } else { node.prev = t; if (compareAndSetTail(t, node)) { t.next = node; return t; } } } }
上述尾节点的添加代码可知,使用了CAS的方式保证了尾节点的安全添加,避免了在并发的情况下节点数量偏差或者顺序混乱的情况。
final boolean acquireQueued(final Node node, int arg) { boolean failed = true; try { boolean interrupted = false; for (;;) { final Node p = node.predecessor(); if (p == head && tryAcquire(arg)) { setHead(node); p.next = null; // help GC failed = false; return interrupted; } if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) interrupted = true; } } finally { if (failed) cancelAcquire(node); } }
当前线程以“死循环”的方式获取同步状态,而只有前驱节点是头节点才能尝试获取同步状态,这又是为什么呢,原因有两点。
第一,头节点是成功获取到同步状态的节点,而头节点释放状态后,将唤醒后继节点,后继节点被唤醒后也需要检查自己的前驱节点是否是头节点。
第二,维护同步队列的FIFO原则。
独占式的获取同步状态的流程如下:
当一个线程获取同步状态并执行完相关逻辑后,就需要释放同步状态,使得后续节点能够获取,具体代码如下:
public final boolean release(int arg) { if (tryRelease(arg)) { Node h = head; if (h != null && h.waitStatus != 0) unparkSuccessor(h); return true; } return false; }
该方法会在释放同步状态后,唤醒后继节点,unparkSuccessor(Node node)方法使用LockSupport来唤醒等待的线程。
总结:在获取同步状态时,同步器维护了一个同步队列,获取状态失败的线程都会被加入到队列并在队列自旋;移除队列的条件是前驱节点为头节点且成功获取同步状态。在释放同步状态时,同步器调用tryRelease(int arg)方法释放同步状态,然后唤醒头节点的后继节点。
这里需要注意的是AQS类并没有提供可用的tryAcquire 和 tryRelease,正如AQS是锁阻塞和同步容器的基础框架,是抽象类,tryAcquire和 tryRelease 需要有具体的子类来实现的。
子类在实现tryAcquire 和 tryRelease 时候要根据具体场景使用CAS算法尝试修改该状态值state,成功则返回true,否则返回false。子类还需要定义在调用acquire 和 release 方法时候 state 状态值的增减代表什么含义。
比如继承自AQS实现的独占锁ReentrantLock,定义当status为0的时候标示锁空闲,为1 的时候标示锁已经被占用,在重写tryAcquire的时候,内部需要使用CAS算法看当前status是否为0,如果为0 则使用CAS设置为1,
并设置当前线程持有者为当前线程,并返回true,如果CAS失败则返回false。继承自 AQS 实现的独占锁实现 tryRelease 时候,内部需要使用CAS算法把当前status值从1 修改为0,并设置当前锁的持有者为null,然后返回true,如果CAS失败则返回false。
3、共享式同步状态获取与释放
共享模式和独占模式最大的区别就是同一时刻是否能有多个线程同时获取状态。
当共享模式访问资源时,其他共享式的访问均被允许,而独占式访问被阻塞。而独占式访问资源时,同一时刻其他任何访问均被阻塞。
public final void acquireShared(int arg) { if (tryAcquireShared(arg) < 0) doAcquireShared(arg); }
private void doAcquireShared(int arg) { final Node node = addWaiter(Node.SHARED); boolean failed = true; try { boolean interrupted = false; for (;;) { final Node p = node.predecessor(); if (p == head) { int r = tryAcquireShared(arg); if (r >= 0) { setHeadAndPropagate(node, r); p.next = null; // help GC if (interrupted) selfInterrupt(); failed = false; return; } } if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) interrupted = true; } } finally { if (failed) cancelAcquire(node); } }
线程调用acquireShared(int arg) 获取共享资源时候,会首先使用tryAcquireShared尝试获取资源,当返回值大于等于0,表示可以获取状态。失败则将当前线程封装为类型Node.SHARED 的 Node 节点后插入到 AQS 阻塞队列尾部,并使用 LockSupport.park(this) 挂起当前线程。
在doAcquireShared(arg)方法中,如果前驱节点为头节点,尝试获取同步状态大于0,则表示可以获取同步状态并从自旋过程中退出。
public final boolean releaseShared(int arg) { if (tryReleaseShared(arg)) { doReleaseShared(); return true; } return false; }
当一个线程调用 releaseShared(int arg) 时候会尝试使用, tryReleaseShared 操作释放资源,这里是设置状态变量 state 的值,然后使用 LockSupport.unpark(thread)激活 AQS 队列里面最早被阻塞的线程 (thread)。
同理需要注意的 AQS 类并没有提供可用的 tryAcquireShared 和 tryReleaseShared,正如 AQS 是锁阻塞和同步器的基础框架,tryAcquireShared 和 tryReleaseShared 需要有具体的子类来实现。
子类在实现 tryAcquireShared 和 tryReleaseShared 时候要根据具体场景使用 CAS 算法尝试修改状态值 state, 成功则返回 true,否者返回 false。
比如继承自 AQS 实现的读写锁 ReentrantReadWriteLock 里面的读锁在重写 tryAcquireShared 时候,首先看写锁是否被其它线程持有,如果是则直接返回 false,否者使用 CAS 递增 status 的高 16 位,在 ReentrantReadWriteLock 中 status 的高 16 为获取读锁的次数。
继承自 AQS 实现的读写锁 ReentrantReadWriteLock 里面的读锁在重写 tryReleaseShared 时候,内部需要使用 CAS 算法把当前 status 值的高 16 位减一,然后返回 true, 如果 cas 失败则返回 false。
❤本博客只适用于研究学习为目的,大多为学习笔记,如有错误欢迎指正,如有误导敬请谅解(本人尽力保证90%的验证和10%的猜想)。
❤如果这篇文章对你有一点点的帮助请给一份推荐! 谢谢!你们的鼓励是我继续前进的动力。