CSDN话题挑战赛第2期
参赛话题:学习笔记
目录
子类主要实现的一些方法(默认抛出 UnsupportedOperationException)
全称是 AbstractQueuedSynchronizer,是阻塞式锁和相关的同步器工具的框架。
1.概述
同步器的主要使用方式是继承,子类通过继承同步器并实现它的抽象方法来管理同步状态,在抽象方法的实现过程中免不了要对同步状态进行更改,这时就需要使用同步器提供的 3 个方法(getState()、setState(int newState)和 compareAndSetState(int expect,int update))来进行操作,因为它们能够保证状态的改变是安全的。
子类推荐被定义为自定义同步组件的静态内部类,同步器自身没有实现任何同步接口,它仅仅是定义了若干同步状态获取和释放的方法来供自定义同步组件使用,同步器既可以支持独占式地获取同步状态,也可以支持共享式地获取同步状态,这样就可以方便实现不同类型的同步组件 (ReentrantLock、ReentrantReadWriteLock 和 CountDownLatch 等)。
同步器是实现锁(也可以是任意同步组件)的关键,在锁的实现中聚合同步器,利用同步器实现锁的语义。可以这样理解二者之间的关系:锁是面向使用者的,它定义了使用者与锁交互的接口(比如可以允许两个线程并行访问),隐藏了实现细节;同步器面向的是锁的实现者,它简化了锁的实现方式,屏蔽了同步状态管理、线程的排队、等待与唤醒等底层操作。锁和同步器很好地隔离了使用者和实现者所需关注的领域。
-
特点:
- 用 state 属性来表示资源的状态(分独占模式和共享模式),子类需要定义如何维护这个状态,控制如何获取锁和释放锁
- getState - 获取 state 状态
- setState - 设置 state 状态
- compareAndSetState - cas 机制设置 state 状态
- 独占模式是只有一个线程能够访问资源,而共享模式可以允许多个线程访问资源
- 提供了基于 FIFO 的等待队列,类似于 Monitor 的 EntryList
- 条件变量来实现等待、唤醒机制,支持多个条件变量,类似于 Monitor 的 WaitSet
- 用 state 属性来表示资源的状态(分独占模式和共享模式),子类需要定义如何维护这个状态,控制如何获取锁和释放锁
-
子类主要实现的一些方法(默认抛出 UnsupportedOperationException)
- 同步器提供的模板方法基本上分为 3 类:
- 独占式获取与释放同步状态、共享式获取与释放同步状态和查询同步队列中的等待线程情况。自定义同步组件将使用同步器提供的模板方法来实现自己的同步语义。
- tryAcquire
-
尝试以独占模式获取。 此方法应查询对象的状态是否允许以独占模式获取它,如果允许则获取它。 此方法始终由执行获取的线程调用。 如果此方法报告失败,如果该线程尚未排队,则获取方法可以将该线程排队, 直到由某个其他线程的释放发出信号。这可用于实现方法Lock.tryLock() 。 默认实现抛出UnsupportedOperationException 。 参形: arg - 获取参数。该值始终是传递给获取方法的值,或者是在进入条件等待时保存的值。该值是未解释的,可以表示您喜欢的任何内容。 返回值: 如果成功,则为true 。成功后,该对象已被获取。 抛出: IllegalMonitorStateException – 如果获取会将此同步器置于非法状态。必须以一致的方式抛出此异常,同步才能正常工作。 UnsupportedOperationException – 如果不支持独占模式 protected boolean tryAcquire(int arg) { throw new UnsupportedOperationException(); }
- tryRelease
-
尝试设置状态以反映独占模式下的发布。 该方法总是由执行释放的线程调用。 默认实现抛出UnsupportedOperationException 。 参形: arg - 释放参数。此值始终是传递给释放方法的值,或者是进入条件等待时的当前状态值。该值是未解释的,可以表示您喜欢的任何内容。 返回值: 如果此对象现在处于完全释放状态,则为true ,以便任何等待的线程都可以尝试获取;否则false 。 抛出: IllegalMonitorStateException – 如果释放会将此同步器置于非法状态。必须以一致的方式抛出此异常,同步才能正常工作。 UnsupportedOperationException – 如果不支持独占模式 protected boolean tryRelease(int arg) { throw new UnsupportedOperationException(); }
- tryAcquireShared
-
尝试以共享模式获取。 此方法应查询对象的状态是否允许以共享模式获取它,如果允许则获取它。 此方法始终由执行获取的线程调用。 如果此方法报告失败,如果该线程尚未排队, 则获取方法可以将该线程排队,直到由某个其他线程的释放发出信号。 默认实现抛出UnsupportedOperationException 。 参数: arg - 获取参数。该值始终是传递给获取方法的值,或者是在进入条件等待时保存的值。该值是未解释的,可以表示您喜欢的任何内容。 返回值: 失败的负值; 如果以共享模式获取成功但后续共享模式获取不能成功,则为零; 如果在共享模式下获取成功并且后续共享模式获取也可能成功,则为正值, 在这种情况下,后续等待线程必须检查可用性。 (对三种不同返回值的支持使此方法可以在仅获取有时独占的上下文中使用。)成功后,此对象已被获取。 抛出: IllegalMonitorStateException – 如果获取会将此同步器置于非法状态。必须以一致的方式抛出此异常,同步才能正常工作。 UnsupportedOperationException – 如果不支持共享模式 protected int tryAcquireShared(int arg) { throw new UnsupportedOperationException(); }
- tryReleaseShared
-
尝试设置状态以反映共享模式下的发布。 该方法总是由执行释放的线程调用。 默认实现抛出UnsupportedOperationException 。 参形: arg - 释放参数。此值始终是传递给释放方法的值,或者是进入条件等待时的当前状态值。该值是未解释的,可以表示您喜欢的任何内容。 返回值: 如果此共享模式的发布可能允许等待获取(共享或独占)成功,则为true ;否则false 抛出: IllegalMonitorStateException – 如果释放会将此同步器置于非法状态。必须以一致的方式抛出此异常,同步才能正常工作。 UnsupportedOperationException – 如果不支持共享模式 protected boolean tryReleaseShared(int arg) { throw new UnsupportedOperationException(); }
- isHeldExclusively
-
当前线程是否在独占模式下被线程占用,一般用来表示线程是否被占用 如果同步仅针对当前(调用)线程进行,则返回true 。 每次调用非等待AbstractQueuedSynchronizer.ConditionObject方法时都会调用此方法。 (等待方法改为调用release 。) 默认实现抛出UnsupportedOperationException 。 此方法仅在AbstractQueuedSynchronizer.ConditionObject方法内部调用, 因此如果不使用条件则无需定义。 返回值: 如果同步是独占的,则为true ;否则false 抛出: UnsupportedOperationException – 如果条件不受支持 protected boolean isHeldExclusively() { throw new UnsupportedOperationException(); }
2.自定义锁——独占锁
class Mutex implements Lock {
// 静态内部类,自定义同步器
private static class Sync extends AbstractQueuedSynchronizer {
// 是否处于占用状态
protected boolean isHeldExclusively() {
return getState() == 1;
}
// 当状态为 0 的时候获取锁
public boolean tryAcquire(int acquires) {
if (compareAndSetState(0, 1)) {
setExclusiveOwnerThread(Thread.currentThread());
return true;
}
return false;
}
// 释放锁,将状态设置为 0
protected boolean tryRelease(int releases) {
if (getState() == 0) throw new IllegalMonitorStateException();
setExclusiveOwnerThread(null);
setState(0);
return true;
}
// 返回一个 Condition,每个 condition 都包含了一个 condition 队列
Condition newCondition() {
return new ConditionObject();
}
}
// 仅需要将操作代理到 Sync 上即可
private final Sync sync = new Sync();
public void lock() {
sync.acquire(1);
}
public boolean tryLock() {
return sync.tryAcquire(1);
}
public void unlock() {
sync.release(1);
}
public Condition newCondition() {
return sync.newCondition();
}
public boolean isLocked() {
return sync.isHeldExclusively();
}
public boolean hasQueuedThreads() {
return sync.hasQueuedThreads();
}
public void lockInterruptibly() throws InterruptedException {
sync.acquireInterruptibly(1);
}
public boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException{
return sync.tryAcquireNanos(1, unit.toNanos(timeout));
}
}
上述示例中,独占锁 Mutex 是一个自定义同步组件,它在同一时刻只允许一个线程占有锁。Mutex 中定义了一个静态内部类,该内部类继承了同步器并实现了独占式获取和释放同步状态。
在 tryAcquire(int acquires)方法中,如果经过 CAS 设置成功(同步状态设置为 1),则代表获取了同步状态,而在 tryRelease(int releases)方法中只是将同步状态重置为 0。
用户使用 Mutex 时并不会直接和内部同步器的实现打交道,而是调用 Mutex 提供的方法,在 Mutex 的实现中,以获取锁的 lock()方法为例,只需要在方法实现中调用同步器的模板方法 acquire(int args)即可,当前线程调用该方法获取同步状态失败后会被加入到同步队列中等待,这样就大大降低了实现一个可靠自定义同步组件的门槛。
3.队列同步器的实现分析
1) 同步队列
同步器依赖内部的同步队列(一个 FIFO 双向队列)来完成同步状态的管理。
当前线程获取同步状态失败时,同步器会将当前线程以及等待状态等信息构造成为一个节点 (Node)并将其加入同步队列,同时会阻塞当前线程。
当同步状态释放时,会把首节点中的线程唤醒,使其再次尝试获取同步状态。
![](https://i-blog.csdnimg.cn/blog_migrate/e51fe72f7f047de2fd041b565cc44576.png)
同步器拥有首节 点(head)和尾节点(tail),没有成功获取同步状态的线程将会成为节点加入该队列的尾部。
同步器提供了一个基于 CAS 的设置尾节点的方法: compareAndSetTail(Node expect,Node update),它需要传递当前线程“认为”的尾节点和当前节点,只有设置成功后,当前节点才正式与之前的尾节点建立关联。
同步队列遵循 FIFO,首节点是获取同步状态成功的节点,首节点的线程在释放同步状态时,将会唤醒后继节点,而后继节点将会在获取同步状态成功时将自己设置为首节点。
设置首节点是通过获取同步状态成功的线程来完成的,由于只有一个线程能够成功获取到同步状态,因此设置头节点的方法并不需要使用 CAS 来保证,它只 需要将首节点设置成为原首节点的后继节点并断开原首节点的 next 引用即可
2) 独占式同步状态获取与释放
通过调用同步器的 acquire(int arg)方法可以获取同步状态,该方法对中断不敏感,也就是由于线程获取同步状态失败后进入同步队列中,后续对线程进行中断操作时,线程不会从同步队列中移出。
public final void acquire(int arg) {
if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) {
selfInterrupt();
}
}
上述代码主要完成了同步状态获取、节点构造、加入同步队列以及在同步队列中自旋等待的相关工作,其主要逻辑是:
首先调用自定义同步器实现的 tryAcquire(int arg)方 法,该方法保证线程安全的获取同步状态,如果同步状态获取失败,则构造同步节点 (独占式 Node.EXCLUSIVE,同一时刻只能有一个线程成功获取同步状态)并通过 addWaiter(Node node) 方法将该节点加入到同步队列的尾部,最后调用 acquireQueued(Node node,int arg)方法,使得该节点以“死循环”的方式获取同步状态。如 果获取不到则阻塞节点中的线程,而被阻塞线程的唤醒主要依靠前驱节点的出队或阻塞线程被中断来实现。
private Node addWaiter(Node mode) {
Node node = new Node(Thread.currentThread(), mode);
// 快速尝试在尾部添加
Node pred = tail;
if (pred != null) {
node.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
enq(node);
return node;
}
//
同步器通过“死循环”来保证节点的正确添加,
在“死循环”中只有通过CAS 将节点设置成为尾节点之后,当前线程才能从该方法返回,
否则,当前线程不断地尝试设置。
可以看出,enq(final Node node)方法将并发添加节点的请求通过 CAS 变得“串行化”了。
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;
}
}
}
}
节点进入同步队列之后,就进入了一个自旋的过程,每个节点(或者说每个线程)都在自省地观察,当条件满足,获取到了同步状态,就可以从这个自旋过程中退出,否则依旧留在这个自旋过程中(并会阻塞节点的线程)。
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(final Node node,int arg)方法中,当前线程在“死循环”中尝试获取同 步状态,而只有前驱节点是头节点才能够尝试获取同步状态。
- 头节点是成功获取到同步状态的节点,而头节点的线程释放了同步状态之 后,将会唤醒其后继节点,后继节点的线程被唤醒后需要检查自己的前驱节点是否是头节点。
- 维护同步队列的 FIFO 原则,该方法中,节点自旋获取同步状态的行为如图。
由于非首节点线程前驱节点出队或者被中断而从等待状态返回,随后检查自己的前驱是否是头节点,如果是则尝试获取同步状态。可以看到节点和节点之间在循环检查的过程中基本不相互通信,而是简单地判断自己的前驱是否为头节点,这样就使得节点的释放规则符合 FIFO,并且也便于对过早通知的处理(过早通知是指前驱节点不是头节点的线程由于中断而被唤醒)。
前驱节点为头节点且能够获取同步状态的判断条件和线程进入等待状 态是获取同步状态的自旋过程。当同步状态获取成功之后,当前线程从 acquire(int arg)方 法返回,如果对于锁这种并发组件而言,代表着当前线程获取了锁。
当前线程获取同步状态并执行了相应逻辑之后,就需要释放同步状态,使得后续节点能够继续获取同步状态。通过调用同步器的 release(int arg)方法可以释放同步状态,该 方法在释放了同步状态之后,会唤醒其后继节点(进而使后继节点重新尝试获取同步状态)。
该方法执行时,会唤醒头节点的后继节点线程,unparkSuccessor(Node node)方法使用
LockSupport来唤醒处于等待状态的线程。
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0) unparkSuccessor(h);
return true;
}
return false;
}
总结:在获取同步状态时,同步器维护一个同步队列,获取状态失败的线程都会被加入到队列中并在队列中进行自旋;移出队列(或停止自旋)的条件是前驱节点为头节点且成功获取了同步状态。在释放同步状态时,同步器调用 tryRelease(int arg)方法释放同步状态,然后唤醒头节点的后继节点。
3) 共享式同步状态获取与释放
共享式获取与独占式获取最主要的区别在于同一时刻能否有多个线程同时获取到同步状态。
![](https://i-blog.csdnimg.cn/blog_migrate/d933bfdeb24dd212f89c400a8d0fc06d.png)
而独占式访问被阻塞,右半部分是独占式访问资源时,同一时刻其他访问均被阻塞。
调用同步器的 acquireShared(int arg)方法可以共享式地获取同步状态。
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;
if (interrupted) selfInterrupt();
failed = false;
return;
}
}
if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed) cancelAcquire(node);
}
}
在 acquireShared(int arg)方法中,同步器调用 tryAcquireShared(int arg)方法尝试获取同步状态,tryAcquireShared(int arg)方法返回值为 int 类型,当返回值大于等于 0 时,表示能够获取到同步状态。
因此,在共享式获取的自旋过程中,成功获取到同步状态并退出自旋的条件就是 tryAcquireShared(int arg)方法返回值大于等于 0。可以看到,在 doAcquireShared(int arg)方法的自旋过程中,如果当前节点的前驱为头节点时,尝试获取同步状态,如果返回值大于等于 0,表示该次获取同步状态成功并从自旋过程中退出。
与独占式一样,共享式获取也需要释放同步状态,通过调用 releaseShared(int arg)方 法可以释放同步状态。
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}
该方法在释放同步状态之后,将会唤醒后续处于等待状态的节点。对于能够支持多个线程同时访问的并发组件(比如 Semaphore),它和独占式主要区别在于 tryReleaseShared(int arg) 方法必须确保同步状态(或者资源数)线程安全释放,一般是通过循环和 CAS 来保证的,因为释放同步状态的操作会同时来自多个线程。
4) 独占式超时获取同步状态
通过调用同步器的 doAcquireNanos(int arg,long nanosTimeout)方法可以超时获取同步状态,即在指定的时间段内获取同步状态,如果获取到同步状态则返回 true,否则,返回 false。该方法提供了传统 Java 同步操作(比如 synchronized 关键字)所不具备的特 性。
在分析该方法的实现前,先介绍一下响应中断的同步状态获取过程。
在 Java 5 之前,当一个线程获取不到锁而被阻塞在 synchronized 之外时,对该线程进行中断操作, 此时该线程的中断标志位会被修改,但线程依旧会阻塞在 synchronized 上,等待着获取锁。
在 Java 5 中,同步器提供了 acquireInterruptibly(int arg)方法,这个方法在等待获取同步状态时,如果当前线程被中断,会立刻返回,并抛出 InterruptedException。
超时获取同步状态过程可以被视作响应中断获取同步状态过程的“增强版”, doAcquireNanos(int arg,long nanosTimeout)方法在支持响应中断的基础上,增加了超时获取的特性。针对超时获取,主要需要计算出需要睡眠的时间间隔 nanosTimeout,为了防止过早通知, nanosTimeout 计算公式为:nanosTimeout-=now-lastTime,其中 now 为当前 唤醒时间,lastTime 为上次唤醒时间,如果 nanosTimeout 大于 0 则表示超时时间未到, 需要继续睡眠 nanosTimeout 纳秒,反之,表示已经超时。
private boolean doAcquireNanos(int arg, long nanosTimeout) throws
InterruptedException {
long lastTime = System.nanoTime();
final Node node = addWaiter(Node.EXCLUSIVE);
boolean failed = true;
try {
for (; ; ) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return true;
}
if (nanosTimeout <= 0) return false;
if (shouldParkAfterFailedAcquire(p, node)
&& nanosTimeout > spinForTimeoutThreshold)
LockSupport.parkNanos(this, nanosTimeout);
long now = System.nanoTime();
//计算时间,当前时间 now 减去睡眠之前的时间 lastTime 得到已经睡眠
//的时间 delta,然后被原有超时时间 nanosTimeout 减去,得到了
//还应该睡眠的时间
nanosTimeout -= now - lastTime;
lastTime = now;
if (Thread.interrupted()) throw new InterruptedException();
}
} finally {
if (failed) cancelAcquire(node);
}
}
该方法在自旋过程中,当节点的前驱节点为头节点时尝试获取同步状态,如果获取成功则从该方法返回,这个过程和独占式同步获取的过程类似,但是在同步状态获取失败的处理上有所不同。
如果当前线程获取同步状态失败,则判断是否超时 (nanosTimeout 小于等于 0 表示已经超时),如果没有超时,重新计算超时间隔 nanosTimeout,然后使当前线程等待 nanosTimeout 纳秒(当已到设置的超时时间,该线 程会从 LockSupport.parkNanos(Object blocker,long nanos)方法返回)。
如果 nanosTimeout 小于等于 spinForTimeoutThreshold(1000 纳秒)时,将不会使该线程进行超时等待,而是进入快速的自旋过程。原因在于,非常短的超时等待无法做到十分精确,如果这时再进行超时等待,相反会让 nanosTimeout 的超时从整体上表现得反 而不精确。因此,在超 时非常短的场景下,同步器会进入无条件的快速自旋。
5) 自定义同步组件——TwinsLock
设计一个同步工具:该工具在同一时刻,只允许至多两个线程同时访问,超过两个线程的访问将被阻塞,我们将这个同步工具命名为 TwinsLock。
首先,确定访问模式。TwinsLock 能够在同一时刻支持多个线程的访问,这显然是共享式访问,因此,需要使用同步器提供的 acquireShared(int args)方法等和 Shared 相关 的方法,这就要求 TwinsLock 必须重写 tryAcquireShared(int args)方法和 tryReleaseShared(int args)方法,这样才能保证同步器的共享式同步状态的获取与释放方法得以执行。
其次,定义资源数。TwinsLock 在同一时刻允许至多两个线程的同时访问,表明同步资源数为 2,这样可以设置初始状态 status 为 2,当一个线程进行获取,status 减 1,该线程释放,则 status 加 1,状态的合法范围为 0、1 和 2,其中 0 表示当前已经有两个线程获取了同步资源,此时再有其他线程对同步状态进行获取,该线程只能被阻塞。
在同步状态变更时,需要使用 compareAndSet(int expect,int update)方法做原子性保障。最后, 组合自定义同步器。前面的章节提到,自定义同步组件通过组合自定义同步器来完成同步功能,一般情况下自定义同步器会被定义为自定义同步组件的内部类。
public class TwinsLock implements Lock {
private final Sync sync = new Sync(2);
public void lock() {
sync.acquireShared(1);
}
public void unlock() {
sync.releaseShared(1);
}
private static final class Sync extends AbstractQueuedSynchronizer {
Sync(int count) {
if (count <= 0) {
throw new IllegalArgumentException("count must large than zero.");
}
setState(count);
}
public int tryAcquireShared(int reduceCount) {
for (; ; ) {
int current = getState();
int newCount = current - reduceCount;
if (newCount < 0 || compareAndSetState(current, newCount)) {
return newCount;
}
}
}
public boolean tryReleaseShared(int returnCount) {
for (; ; ) {
int current = getState();
int newCount = current + returnCount;
if (compareAndSetState(current, newCount)) {
return true;
}
}
}
}
// 其他接口方法略
}
TwinsLock 实现了 Lock 接口,提供了面向使用者的接口,使用者调用 lock() 方法获取锁,随后调用 unlock()方法释放锁,而同一时刻只能有两个线程同时获取到锁。
TwinsLock 同时包含了一个自定义同步器 Sync,而该同步器面向线程访问和同步状态控制。以共享式获取同步状态为例:同步器会先计算出获取后的同步状态,然后通过 CAS 确保状态的正确设置,当 tryAcquireShared(int reduceCount)方法返回值大于等于 0 时,当前线程才获取同步状态,对于上层的 TwinsLock 而言,则表示当前线程获得了锁。 同步器作为一个桥梁,连接线程访问以及同步状态控制等底层技术与不同并发组件 (比如 Lock、CountDownLatch 等)的接口语义。
public class TwinsLockTest {
@Test
public void test() {
final Lock lock = new TwinsLock();
class Worker extends Thread {
public void run() {
while (true) {
lock.lock();
try {
SleepUtils.second(1);
System.out.println(Thread.currentThread().getName());
SleepUtils.second(1);
} finally {
lock.unlock();
}
}
}
}
// 启动 10 个线程
for (int i = 0; i < 10; i++) {
Worker w = new Worker();
w.setDaemon(true);
w.start();
}
// 每隔 1 秒换行
for (int i = 0; i < 10; i++) {
SleepUtils.second(1);
System.out.println();
}
}
}