Java併發編程解析 | 解析AQS基礎同步器的設計與實現

關健術語

Picture-Keyword

本文用到的一些關鍵詞語以及常用術語,主要如下:

  • 信號量(Semaphore): 是在多線程環境下使用的一種設施,是可以用來保證兩個或多個關鍵代碼段不被併發調用,也是作系統用來解決併發中的互斥和同步問題的一種方法。
  • 信號量機制(Semaphores): 用來解決同步/互斥的問題的,它是1965年,荷蘭學者 Dijkstra提出了一種卓有成效的實現進程互斥與同步的方法。
  • 管程(Monitor) : 一般是指管理共享變量以及對共享變量的操作過程,讓它們支持併發的一種機制。

基本概述

在Java領域中,我們可以將鎖大致分爲基於Java語法層面(關鍵詞)實現的鎖和基於JDK層面實現的鎖。

在Java領域中, 尤其是在併發編程領域,對於多線程併發執行一直有兩大核心問題:同步和互斥。其中:

  • 互斥(Mutual Exclusion):一個公共資源同一時刻只能被一個進程或線程使用,多個進程或線程不能同時使用公共資源。即就是同一時刻只允許一個線程訪問共享資源的問題。
  • 同步(Synchronization):兩個或兩個以上的進程或線程在運行過程中協同步調,按預定的先後次序運行。即就是線程之間如何通信、協作的問題。

針對對於這兩大核心問題,利用管程是能夠解決和實現的,因此可以說,管程是併發編程的萬能鑰匙。

雖然,Java在基於語法層面(synchronized 關鍵字)實現了對管程技術,但是從使用方式和性能上來說,內置鎖(synchronized 關鍵字)的粒度相對過大,不支持超時和中斷等問題。

爲了彌補這些問題,從JDK層面對其“重複造輪子”,在JDK內部對其重新設計和定義,甚至實現了新的特性。

在Java領域中,從JDK源碼分析來看,基於JDK層面實現的鎖大致主要可以分爲以下4種方式:

  • 基於Lock接口實現的鎖
  • 基於ReadWriteLock接口實現的鎖
  • 基於AQS基礎同步器實現的鎖
  • 基於自定義API操作實現的鎖

從閱讀源碼不難發現,在Java SDK 併發包主要通過AbstractQueuedSynchronizer(AQS)實現多線程同步機制的封裝與定義,而通過Lock 和 Condition 兩個接口來實現管程,其中 Lock 用於解決互斥問題,Condition 用於解決同步問題。

一. 基本理論

在併發編程領域,有兩大核心問題:一個是互斥,即同一時刻只允許一個線程訪問共享資源;另一個是同步,即線程之間如何通信、協作。

在操作系統中,一般有如果I/O操作時,對於阻塞和非阻塞是從函數調用角度來說的,其中:

  • 阻塞:如果讀寫操作沒有就緒或者完成,則函數一直等待。
  • 非阻塞: 函數立即調用,然後讓應用程序輪詢循環。

而同步和異步則是從“讀寫是主要是由誰完成”的角度來說的,其中:

  • 同步: 讀寫操作主要交給應用程序完成
  • 異步: 讀寫操作主要由操作系統完成,一般完成之後,回調函數和事件通知應用程序。

其中,信號量機制(Semaphores)是用來解決同步/互斥的問題的,但是信號量(Semaphore)的操作分散在各個進程或線程中,不方便進行管理,因每次需調用P/V(來自荷蘭語 proberen和 verhogen)操作,還可能導致死鎖或破壞互斥請求的問題。

由於PV操作對於解決進程互斥/同步編程複雜,因而在此基礎上提出了與信號量等價的——“管程技術”。

其中,管程(Monitor)當中定義了共享數據結構只能被管程內部定義的函數所修改,所以如果我們想修改管程內部的共享數據結構的話,只能調用管程內部提供的函數來間接的修改這些數據結構。

一般來說,管程(Monitor)和信號量(Semaphore)是等價的,所謂等價指的是用管程能夠實現信號量,也能用信號量實現管程。

