Java中的鎖原理–AQS
- 2019 年 12 月 4 日
- 筆記
大家或多或少會接觸一些執行緒安全問題,什麼是執行緒安全?
通俗的來講,某個函數被多個執行緒調用多次,都能夠處理各個執行緒中的局部變數,並且計算結果正確,我們一般稱為執行緒安全。
如何解決執行緒安全問題?
一般有三種方式
- 使用 ThreadLocal 避免執行緒共享變數
- 使用 synchronized 和 Lock 進行同步控制。
- 使用原子變數聲明變數。
Lock 的實現原理是什麼?
AQS(AbstracctQueuedSynchronized) 隊列同步器,是用來構建鎖或者其他同步器組件的基礎框架。
AQS 使用了一個 int 變數來表示同步狀態,通過內置的 FIFO 隊列來完成資源獲取執行緒的排隊工作。
經常使用的同步組件ReentrantLock、ReentrantReadWriteLock和 CountDownLatch 等都是基於同步器實現的。
AQS 主要包含兩點,一個是同步狀態,第二個是隊列。
AQS 是怎麼實現執行緒同步的?主要包括同步隊列、獨佔是同步狀態的釋放和獲取、共享式同步狀態的釋放和獲取。
同步器依賴的是同步隊列的來進行同步狀態的管理。
同步隊列的結構

隊列中的節點 Node 是構成同步器的基礎。
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; /** * Status field, taking on only the values: * SIGNAL: The successor of this node is (or will soon be) * blocked (via park), so the current node must * unpark its successor when it releases or * cancels. To avoid races, acquire methods must * first indicate they need a signal, * then retry the atomic acquire, and then, * on failure, block. * CANCELLED: This node is cancelled due to timeout or interrupt. * Nodes never leave this state. In particular, * a thread with cancelled node never again blocks. * CONDITION: This node is currently on a condition queue. * It will not be used as a sync queue node * until transferred, at which time the status * will be set to 0. (Use of this value here has * nothing to do with the other uses of the * field, but simplifies mechanics.) * PROPAGATE: A releaseShared should be propagated to other * nodes. This is set (for head node only) in * doReleaseShared to ensure propagation * continues, even if other operations have * since intervened. * 0: None of the above * * The values are arranged numerically to simplify use. * Non-negative values mean that a node doesn't need to * signal. So, most code doesn't need to check for particular * values, just for sign. * * The field is initialized to 0 for normal sync nodes, and * CONDITION for condition nodes. It is modified using CAS * (or when possible, unconditional volatile writes). */ volatile int waitStatus; /** * Link to predecessor node that current node/thread relies on * for checking waitStatus. Assigned during enqueuing, and nulled * out (for sake of GC) only upon dequeuing. Also, upon * cancellation of a predecessor, we short-circuit while * finding a non-cancelled one, which will always exist * because the head node is never cancelled: A node becomes * head only as a result of successful acquire. A * cancelled thread never succeeds in acquiring, and a thread only * cancels itself, not any other node. */ volatile Node prev; /** * Link to the successor node that the current node/thread * unparks upon release. Assigned during enqueuing, adjusted * when bypassing cancelled predecessors, and nulled out (for * sake of GC) when dequeued. The enq operation does not * assign next field of a predecessor until after attachment, * so seeing a null next field does not necessarily mean that * node is at end of queue. However, if a next field appears * to be null, we can scan prev's from the tail to * double-check. The next field of cancelled nodes is set to * point to the node itself instead of null, to make life * easier for isOnSyncQueue. */ volatile Node next; /** * The thread that enqueued this node. Initialized on * construction and nulled out after use. */ volatile Thread thread; /** * Link to next node waiting on condition, or the special * value SHARED. Because condition queues are accessed only * when holding in exclusive mode, we just need a simple * linked queue to hold nodes while they are waiting on * conditions. They are then transferred to the queue to * re-acquire. And because conditions can only be exclusive, * we save a field by using special value to indicate shared * mode. */ Node nextWaiter; /** * Returns true if node is waiting in shared mode. */ final boolean isShared() { return nextWaiter == SHARED; } /** * Returns previous node, or throws NullPointerException if null. * Use when predecessor cannot be null. The null check could * be elided, but is present to help the VM. * * @return the predecessor of this node */ 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 的構造方法可以看到,包含了執行緒 Thread 和 狀態 waitStatus 或者 Thread 和 nextWaiter(Node) 。
/** 值為1,由於同步隊列中等待的執行緒超時或者被中斷,需要到同步隊列中取消等待,節點進入該狀態將不會變*/ static final int CANCELLED = 1; /**後繼節點的執行緒處於阻塞狀態,而如果當前節點的執行緒如果釋放同步狀態或者被取消,通知後繼節點,使得後繼節點可以運行*/ static final int SIGNAL = -1; /** 值為-2 節點在等待隊列中,節點執行緒等待在Condition上,當其他執行緒對 Condition 調用了 signal() 方法後,該節點會從等待隊列轉移到同步隊列中,進行同步狀態的獲取 */ static final int CONDITION = -2;
節點加入到同步隊列
同步器擁有首節點 head 和 尾節點 tail 沒有成功獲取同步狀態的執行緒將會組成Node 加入該隊列的尾部。這個加入隊尾的過程需要是執行緒安全的。同步器提供了一個基於 CAS 的設置尾節點的方法 compareAndSetTail(Node expt, Node update) 需要傳遞當前執行緒認為的尾節點 expt 和當前節點 update。

為什麼 CAS 能夠保證執行緒安全?
java 中的 CAS 是對 cmpxchg 的封裝。
cmpxchg 中x86 中有 CAS 指令。 cmpxchg是彙編指令 作用:比較並交換操作數. 如:CMPXCHG r/m,r 將累加器AL/AX/EAX/RAX中的值與首操作數(目的操作數)比較,如果相等,第2操作數(源操作數)的值裝載到首操作數,zf置1。如果不等, 首操作數的值裝載到AL/AX/EAX/RAX並將zf清0 該指令只能用於486及其後繼機型。第2操作數(源操作數)只能用8位、16位或32位暫存器。第1操作數(目地操作數)則可用暫存器或任一種存儲器定址方式
cmpxchg 功能就是保證一次只原子性的修改一個變數。
執行緒釋放同步狀態,節點出隊
首節點的執行緒在釋放同步狀態時,將會喚醒後繼節點。而後繼節點將會在獲取同步狀態時,將自己設置成首節點。

設置首節點是通過獲取同步狀態成功的執行緒來完成的,由於只有一個執行緒能夠成功的獲取同步狀態,因此,不需要使用 CAS 來保證只需要將首節點的後繼節點設置成首節點即可。