- 从同步器的同步队列、独占式获取和释放同步状态、共享式获取和释放同步状态等核心数据结构和模板方法,分析同步器的实现。
1. 同步队列
① 同步队列的基本结构
- 同步器依赖内部的同步队列(一种FIFO双向队列)实现同步状态的管理:
- 当前线程获取同步状态失败,同步器将当前线程和等待状态等信息封装成Node节点,添加到同步队列的尾部。同时,会阻塞当前线程。
- 同步状态释放,会唤醒同步队列的首节点中的线程,使其再次尝试获取同步状态。
- Node节点中的重要属性:prev和next,构成双向队列。
- Node prev: 用于指向当前节点的前驱节点
- Node next: 用于指向当前节点的后继节点
- 同步队列的重要属性:head和tail
- Node head: 指向头结点的引用,即指向队列的头结点(但head并非真正的头结点)
- Node tail: 指向尾节点的引用,即指向队列的尾节点(但tail并非真正的尾节点)
- 同步队列的基本结构,如下图所示。
② 节点的添加——设置尾节点
- 由于同步队列是一个FIFO的双向队列,每次新添加的节点都从队列尾部进行添加。
- 当一个线程成功获取了同步状态,其他线程只能被同步器封装成节点添加到队列中。
- 节点入队的过程,必须保证线程安全。原因: 在多线程环境下,可能造成添加到同步队列的节点顺序错误或者数量不对。
- 为了保证线程安全,同步器提供了一种CAS设置尾节点的方法:
compareAndSetTail(Node expect, Node update)
。通过expect
的限定,保证节点的加入是有序的。
- 当expect节点为队列的原尾节点时,将新尾节点的prev引用指向原尾节点,将原尾节点的next引用指向新尾节点。
- 将队列的tail引用指向新尾节点,完成CAS设置尾节点。
③ 设置头结点
- 头结点是成功获取同步状态的节点,头结点中的线程在释放同步状态的同时,也会唤醒后继t节点中的线程。 如果后继节点中的线程成功获取同步状态,将会成为新的头结点。
- 设置头结点是由获取同步状态成功的线程来完成,由于只有一个线程能获取到同步状态,因此头结点的设置不需要CAS保证。
- 将head引用指向新头结点
- 断开原头结点和新头节点之间的prev和next引用。
2. 独占式获取和释放同步状态
① 独占式获取同步状态
-
独占式获取同步状态,使用同步器中的
acquire()
模板方法。节点中的线程无法响应中断,发生中断后,不会从同步队列中移出。public final void acquire(int arg) { if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt(); }
-
acquire()
模板方法主要完成了同步状态获取、节点构造、加入同步队列以及在同步队列中自旋等待的相关工作,其主要逻辑是:
- 当前线程通过调用
tryAcquire()
可重写方法独占式的获取同步状态,获取成功直接返回。 - 获取同步状态失败,构建独占式同步节点(
Node.EXCLUSIVE
),并调用addWaiter(Node mode)
方法将该节点加入到同步队列中。 - 最后调用
acquireQueued(finalNode node, int args)
方法,使该节点以死循环的方式获取同步状态。如果获取同步状态失败则阻塞节点中的线程,直到该节点的前驱节点出队或者阻塞线程被中断,才能唤醒阻塞线程。
acquire()
方法的调用流程:
② 同步节点的自旋
-
调用
acquireQueued(finalNode node, int args)
方法,会让节点以死循环的方式获取同步状态,这种死循环叫做节点的自旋。 -
节点进入同步队列后,就进入了一个自旋的过程:每个节点(或者说每个线程)都在自省的观察,当条件满足,获取到了同步状态,就可以从这个自旋过程中退出。否则, 依旧留在这个自旋过程中。
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); } }
-
自己对自旋的理解:
- 当前线程获取同步状态失败,会被封装成同步节点并加入到同步队列中。加入到同步队列中的线程是阻塞的,等待被唤醒。
- 节点中的线程被唤醒,如果该节点的前驱节点是头节点且获取同步状态成功,则该节点会退出旋过程,然后将自己设置为新的头结点。
- 节点中的线程被唤醒,如果发现自己的前驱节点不是头结点或者获取同步状态失败,则会重新阻塞,等待下一次被唤醒。
- 在
acquireQueued()
方法中,要求只有前驱节点是头结点才能获取同步状态,这样做的原因:
- 头结点是获取同步状态的节点,头结点释放同步状态的同时将会唤醒后继节点,后继节点中的线程将会检查自己的前驱节点是否为头结点。
- 这样可以保证同步队列的FIFO规则。
- 处于自旋的同步节点各自独立地检查自己的状态,通过简单的判断自己前驱节点是否为头结点,既可以保证同步队列的FIFO原则,又能方便地处理过早被通知的情况。
- 过早被通知: 前驱节点不是头结点的线程因为中断而被唤醒。
③ 独占式的释放同步状态
- 调用同步器的release()模板方法,实现独占式的释放同步状态。
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()
方法唤醒后继节点线程。 unparkSuccessor()
内部通过LockSuppor.unpark()
方法,唤醒于阻塞的线程。
3. 共享式的获取和释放同步状态
① 共享式的获取同步状态
- 共享式获取与独占式获取的区别: 同一时刻能否有多个线程同时获取到同步状态。
- 以文件的读写为例:
- 如果有一个程序在读文件,那么这一时刻的写操作均被阻塞,而读操作能够同时进行。
- 如果有一个程序在写文件,那么这一时刻不管是其他的写操作还是读操作,均被阻塞。
- 写操作要求对资源的独占式访问,而读操作可以是共享式访问。
-
调用同步器的
acquireShared()
模板方法,可以实现共享式获取同步状态。public final void acquireShared(int arg) { if (tryAcquireShared(arg) < 0) doAcquireShared(arg); }
- 当前线程首先调用
tryAcquireShared()
可重写方法,共享式的获取同步状态。如果返回值大于等于0,表示获取成功并返回。 - 如果返回值小于表示获取失败,调用
doAcquireShared()
方法,让线程进入自旋状态。 - 自旋过程中,如果当前节点的前驱节点是头结点,且调用
tryAcquireShared()
方法返回值大于等于0,则退出自旋。否则,继续进行自旋。
② 共享式的释放同步状态
-
调用
releaseShared()
模板方法,共享式的释放同步状态。public final boolean releaseShared(int arg) { if (tryReleaseShared(arg)) { doReleaseShared(); return true; } return false; }
-
共享式释放同步状态,使得同步状态的释放可能同时来自多个线程。为了确保同步状态的安全释放,一般通过循环和CAS来保证。
4. 关于AQS的问题总结
1. 什么是AQS?与锁的区别和联系?
- AQS:AbstractQueuedSynchronizer,队列同步器。
- AQS与锁的区别和联系:
- AQS是实现锁和其他同步组件的基础框架
- AQS是面向锁的实现者,它简化了锁的实现方式,屏蔽了同步状态管理,线程的排队、等待与唤醒等底层操作。
- 锁是面向使用者,它定义了使用者与锁交互的接口,隐藏了实现细节。
- AQS和锁有效的隔离了实现者和使用者所需关注的领域。
2. AQS中,自定义同步器需要重写哪些方法?
- 基础: AQS中5种指定的可重写方法的名称、作用,
tryAcquire()
的实现:compareAndSetState()
。 - 进阶: 以实现锁为例,讲解如何使用同步器的实现自定义锁。
3. AQS是什么?底层如何实现?
- 关于如何为实现:
- 两个核心(同步状态和同步队列)
- 更改同步状态的三种核心方法
- 同步队列的结构、如何设置尾节点(需要CAS保证)和头结点(不需要CAS保证)。