在管程的發展歷程上,先後出現過Hasen模型、Hoare模型和MESA模型等三種不同的管程模型,現在正在廣泛使用的是MESA模型。

在MESA模型中,管程中引入了條件變量(Conditional Variable)的概念,而且每個條件變量都對應有一個等待隊列(Wait Queue)。其中,條件變量和等待隊列的作用是解決線程之間的同步問題。

而對於解決線程之間的互斥問題,將共享變量(Shared Variable)及其對共享變量的操作統一封裝起來,一般主要是實現一個線程安全的阻塞隊列(Blocking Queue),將線程不安全的隊列封裝起來,對外提供線程安全的操作方法,例如入隊操作(Enqueue)和出隊操作(Dequeue)。

在Java領域中,對於Java語法層面實現的鎖(synchronized 關鍵字), 其實就是參考了 MESA 模型,並且對 MESA 模型進行了精簡,一般在MESA 模型中,條件變量可以有多個,Java 語言內置的管程(synchronized)裏只有一個條件變量。

這就意味着,被synchronized 關鍵字修飾的代碼塊或者直接標記靜態方法以及實例方法,在編譯期會自動生成相關加鎖(lock)和解鎖(unlock)的代碼,即就是monitorenter和monitorexit指令。

對於synchronized 關鍵字來說,主要是在Java HotSpot(TM) VM 虛擬機通過Monitor(監視器)來實現monitorenter和monitorexit指令的。

同時,在Java HotSpot(TM) VM 虛擬機中,每個對象都會有一個監視器,監視器和對象一起創建、銷燬。

監視器相當於一個用來監視這些線程進入的特殊房間,其義務是保證(同一時間)只有一個線程可以訪問被保護的臨界區代碼塊。

本質上,監視器是一種同步工具,也可以說是JVM對管程的同步機制的封裝實現,主要特點是:

  • 同步:監視器所保護的臨界區代碼是互斥地執行的。一個監視器是一個運行許可,任一線程進入臨界區代碼都需要獲得這個許可,離開時把許可歸還。
  • 協作:監視器提供Signal機制,允許正持有許可的線程暫時放棄許可進入阻塞等待狀態,等待其他線程發送Signal去喚醒;其他擁有許可的線程可以發送Signal,喚醒正在阻塞等待的線程,讓它可以重新獲得許可並啓動執行。

在Hotspot虛擬機中,監視器是由C++類ObjectMonitor實現的,ObjectMonitor類定義在ObjectMonitor.hpp文件中,其中:

v7MaNT.png

  • Owner: 指向的線程即爲獲得鎖的線程
  • Cxq:競爭隊列(Contention Queue),所有請求鎖的線程首先被放在這個競爭隊列中
  • EntryList:對象實體列表,表示Cxq中那些有資格成爲候選資源的線程被移動到EntryList中。
  • WaitSet:類似於等待隊列,某個擁有ObjectMonitor的線程在調用Object.wait()方法之後將被阻塞,然後該線程將被放置在WaitSet鏈表中。

同時,管程與Java中面向對象原則(Object Oriented Principle)也是非常契合的,主要體現在 java.lang.Object類中wait()、notify()、notifyAll() 這三個方法,其中:

  • wait()方法: 阻塞線程並且進入等待隊列
  • notify()方法:隨機地通知等待隊列中的一個線程
  • notifyAll()方法: 通知等待隊列中的所有線程

不難發現,在Java中synchronized 關鍵字及 java.lang.Object類中wait()、notify()、notifyAll() 這三個方法都是管程的組成部分。

由此可見,我們可以得到一個比較通用的併發同步工具基礎模型,大致包含如下幾個內容,其中:

  • 條件變量(Conditional Variable): 利用線程間共享的變量進行同步的一種工作機制
  • 共享變量((Shared Variable)):一般指對象實體對象的成員變量和屬性
  • 阻塞隊列(Blocking Queue):共享變量(Shared Variable)及其對共享變量的操作統一封裝
  • 等待隊列(Wait Queue):每個條件變量都對應有一個等待隊列(Wait Queue),內部需要實現入隊操作(Enqueue)和出隊操作(Dequeue)方法
  • 變量狀態描述機(Synchronization Status):描述條件變量和共享變量之間狀態變化,又可以稱其爲同步狀態
  • 工作模式(Operation Mode): 線程資源具有排他性,因此定義獨佔模式和共享模式兩種工作模式

