Java中的鎖原理–AQS

  • 2019 年 12 月 4 日
  • 筆記

大家或多或少會接觸一些執行緒安全問題,什麼是執行緒安全?

通俗的來講,某個函數被多個執行緒調用多次,都能夠處理各個執行緒中的局部變數,並且計算結果正確,我們一般稱為執行緒安全。

如何解決執行緒安全問題?

一般有三種方式

  1. 使用 ThreadLocal 避免執行緒共享變數
  2. 使用 synchronized 和 Lock 進行同步控制。
  3. 使用原子變數聲明變數。

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 來保證只需要將首節點的後繼節點設置成首節點即可。