关于AQS源码的一些理解(1)-独占锁的获取和释放
AQS(AbstractQueuedSynchronizer)是JAVA中众多锁以及并发工具的基础。
锁机制:其底层采用乐观锁,大量使用了CAS操作, 并且在冲突时,采用自旋方式重试,以实现轻量级和高效地获取锁。
类设计:AQS虽然被定义为抽象类,但事实上它并不包含任何抽象方法。这是因为AQS是被设计来支持多种用途的,如果定义抽象方法,则子类在继承时必须要覆写所有的抽象方法,这显然是不合理的。
所以AQS将一些需要子类覆写的方法都设计成protect方法,将其默认实现为抛出UnsupportedOperationException
异常。如果子类使用到这些方法,但是没有覆写,则会抛出异常;如果子类没有使用到这些方法,则不需要做任何操作。
AQS中实现了锁的获取框架,锁的实际获取逻辑交由子类去实现,就锁的获取操作而言,子类必须重写 tryAcquire
方法。
本篇我们将以ReentrantLock的公平锁为例来详细看看使用AQS获取独占锁的流程。
本文中的源码基于JDK1.8 。
首先我们来看下下面这个图
volatile和cas是最基本的组成单元,上层设计都依赖底层的支持,非阻塞的数据结构和原子变量没什么好说的,AQS是上层很多同步工具的基础,本质是上为了保证在线程并发访问情况下保证状态的一致性,毫无疑问是通过锁来实现的,那么问题来了:
锁是怎么设计的?又是怎么实现的?
AQS是什么?又到底做了什么?
ok带着一些列的问题,我们详细看下AQS源代码。
在这之前我查阅了一些资料,原作者是这么描述的,要实现这么一个同步器工具,我们需要做这三件事:
• Atomically managing synchronization state
• Blocking and unblocking threads
• Maintaining queues
,看到了有人总结JUC并发工具的三板斧,觉得不错,所谓三板斧:
状态,队列,CAS
-
状态:一般是一个state属性,它基本是整个工具的核心,通常整个工具都是在设置和修改状态,很多方法的操作都依赖于当前状态是什么。由于状态是全局共享的,一般会被设置成volatile类型,以保证其修改的可见性;
-
队列:队列通常是一个等待的集合,大多数以链表的形式实现。队列采用的是悲观锁的思想,表示当前所等待的资源,状态或者条件短时间内可能无法满足。因此,它会将当前线程包装成某种类型的数据结构,扔到一个等待队列中,当一定条件满足后,再从等待队列中取出。
-
CAS: CAS操作是最轻量的并发处理,通常我们对于状态的修改都会用到CAS操作,因为状态可能被多个线程同时修改,CAS操作保证了同一个时刻,只有一个线程能修改成功,从而保证了线程安全,CAS操作基本是由Unsafe工具类的compareAndSwapXXX来实现的;CAS采用的是乐观锁的思想,因此常常伴随着自旋,如果发现当前无法成功地执行CAS,则不断重试,直到成功为止,自旋的的表现形式通常是一个死循环for(;;)。
状态
private volatile int state;
private transient Thread exclusiveOwnerThread; //继承自AbstractOwnableSynchronizer
队列
static final class Node { /** Marker to indicate a node is waiting in shared mode */ static final Node SHARED = new Node(); /** Marker to indicate a node is waiting in exclusive mode */ static final Node EXCLUSIVE = null; /** waitStatus value to indicate thread has cancelled */ static final int CANCELLED = 1; /** waitStatus value to indicate successor's thread needs unparking */ static final int SIGNAL = -1; /** waitStatus value to indicate thread is waiting on condition */ static final int CONDITION = -2; /** * waitStatus value to indicate the next acquireShared should * unconditionally propagate */ static final int PROPAGATE = -3;
// 线程所处的等待锁的状态,初始化时,该值为0 volatile int waitStatus;
volatile Node prev; volatile Node next;
// 节点所代表的线程 volatile Thread thread;
// 该属性用于条件队列或者共享锁 Node nextWaiter; final boolean isShared() { return nextWaiter == SHARED; } final Node predecessor() throws NullPointerException { Node p = prev; if (p == null) throw new NullPointerException(); else return p; } Node() { // Used to establish initial head or SHARED marker } Node(Thread thread, Node mode) { // Used by addWaiter this.nextWaiter = mode; this.thread = thread; } Node(Thread thread, int waitStatus) { // Used by Condition this.waitStatus = waitStatus; this.thread = thread; } }
在这个Node类中也有一个状态变量waitStatus,它表示了当前Node所代表的线程的等待锁的状态,在独占锁模式下,我们只需要关注CANCELLED SIGNAL两种状态即可。这里还有一个nextWaiter属性,它在独占锁模式下永远为null,仅仅起到一个标记作用,没有实际意义。
说完队列中的节点,我们接着说回这个sync queue,AQS是怎么使用这个队列的呢,既然是双向链表,操纵它自然只需要一个头结点和一个尾节点:
// 头结点,不代表任何线程,是一个哑结点 private transient volatile Node head; // 尾节点,每一个请求锁的线程会加到队尾 private transient volatile Node tail;
所以这个syncqueue的全貌如下
在AQS中的队列是一个CLH队列,它的head节点永远是一个哑结点(dummy node), 它不代表任何线程(某些情况下可以看做是代表了当前持有锁的线程),因此head所指向的Node的thread属性永远是null。只有从次头节点往后的所有节点才代表了所有等待锁的线程。也就是说,在当前线程没有抢到锁被包装成Node扔到队列中时,即使队列是空的,它也会排在第二个,我们会在它的前面新建一个dummy节点(具体的代码我们在后面分析源码时再详细讲)。为了便于描述,下文中我们把除去head节点的队列称作是等待队列,在这个队列中的节点才代表了所有等待锁的线程:
- thread:表示当前Node所代表的线程
- waitStatus:表示节点所处的等待状态,共享锁模式下只需关注三种状态:SIGNAL CANCELLED 初始态(0)
- prev next:节点的前驱和后继
- nextWaiter:进作为标记,值永远为null,表示当前处于独占锁模式
CAS操作
private static final Unsafe unsafe = Unsafe.getUnsafe(); private static final long stateOffset; private static final long headOffset; private static final long tailOffset; private static final long waitStatusOffset; private static final long nextOffset; static { try { stateOffset = unsafe.objectFieldOffset (AbstractQueuedSynchronizer.class.getDeclaredField("state")); headOffset = unsafe.objectFieldOffset (AbstractQueuedSynchronizer.class.getDeclaredField("head")); tailOffset = unsafe.objectFieldOffset (AbstractQueuedSynchronizer.class.getDeclaredField("tail")); waitStatusOffset = unsafe.objectFieldOffset (Node.class.getDeclaredField("waitStatus")); nextOffset = unsafe.objectFieldOffset (Node.class.getDeclaredField("next")); } catch (Exception ex) { throw new Error(ex); } }
protected final boolean compareAndSetState(int expect, int update) { return unsafe.compareAndSwapInt(this, stateOffset, expect, update); } private final boolean compareAndSetHead(Node update) { return unsafe.compareAndSwapObject(this, headOffset, null, update); } private final boolean compareAndSetTail(Node expect, Node update) { return unsafe.compareAndSwapObject(this, tailOffset, expect, update); } private static final boolean compareAndSetWaitStatus(Node node, int expect,int update) { return unsafe.compareAndSwapInt(node, waitStatusOffset, expect, update); } private static final boolean compareAndSetNext(Node node, Node expect, Node update) { return unsafe.compareAndSwapObject(node, nextOffset, expect, update); }
那么到这里我们已经初步认识了AQS,可以回答上面的第一个问题,锁是怎么设计的:锁通过一个状态来控制,又是怎么实现的:底层通过调用OS的CAS来实现。
那么下面我们要讨论的是,AQS是什么?又到底做了什么?
AQS是一个定义了如何实现和控制同步的工具类,用户可以通过继承AQS实现不同场景下的状态同步,对用户屏蔽了内部同步是如何实现的底层细节,专注于使用场景的编程。
举个例子:ReentrantLock
public class ReentrantLock implements Lock, java.io.Serializable { /** Synchronizer providing all implementation mechanics */ private final Sync sync; /** * Base of synchronization control for this lock. Subclassed * into fair and nonfair versions below. Uses AQS state to * represent the number of holds on the lock. */ abstract static class Sync extends AbstractQueuedSynchronizer { ... } /** * Sync object for non-fair locks */ static final class NonfairSync extends Sync{ ... } /** * Sync object for fair locks */ static final class FairSync extends Sync { ... } /** * Creates an instance of {@code ReentrantLock}. * This is equivalent to using {@code ReentrantLock(false)}. */ public ReentrantLock() { sync = new NonfairSync(); } /** * Creates an instance of {@code ReentrantLock} with the * given fairness policy. * * @param fair {@code true} if this lock should use a fair ordering policy */ public ReentrantLock(boolean fair) { sync = fair ? new FairSync() : new NonfairSync(); } // 获取锁 public void lock() { sync.lock(); } ... }
static final class FairSync extends Sync { private static final long serialVersionUID = -3000897897090466540L; //获取锁 final void lock() { acquire(1); } ... }
//获取锁
acquire(){ while (synchronization state does not allow acquire) { enqueue current thread if not already queued; possibly block current thread; } dequeue current thread if it was queued; }
//释放锁 release(){ update synchronization state; if (state may permit a blocked thread to acquire) unblock one or more queued threads; }

原始设计流程上看上去很简单,我们来详细看一下acquire 到底做了什么。
acquire
public final void acquire(int arg) { if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt(); }
-
在前驱节点就是head节点的时候,继续尝试获取锁
-
将当前线程挂起,使CPU不再调度它
tryAcquire
-
如果锁没有被占用, 尝试以公平的方式获取锁
-
如果锁已经被占用, 检查是不是锁重入
protected final boolean tryAcquire(int acquires) { final Thread current = Thread.currentThread(); // 首先获取当前锁的状态 int c = getState(); // c=0 说明当前锁是avaiable的, 没有被任何线程占用, 可以尝试获取 // 因为是实现公平锁, 所以在抢占之前首先看看队列中有没有排在自己前面的Node // 如果没有人在排队, 则通过CAS方式获取锁, 就可以直接退出了 if (c == 0) { if (!hasQueuedPredecessors() /* 为了阅读方便, hasQueuedPredecessors源码就直接贴在这里了, 这个方法的本质实际上是检测自己是不是head节点的后继节点,即处在阻塞队列第一位的节点 public final boolean hasQueuedPredecessors() { Node t = tail; Node h = head; Node s; return h != t && ((s = h.next) == null || s.thread != Thread.currentThread()); } */ && compareAndSetState(0, acquires)) { setExclusiveOwnerThread(current); // 将当前线程设置为占用锁的线程 return true; } } // 如果 c>0 说明锁已经被占用了 // 对于可重入锁, 这个时候检查占用锁的线程是不是就是当前线程,是的话,说明已经拿到了锁, 直接重入就行 else if (current == getExclusiveOwnerThread()) { int nextc = c + acquires; if (nextc < 0) throw new Error("Maximum lock count exceeded"); setState(nextc); /* setState方法如下: protected final void setState(int newState) { state = newState; } */ return true; } // 到这里说明有人占用了锁, 并且占用锁的不是当前线程, 则获取锁失败 return false; }
将state的状态通过CAS操作由0改写成1
这段代码是否是线程安全的?同一时刻是否可能有多个线程在执行这行代码?
addWaiter
addWaiter(Node.EXCLUSIVE) private Node addWaiter(Node mode) { Node node = new Node(Thread.currentThread(), mode); //将当前线程包装成Node // 这里我们用注释的形式把Node的构造函数贴出来 // 因为传入的mode值为Node.EXCLUSIVE,所以节点的nextWaiter属性被设为null /* static final Node EXCLUSIVE = null; Node(Thread thread, Node mode) { // Used by addWaiter this.nextWaiter = mode; this.thread = thread; } */ Node pred = tail; // 如果队列不为空, 则用CAS方式将当前节点设为尾节点 if (pred != null) { node.prev = pred; if (compareAndSetTail(pred, node)) { pred.next = node; return node; } } // 代码会执行到这里, 只有两种情况: // 1. 队列为空 // 2. CAS失败 // 注意, 这里是并发条件下, 所以什么都有可能发生, 尤其注意CAS失败后也会来到这里 enq(node); //将节点插入队列 return node; }
enq
-
等待队列现在是空的,没有线程在等待。
-
其他线程在当前线程入队的过程中率先完成了入队,导致尾节点的值已经改变了,CAS操作失败。
private Node enq(final Node node) { for (;;) { Node t = tail; // 如果是空队列, 首先进行初始化 // 这里也可以看出, 队列不是在构造的时候初始化的, 而是延迟到需要用的时候再初始化, 以提升性能 if (t == null) { // 注意,初始化时使用new Node()方法新建了一个dummy节点 if (compareAndSetHead(new Node())) tail = head; // 这里仅仅是将尾节点指向dummy节点,并没有返回 } else { // 到这里说明队列已经不是空的了, 这个时候再继续尝试将节点加到队尾 node.prev = t; if (compareAndSetTail(t, node)) { t.next = node; return t; } } } }
尾分叉
} else { // 到这里说明队列已经不是空的了, 这个时候再继续尝试将节点加到队尾 node.prev = t; if (compareAndSetTail(t, node)) { t.next = node; return t; } }
-
设置node的前驱节点为当前的尾节点:node.prev = t
-
修改tail属性,使它指向当前节点
-
修改原来的尾节点,使它的next指向当前节点
addWaiter总结
public final void acquire(int arg) { if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt(); }
acquireQueued
当前节点的前驱节点就是HEAD节点
final boolean acquireQueued(final Node node, int arg) { boolean failed = true; try { boolean interrupted = false; for (;;) { final Node p = node.predecessor(); // 在当前节点的前驱就是HEAD节点时, 再次尝试获取锁 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); } }
final Node p = node.predecessor(); // 在当前节点的前驱就是HEAD节点时, 再次尝试获取锁if (p == head && tryAcquire(arg)) { setHead(node); p.next = null; // help GC failed = false; return interrupted; }
private void setHead(Node node) { head = node; node.thread = null; node.prev = null; }
-
此时没有其他线程在创建新的头节点——因为很明显此时队列并不是空的,不会执行到创建头节点的代码
-
此时能执行setHead的只有一个线程——因为要执行到setHead, 必然是tryAcquire已经返回了true, 而同一时刻,只有一个线程能获取到锁
shouldParkAfterFailedAcquire
static final int CANCELLED = 1; static final int SIGNAL = -1; static final int CONDITION = -2; static final int PROPAGATE = -3;
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) { int ws = pred.waitStatus; // 获得前驱节点的ws if (ws == Node.SIGNAL) // 前驱节点的状态已经是SIGNAL了,说明闹钟已经设了,可以直接睡了 return true; if (ws > 0) { // 当前节点的 ws > 0, 则为 Node.CANCELLED 说明前驱节点已经取消了等待锁(由于超时或者中断等原因) // 既然前驱节点不等了, 那就继续往前找, 直到找到一个还在等待锁的节点 // 然后我们跨过这些不等待锁的节点, 直接排在等待锁的节点的后面 (是不是很开心!!!) do { node.prev = pred = pred.prev; } while (pred.waitStatus > 0); pred.next = node; } else { // 前驱节点的状态既不是SIGNAL,也不是CANCELLED // 用CAS设置前驱节点的ws为 Node.SIGNAL,给自己定一个闹钟 compareAndSetWaitStatus(pred, ws, Node.SIGNAL); } return false; }
-
如果为前驱节点的waitStatus值为 Node.SIGNAL 则直接返回 true
-
如果为前驱节点的waitStatus值为 Node.CANCELLED (ws > 0), 则跳过那些节点, 重新寻找正常等待中的前驱节点,然后排在它后面,返回false
-
其他情况, 将前驱节点的状态改为 Node.SIGNAL, 返回false
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); } }
parkAndCheckInterrupt
private final boolean parkAndCheckInterrupt() { LockSupport.park(this); // 线程被挂起,停在这里不再往下执行了 return Thread.interrupted(); }
总结
-
AQS中用state属性表示锁,如果能成功将state属性通过CAS操作从0设置成1即获取了锁
-
获取了锁的线程才能将exclusiveOwnerThread设置成自己
-
addWaiter负责将当前等待锁的线程包装成Node,并成功地添加到队列的末尾,这一点是由它调用的enq方法保证的,enq方法同时还负责在队列为空时初始化队列。
-
acquireQueued方法用于在Node成功入队后,继续尝试获取锁(取决于Node的前驱节点是不是head),或者将线程挂起
-
shouldParkAfterFailedAcquire方法用于保证当前线程的前驱节点的waitStatus属性值为SIGNAL,从而保证了自己挂起后,前驱节点会负责在合适的时候唤醒自己。
-
parkAndCheckInterrupt方法用于挂起当前线程,并检查中断状态。
-
如果最终成功获取了锁,线程会从lock()方法返回,继续往下执行;否则,线程会阻塞等待。