綜上所述,條件變量和等待隊列的作用是解決線程之間的同步問題;共享變量與阻塞隊列的作用是解決線程之間的互斥問題。

二.AQS基礎同步器的設計與實現

在Java領域中,同步器是專門爲多線程併發設計的同步機制,主要是多線程併發執行時線程之間通過某種共享狀態來實現同步,只有當狀態滿足這種條件時線程才往下執行的一種同步機制。

對於多線程實現實現併發處理機制來說,一直以來,多線程都存在2個問題:

  • 線程之間內存共享,需要通過加鎖進行控制,但是加鎖會導致性能下降,同時複雜的加鎖機制也會增加編程編碼難度
  • 過多線程造成線程之間的上下文切換,導致效率低下

因此,在併發編程領域中,一直有一個很重要的設計原則: “ 不要通過內存共享來實現通信,而應該通過通信來實現內存共享。”

簡單來說,就是儘可能通過消息通信,而不是內存共享來實現進程或者線程之間的同步。

其中,同步器是專門爲多線程併發設計的同步機制,主要是多線程併發執行時線程之間通過某種共享狀態來實現同步,只有當狀態滿足這種條件時線程才往下執行的一種同步機制。

由於在不同的應用場景中,對於同步器的需求也會有所不同,一般在我們自己去實現和設計一種併發工具的時候,都需會考慮以下幾個問題:

  • 是否支持響應中斷? 如果阻塞狀態的線程能夠響應中斷信號,也就是說當我們給阻塞的線程發送中斷信號的時候,能夠喚醒它,那它就有機會釋放曾經持有的鎖。
  • 是否支持超時?如果線程在一段時間之內沒有獲取到鎖,不是進入阻塞狀態,而是返回一個錯誤,那這個線程也有機會釋放曾經持有的鎖。
  • 是否支持非阻塞地獲取鎖資源 ? 如果嘗試獲取鎖失敗,並不進入阻塞狀態,而是直接返回,那這個線程也有機會釋放曾經持有的鎖。

從閱讀JDK源碼不難發現,主要是採用設計模式中模板模式的原則,JDK將各種同步器中相同的部分抽象封裝成了一個統一的基礎同步器,然後基於基礎同步器爲模板通過繼承的方式來實現不同的同步器。

也就是說,在實際開發過程中,除了直接使用JDK實現的同步器,還可以基於這個基礎同步器我們也可以自己自定義實現符合我們業務需求的同步器。

在JDK源碼中,同步器位於java.util.concurrent.locks包下,其基本定義是AbstractQueuedSynchronizer類,即就是我們常說的AQS同步器。

1. 設計思想

一個標準的AQS同步器主要有同步狀態機制,等待隊列,條件隊列,獨佔模式,共享模式等五大核心要素組成。

JDK的JUC(java.util.concurrent.)包中提供了各種併發工具,但是大部分同步工具的實現基於AbstractQueuedSynchronizer類實現,其內部結構主要如下:

  • 同步狀態機制(Synchronization Status):主要用於實現鎖(Lock)機制,是指同步狀態,其要求對於狀態的更新必須原子性的
  • 等待隊列(Wait Queue):主要用於存放等待線程獲取到的鎖資源,並且把線程維護到一個Node(節點)裏面和維護一個非阻塞的CHL Node FIFO(先進先出)隊列,主要是採用自旋鎖+CAS操作來保證節點插入和移除的原子性操作。
  • 條件隊列(Condition Queue):用於實現鎖的條件機制,一般主要是指替換“等待-通知”工作機制,主要是通過ConditionObject對象實現Condition接口提供的方法實現。
  • 獨佔模式(Exclusive Mode):主要用於實現獨佔鎖,主要是基於靜態內部類Node的常量標誌EXCLUSIVE來標識該節點是獨佔模式
  • 共享模式(Shared Mode):主要用於實現共享鎖,主要是基於靜態內部類Node的常量標誌SHARED來標識該節點是共享模式

其中,對於AbstractQueuedSynchronizer類的實現原理,我們可以從如下幾個方面來看:


public abstract class AbstractQueuedSynchronizer
extends AbstractOwnableSynchronizer
implements java.io.Serializable {

    private static final long serialVersionUID = 7373984972572414691 L;


    protected AbstractQueuedSynchronizer() {}

    /**
     * 等待隊列: head-頭節點
     */
    private transient volatile Node head;

    /**
     * 等待隊列: tail-尾節點
     */
    private transient volatile Node tail;

    /**
     * 同步狀態:32位整數類型,更新同步狀態(state)時必須保證其是原子性的
     */
    private volatile int state;

    /**
     * 自旋鎖消耗超時時間閥值(threshold): threshold < 1000ns時,表示競爭時選擇自旋;threshold > 1000ns時,表示競爭時選擇系統阻塞
     */
    static final long spinForTimeoutThreshold = 1000 L;

    /**
     * CAS原子性操作
     */
    private static final Unsafe unsafe = Unsafe.getUnsafe();

    /**
     * stateOffset
     */
    private static final long stateOffset;

    /**
     * headOffset
     */
    private static final long headOffset;

    /**
     * tailOffset
     */
    private static final long tailOffset;

    /**
     * waitStatusOffset
     */
    private static final long waitStatusOffset;

    /**
     * nextOffset
     */
    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);
        }
    }
		
		    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);
    }
		
		    protected final boolean compareAndSetState(int expect, int update) {
        return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
    }

}

[1]. AbstractQueuedSynchronizer類的實現原理是繼承了基於AbstractOwnableSynchronizer類的抽象類,其中主要對AQS同步器的通用特性和方法進行抽象封裝定義,主要包括如下方法:

public abstract class AbstractOwnableSynchronizer
implements java.io.Serializable {


    private static final long serialVersionUID = 3737899427754241961 L;


    protected AbstractOwnableSynchronizer() {}

    /**
     *  同步器擁有者
     */
    private transient Thread exclusiveOwnerThread;

    /**
     * 設置同步器擁有者:把線程當作參數傳入,指定某個線程爲獨享
     */
    protected final void setExclusiveOwnerThread(Thread thread) {
        exclusiveOwnerThread = thread;
    }

    /**
     * 獲取同步器擁有者:獲取指定的某個線程
     */
    protected final Thread getExclusiveOwnerThread() {
        return exclusiveOwnerThread;
    }
}
  • setExclusiveOwnerThread(Thread thread)方法: 把某個線程作爲參數傳入,從而設置AQS同步器的所有者,即就是我們設置的某個線程
  • getExclusiveOwnerThread()方法: 獲取當前AQS同步器的所有者,即就是我們指定的某個線程

[2]. 對於同步狀態(state),其類型是32位整數類型,並且是被volatile修飾的,表示在更新同步狀態(state)時必須保證其是原子性的。

[3]. 對於等待隊列的結構,主要是在Node定義了head和tail變量,其中head表示頭部節點,tail表示尾部節點

[4].對於等待隊列的結構提到的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 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
       */
      volatile Node prev;

      /**
       * Link to the successor node that the current node/thread
       */
      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
       */
      Node nextWaiter;

      /**
       * Returns true if node is waiting in shared mode.
       */
      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的工作模式常量標記:主要維護了SHARED和EXCLUSIVE等2個靜態字面常量,其中 SHARED 用於標記Node中是共享模式,EXCLUSIVE:用於標記Node中是獨享模式
  • 標記等待狀態的靜態字面常量標記: 主要維護了0(表示無狀態),SIGNAL(-1,表示後續節點中的線程通過park進入等待,當前節點在釋放和取消時,需要通過unpark解除後後續節點的等待),CANCELLED(1,表示當前節點中的線程因爲超時和中斷被取消),CONDITION(-2,表示當前節點在條件隊列中),PROPAGATE(-3,SHARED共享模式的頭節點描述狀態,表示無條件往下傳播)等5個靜態字面常量
  • 維護了一個等待狀態(waitStatus): 主要用於描述等待隊列中節點的狀態,其取值範圍爲0(waitStatus=0,表示無狀態),SIGNAL(waitStatus=-1,表示等待信號狀態),CANCELLED(waitStatus=1,表示取消狀態),CONDITION(waitStatus=-2,表示條件狀態),PROPAGATE(waitStatus=-3,表示SHARED共享模式狀態)等5個靜態字面常量,CAS操作時寫入,默認值爲0。
  • 維護了Node的2個結構節點變量: 主要是prev和next,其中,prev表示前驅節點,next表示後續節點,表示構成雙向向鏈表,構成了等待隊列的數據結構
  • 維護了一個狀態工作模式標記: 主要是維護了一個nextWaiter,用於表示在等待隊列中當前節點在是共享模式還是獨享模式,而對於條件隊列來說,用於組成單向鏈表結構
  • 維護了一個線程對象變量: 主要用於記錄當前節點中的線程thread

[5].對於自旋鎖消耗超時時間閥值(spinForTimeoutThreshold),主要表示系統依據這個閥值來選擇自旋方式還是系統阻塞。一般假設這個threshold,當 threshold < 1000ns時,表示競爭時選擇自旋;否則,當threshold > 1000ns時,表示競爭時選擇系統阻塞

[6].對於帶有Offset 等變量對應各自的句柄,主要用於執行CAS操作。在JDK1.8版本之前,CAS操作主要通過Unsafe類來說實現;在JDK1.8版本之後,已經開始利用VarHandle來替代Unsafe類操作實現。

[7].對於CAS操作來說,主要提供瞭如下幾個方法:

  • compareAndSetState(int expect, int update)方法:CAS操作原子更新狀態
  • compareAndSetHead(Node update)方法:CAS操作原子更新頭部節點
  • compareAndSetTail(Node expect, Node update)方法:CAS操作原子更新尾部節點
  • compareAndSetWaitStatus(Node node, int expect,int update)方法:CAS操作原子更新等待狀態
  • compareAndSetNext(Node node,Node expect,Node update)方法:CAS操作原子更新後續節點

[8].除此之外,還包括許多輔助的操作方法,具體可參考源碼分析。

2. 基本實現

一個標準的AQS同步器最核心底層設計實現是一個非阻塞的CHL Node FIFO(先進先出)隊列數據結構,通過採用自旋鎖+CAS操作的方法來保證原子性操作。

總的來說,一個AQS基礎同步器,底層的數據結構採用的是一個非阻塞的CHL Node FIFO(先進先出)隊列數據結構,而實現的核心算法則是採用自旋鎖+CAS操作的方法。

首先,對於非阻塞的CHL Node FIFO(先進先出)隊列數據結構,一般來說,FIFO(First In First Out,先進先出)隊列是一個有序列表,屬於抽象型數據類型(Abstract Data Type,ADT),所有的插入和刪除操作都發生在隊首(Front)和隊尾(Rear)兩端,具有先進先出的特性。


    /**
     * 等待隊列: head-頭節點
     */
    private transient volatile Node head;

    /**
     * 等待隊列: tail-尾節點
     */
    private transient volatile Node tail;
		
    /**
     * Inserts node into queue, initializing if necessary. See picture above.
     * @param node the node to insert
     * @return node's predecessor
     */
    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;
                }
            }
        }
    }

    /**
     * Creates and enqueues node for current thread and given mode.
     *
     * @param mode Node.EXCLUSIVE for exclusive, Node.SHARED for shared
     * @return the new node
     */
    private Node addWaiter(Node mode) {
        Node node = new Node(Thread.currentThread(), mode);
        // Try the fast path of enq; backup to full enq on failure
        Node pred = tail;
        if (pred != null) {
            node.prev = pred;
            if (compareAndSetTail(pred, node)) {
                pred.next = node;
                return node;
            }
        }
        enq(node);
        return node;
    }

在AQS同步器的源碼中,主要是通過靜態內部類Node來實現的這個非阻塞的CHL Node FIFO(先進先出)隊列數據結構, 維護了兩個變量head和tail,其中head對應隊首(Front),tail對應隊尾(Rear)。同時,還定義了addWaiter(Node mode)方法來表示入隊操作,其中有個enq(final Node node)方法,主要用於初始化隊列中head和tail的設置。

其次,AQS同步器以CLH鎖爲基礎,其中CLH鎖是一種自旋鎖,對於自旋鎖的實現方式來看,主要可以分爲普通自旋鎖和自適應自旋鎖,CLH鎖和MCS鎖等4種,其中:

  • 普通自旋鎖:多個線程不斷自旋,不斷嘗試獲取鎖,其不具備公平性和由於要保證CPU和緩存以及主存之間的數據一致性,其開銷較大。
  • 自適應自旋鎖:主要是爲解決普通自旋鎖的公平性問題,引入了一個排隊機制,一般稱爲排他自旋鎖,其具備公平性,但是沒有解決保證CPU和緩存以及主存之間的數據一致性問題,其開銷較大。
  • CLH鎖:通過一定手段將線程對於某一個共享變量的輪詢競爭轉化爲一個線程隊列,且隊列中的線程各自輪詢自己本地變量。
  • MCS鎖:主旨在於解決 CLH鎖的問題,也是基於FIFO隊列,與CLH鎖不同是,只對本地變量自旋,前驅節點負責通知MCS鎖中線程自適結束。

自旋鎖是一種實現同步的方案,屬於一種非阻塞鎖,與常規鎖主要的區別就在於獲取鎖失敗之後的處理方式不同,主要體現在:

  • 一般情況下,常規鎖在獲取鎖失敗之後,會將線程阻塞並適當時重新喚醒
  • 而自旋鎖則是使用自旋來替換阻塞操作,主要是線程會不斷循環檢查該鎖是否被釋放,一旦釋放線程便會獲取鎖資源。

從本質上講,自旋是一鍾忙等待狀態,會一直消耗CPU的執行時間。一般情況下,常規互斥鎖適用於持有鎖長時間的情況,自旋鎖適合持有時間短的情況。

其中,對於CLH鎖來說,其核心是爲解決同步帶來的花銷問題,Craig,Landim,Hagersten三人發明了CLH鎖,其中主要是:

  • 構建一個FIFO(先進先出)隊列,構建時主要通過移動尾部節點tail來實現隊列的排隊,每個想獲得鎖的線程都會創建一個新節點(next)並通過CAS操作原子操作將新節點賦予給tail,當前線程輪詢前一個節點的狀態。
  • 執行完線程後,只需將當前線程對應節點狀態設置爲解鎖即可,主要是判斷當前節點是否爲尾部節點,如果是直接設置尾部節點設置爲空。由於下一個節點一直在輪詢,所以可以獲得鎖。

CLH鎖將衆多線程長時間對資源的競爭,通過有序化這些線程將其轉化爲只需要對本地變量檢測。唯一存在競爭的地方就是入隊之前對尾部節點tail 的競爭,相對來說,當前線程對資源的競爭次數減少,這節省了CPU緩存同步的消耗,從而提升了系統性能。

但是同時也有一個問題,CLH鎖雖然解決了大量線程同時操作同一個變量時帶來的開銷問題,如果前驅節點和當前節點在本地主存中不存在,則訪問時間過長,也會引起性能問題。

爲了讓CLH鎖更容易實現取消和超時的功能,AQS同步器在設計時進行了改造,主要體現在:節點的結構和節點等待機制。其中:

  • 節點的結構: 主要引入了頭節點和尾節點,分別指向隊列頭部和尾部,對於鎖的相關操作都與其息息相關,並且每個節點都引入了前驅節點和後繼節點。
  • 節點等待機制: 主要在原來的自旋基礎上增加了系統阻塞喚醒,主要體現在 自旋鎖消耗超時時間閥值(threshold): threshold < 1000ns時,表示競爭時選擇自旋;threshold > 1000ns時,表示競爭時選擇系統阻塞。

由此可見,主要是通過前驅節點和後繼節點的引用連接起來形成一個鏈表隊列,其中對於入隊,檢測節點,出隊,判斷超時,取消節點等操作主要如下:

  • 入隊(enqueue): 主要採用一個無限循環進行CAS操作,即就是使用自旋方式競爭直到成功。
  • 檢測節點(checkedPrev): 一般在入隊完成後,主要是檢測判斷當前節點的前驅節點是否爲頭節點, 一般自旋方式是直接進入循環檢測,而系統阻塞方式是當前線程先檢測,其中如果是頭節點併成功獲取鎖,則直接返回,當前線程不阻塞,否則對當前線程進行阻塞。
  • 出隊(dequeue):主要負責喚醒等待隊列中的後繼節點,並且按照條件往下傳播有序執行
  • 判斷超時(checkedTimeout): 隊列中等待鎖的線程可能因爲中斷或者超時的情況,當總耗時大於等於自定義耗時就直接返回,即就是
  • 取消節點(cancel): 主要是對於中斷和超時而涉及到取消操作,而且這樣的情況不再參與鎖競爭,即就是一般通過調用compareAndSetNext(Node node, Node expect,Node update)來進行CAS操作。

最後,AQS同步器中使用了CAS操作,其中CAS(Compare And Swap,比較並交換)操作時一種樂觀鎖策略,主要涉及三個操作數據:內存值,預期值,新值,主要是指當且僅當預期值和內存值相等時纔去修改內存值爲新值。

一般來說,CAS操作的具體邏輯,主要可以分爲三個步驟:

  • 首先,檢查某個內存值是否與該線程之前取到值一樣。
  • 其次,如果不一樣,表示此內存值已經被別的線程修改,需要捨棄本次操作。
  • 最後,如果時一樣,表示期間沒有線程更改過,則需要用新值執行更新內存值。

除此之外,需要注意的是CAS操作具有原子性,主要是由CPU硬件指令來保證,並且通過Java本地接口(Java Native Interface,JNI)調用本地硬件指令實現。

當然,CAS操作避免了悲觀策略獨佔對象的 問題,同時提高了併發性能,但是也有以下三個問題:

  • 樂觀策略只能保證一個共享變量的原子操作,如果是多個變量,CAS便不如互斥鎖,主要是CAS操作的侷限所致。
  • 長時間循環操作可能導致開銷過大。
  • 經典的ABA問題: 主要是檢查某個內存值是否與該線程之前取到值一樣,這個判斷邏輯不嚴謹。解決ABA問題的核心在於,引入版本號,每次更新變量值更新版本號。

而在AQS同步器中,爲了保證併發實現保證原子性,而且是硬件級別的原子性,一般是通過JNI(Java native interface,Java 本地接口)方式讓Java代碼調用C/C++本地代碼。

需要注意的是,在Java領域中,對於CAS操作實現,主要有亮點問題:

  • JDK1.8版本之前,CAS操作主要使用Unsafe類來執行底層操作,一般併發和線程操作時,主要用compareAndSwapObject,compareAndSwapInt,compareAndSwapLong等來實現CAS,而對於線程調度主要是park和unpark方法,其主要在sun.misc包下面。
  • JDK1.8版本之後,JDK1.9的CAS操作主要使用VarHandle類,只是用VarHandle替代了一部分Unsafe類的操作,但是對於新版本中Unsafe,本質上Unsafe類會間接調用jdk.internal.misc包下面Unsafe類來實現。

版權聲明:本文爲博主原創文章,遵循相關版權協議,如若轉載或者分享請附上原文出處鏈接和鏈接來源。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章