1. 程式人生 > >死磕 java同步系列之AQS起篇

死磕 java同步系列之AQS起篇

問題

(1)AQS是什麼?

(2)AQS的定位?

(3)AQS的實現原理?

(4)基於AQS實現自己的鎖?

簡介

AQS的全稱是AbstractQueuedSynchronizer,它的定位是為Java中幾乎所有的鎖和同步器提供一個基礎框架。

AQS是基於FIFO的佇列實現的,並且內部維護了一個狀態變數state,通過原子更新這個狀態變數state即可以實現加鎖解鎖操作。

本章及後續章節的內容理解起來可能會比較晦澀,建議先閱讀彤哥上一章的內容【死磕 java同步系列之自己動手寫一個鎖Lock】。

核心原始碼

主要內部類

static final class Node {
    // 標識一個節點是共享模式
    static final Node SHARED = new Node();
    // 標識一個節點是互斥模式
    static final Node EXCLUSIVE = null;

    // 標識執行緒已取消
    static final int CANCELLED =  1;
    // 標識後繼節點需要喚醒
    static final int SIGNAL    = -1;
    // 標識執行緒等待在一個條件上
    static final int CONDITION = -2;
    // 標識後面的共享鎖需要無條件的傳播(共享鎖需要連續喚醒讀的執行緒)
    static final int PROPAGATE = -3;
    
    // 當前節點儲存的執行緒對應的等待狀態
    volatile int waitStatus;

    // 前一個節點
    volatile Node prev;
    
    // 後一個節點
    volatile Node next;

    // 當前節點儲存的執行緒
    volatile Thread thread;

    // 下一個等待在條件上的節點(Condition鎖時使用)
    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
        // 把共享模式還是互斥模式儲存到nextWaiter這個欄位裡面了
        this.nextWaiter = mode;
        this.thread = thread;
    }

    // 節點的構造方法
    Node(Thread thread, int waitStatus) { // Used by Condition
        // 等待的狀態,在Condition中使用
        this.waitStatus = waitStatus;
        this.thread = thread;
    }
}

典型的雙鏈表結構,節點中儲存著當前執行緒、前一個節點、後一個節點以及執行緒的狀態等資訊。

主要屬性

// 佇列的頭節點
private transient volatile Node head;
// 佇列的尾節點
private transient volatile Node tail;
// 控制加鎖解鎖的狀態變數
private volatile int state;

定義了一個狀態變數和一個佇列,狀態變數用來控制加鎖解鎖,佇列用來放置等待的執行緒。

注意,這幾個變數都要使用volatile關鍵字來修飾,因為是在多執行緒環境下操作,要保證它們的值修改之後對其它執行緒立即可見。

這幾個變數的修改是直接使用的Unsafe這個類來操作的:

// 獲取Unsafe類的例項,注意這種方式僅限於jdk自己使用,普通使用者是無法這樣呼叫的
private static final Unsafe unsafe = Unsafe.getUnsafe();
// 狀態變數state的偏移量
private static final long stateOffset;
// 頭節點的偏移量
private static final long headOffset;
// 尾節點的偏移量
private static final long tailOffset;
// 等待狀態的偏移量(Node的屬性)
private static final long waitStatusOffset;
// 下一個節點的偏移量(Node的屬性)
private static final long nextOffset;

static {
    try {
        // 獲取state的偏移量
        stateOffset = unsafe.objectFieldOffset
            (AbstractQueuedSynchronizer.class.getDeclaredField("state"));
        // 獲取head的偏移量
        headOffset = unsafe.objectFieldOffset
            (AbstractQueuedSynchronizer.class.getDeclaredField("head"));
        // 獲取tail的偏移量
        tailOffset = unsafe.objectFieldOffset
            (AbstractQueuedSynchronizer.class.getDeclaredField("tail"));
        // 獲取waitStatus的偏移量
        waitStatusOffset = unsafe.objectFieldOffset
            (Node.class.getDeclaredField("waitStatus"));
        // 獲取next的偏移量
        nextOffset = unsafe.objectFieldOffset
            (Node.class.getDeclaredField("next"));

    } catch (Exception ex) { throw new Error(ex); }
}

// 呼叫Unsafe的方法原子更新state
protected final boolean compareAndSetState(int expect, int update) {
    return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}

關於Unsafe類的講解請參考彤哥之前寫的【死磕 java魔法類之Unsafe解析】。

子類需要實現的主要方法

我們可以看到AQS的全稱是AbstractQueuedSynchronizer,它本質上是一個抽象類,說明它本質上應該是需要子類來實現的,那麼子類實現一個同步器需要實現哪些方法呢?

// 互斥模式下使用:嘗試獲取鎖
protected boolean tryAcquire(int arg) {
    throw new UnsupportedOperationException();
}
// 互斥模式下使用:嘗試釋放鎖
protected boolean tryRelease(int arg) {
    throw new UnsupportedOperationException();
}
// 共享模式下使用:嘗試獲取鎖
protected int tryAcquireShared(int arg) {
    throw new UnsupportedOperationException();
}
// 共享模式下使用:嘗試釋放鎖
protected boolean tryReleaseShared(int arg) {
    throw new UnsupportedOperationException();
}
// 如果當前執行緒獨佔著鎖,返回true
protected boolean isHeldExclusively() {
    throw new UnsupportedOperationException();
}

問題:這幾個方法為什麼不直接定義成抽象方法呢?

因為子類只要實現這幾個方法中的一部分就可以實現一個同步器了,所以不需要定義成抽象方法。

下面我們通過一個案例來介紹AQS中的部分方法。

基於AQS自己動手寫一個鎖

直接上程式碼:

public class MyLockBaseOnAqs {

    // 定義一個同步器,實現AQS類
    private static class Sync extends AbstractQueuedSynchronizer {
        // 實現tryAcquire(acquires)方法
        @Override
        public boolean tryAcquire(int acquires) {
            if (compareAndSetState(0, 1)) {
                setExclusiveOwnerThread(Thread.currentThread());
                return true;
            }
            return false;
        }
        // 實現tryRelease(releases)方法
        @Override
        protected boolean tryRelease(int releases) {
            setExclusiveOwnerThread(null);
            setState(0);
            return true;
        }
    }

    // 宣告同步器
    private final Sync sync = new Sync();

    // 加鎖
    public void lock() {
        sync.acquire(1);
    }

    // 解鎖
    public void unlock() {
        sync.release(1);
    }


    private static int count = 0;

    public static void main(String[] args) throws InterruptedException {
        MyLockBaseOnAqs lock = new MyLockBaseOnAqs();

        CountDownLatch countDownLatch = new CountDownLatch(1000);

        IntStream.range(0, 1000).forEach(i -> new Thread(() -> {
            lock.lock();

            try {
                IntStream.range(0, 10000).forEach(j -> {
                    count++;
                });
            } finally {
                lock.unlock();
            }
//            System.out.println(Thread.currentThread().getName());
            countDownLatch.countDown();
        }, "tt-" + i).start());

        countDownLatch.await();

        System.out.println(count);
    }
}

執行main()方法總是打印出10000000(一千萬),說明這個鎖也是可以直接使用的,當然這也是一個不可重入的鎖。

是不是很簡單,只需要簡單地實現AQS的兩個方法就完成了上一章彤哥自己動手實現的鎖的功能。

它是怎麼實現的呢?

我們這一章先不講原始碼,後面學習了ReentrantLock自然就明白了。

總結

這一章就到此結束了,本篇沒有去深入的解析AQS的原始碼,筆者認為這沒有必要,因為對於從來都沒有看過鎖相關的原始碼的同學來說,一上來就講AQS的原始碼肯定會一臉懵逼的,具體的原始碼我們穿插在後面的鎖和同步器的部分來學習,等所有跟AQS相關的原始碼學習完畢了,再來一篇總結。

下面總結一下這一章的主要內容:

(1)AQS是Java中幾乎所有鎖和同步器的一個基礎框架,這裡說的是“幾乎”,因為有極個別確實沒有通過AQS來實現;

(2)AQS中維護了一個佇列,這個佇列使用雙鏈表實現,用於儲存等待鎖排隊的執行緒;

(3)AQS中維護了一個狀態變數,控制這個狀態變數就可以實現加鎖解鎖操作了;

(4)基於AQS自己動手寫一個鎖非常簡單,只需要實現AQS的幾個方法即可。

彩蛋

上一章彤哥自己動手寫的鎖,其實可以看成是AQS的一個縮影,看懂了那個基本上AQS可以看懂一半了,因為彤哥那個裡面沒有寫Condition相關的內容,下一章ReentrantLock重入鎖中我們將一起學習Condition相關的內容。

所以呢,還是建議大家去看看這篇文章,點選下面的推薦閱讀可以直達。

推薦閱讀

  1. 死磕 java同步系列之自己動手寫一個鎖Lock

  2. 死磕 java魔法類之Unsafe解析

  3. 死磕 java同步系列之JMM(Java Memory Model)

  4. 死磕 java同步系列之volatile解析

  5. 死磕 java同步系列之synchronized解析


歡迎關注我的公眾號“彤哥讀原始碼”,檢視更多原始碼系列文章, 與彤哥一起暢遊原始碼的海洋。

相關推薦

java同步系列AQS

問題 (1)AQS是什麼? (2)AQS的定位? (3)AQS的實現原理? (4)基於AQS實現自己的鎖? 簡介 AQS的全稱是AbstractQueuedSynchronizer,它的定位是為Java中幾乎所有的鎖和同步器提供一個基礎框架。 AQS是基於FIFO的佇列實現的,並且內部維護了一個狀態變數sta

java同步系列AQS(面試)

問題 (1)AQS的定位? (2)AQS的重要組成部分? (3)AQS運用的設計模式? (4)AQS的總體流程? 簡介 AQS的全稱是AbstractQueuedSynchronizer,它的定位是為Java中幾乎所有的鎖和同步器提供一個基礎框架。 在之前的章節中,我們一起學習了ReentrantLock、R

java同步系列開篇

討論 關註 使用 避免死鎖 更新數據 讀寫 上下文切換 monit 缺點 簡介 同步系列,這是彤哥想了好久的名字,本來是準備寫鎖相關的內容,但是java中的CountDownLatch、Semaphore、CyclicBarrier這些類又不屬於鎖,它們和鎖又有很多共同點,

java同步系列JMM(Java Memory Model)

簡介 Java記憶體模型是在硬體記憶體模型上的更高層的抽象,它遮蔽了各種硬體和作業系統訪問的差異性,保證了Java程式在各種平臺下對記憶體的訪問都能達到一致的效果。 硬體記憶體模型 在正式講解Java的記憶體模型之前,我們有必要先了解一下硬體層面的一些東西。 在現代計算機的硬體體系中,CPU的運算速度是非常快

java同步系列volatile解析

問題 (1)volatile是如何保證可見性的? (2)volatile是如何禁止重排序的? (3)volatile的實現原理? (4)volatile的缺陷? 簡介 volatile可以說是Java虛擬機器提供的最輕量級的同步機制了,但是它並不容易被正確地理解,以至於很多人不習慣使用它,遇到多執行緒問題一律

java同步系列synchronized解析

問題 (1)synchronized的特性? (2)synchronized的實現原理? (3)synchronized是否可重入? (4)synchronized是否是公平鎖? (5)synchronized的優化? (6)synchronized的五種使用方式? 簡介 synchronized關鍵字是Ja

java同步系列自己動手寫一個鎖Lock

問題 (1)自己動手寫一個鎖需要哪些知識? (2)自己動手寫一個鎖到底有多簡單? (3)自己能不能寫出來一個完美的鎖? 簡介 本篇文章的目標一是自己動手寫一個鎖,這個鎖的功能很簡單,能進行正常的加鎖、解鎖操作。 本篇文章的目標二是通過自己動手寫一個鎖,能更好地理解後面章節將要學習的AQS及各種同步器實現的原理

java同步系列ReentrantLock原始碼解析(一)——公平鎖、非公平鎖

問題 (1)重入鎖是什麼? (2)ReentrantLock如何實現重入鎖? (3)ReentrantLock為什麼預設是非公平模式? (4)ReentrantLock除了可重入還有哪些特性? 簡介 Reentrant = Re + entrant,Re是重複、又、再的意思,entrant是enter的名詞或

java同步系列ReentrantLock原始碼解析(二)——條件鎖

問題 (1)條件鎖是什麼? (2)條件鎖適用於什麼場景? (3)條件鎖的await()是在其它執行緒signal()的時候喚醒的嗎? 簡介 條件鎖,是指在獲取鎖之後發現當前業務場景自己無法處理,而需要等待某個條件的出現才可以繼續處理時使用的一種鎖。 比如,在阻塞佇列中,當佇列中沒有元素的時候是無法彈出一個元素

java同步系列ReentrantLock VS synchronized——結果可能跟你想的不一樣

問題 (1)ReentrantLock有哪些優點? (2)ReentrantLock有哪些缺點? (3)ReentrantLock

java同步系列ReentrantReadWriteLock原始碼解析

問題 (1)讀寫鎖是什麼? (2)讀寫鎖具有哪些特性? (3)ReentrantReadWriteLock是怎麼實現讀寫鎖的? (4)如何使用ReentrantReadWriteLock實現高效安全的TreeMap? 簡介 讀寫鎖是一種特殊的鎖,它把對共享資源的訪問分為讀訪問和寫訪問,多個執行緒可以同時對共享

java同步系列Semaphore原始碼解析

問題 (1)Semaphore是什麼? (2)Semaphore具有哪些特性? (3)Semaphore通常使用在什麼場景中? (

java同步系列StampedLock原始碼解析

問題 (1)StampedLock是什麼? (2)StampedLock具有什麼特性? (3)StampedLock是否支援可重入

java同步系列CyclicBarrier原始碼解析——有圖有真相

問題 (1)CyclicBarrier是什麼? (2)CyclicBarrier具有什麼特性? (3)CyclicBarrier與

java同步系列Phaser原始碼解析

問題 (1)Phaser是什麼? (2)Phaser具有哪些特性? (3)Phaser相對於CyclicBarrier和Count

java同步系列mysql分散式鎖

問題 (1)什麼是分散式鎖? (2)為什麼需要分散式鎖? (3)mysql如何實現分散式鎖? (4)mysql分散式鎖的優點和缺點? 簡介 隨著併發量的不斷增加,單機的服務遲早要向多節點或者微服務進化,這時候原來單機模式下使用的synchronized或者ReentrantLock將不再適用,我們迫切地需要一

java同步系列zookeeper分散式鎖

(2)zookeeper分散式鎖有哪些優點? (3)zookeeper分散式鎖有哪些缺點? 簡介 zooKeeper是一個分散式的,開放原始碼的分散式應用程式協調服務,它可以為分散式應用提供一致性服務,它是Hadoop和Hbase的重要元件,同時也可以作為配置中心、註冊中心運用在微服務體系中。 本章我們將介

java同步系列redis分散式鎖進化史

(2)redis分散式鎖有哪些優點? (3)redis分散式鎖有哪些缺點? (4)redis實現分散式鎖有沒有現成的輪子可以使用? 簡介 Redis(全稱:Remote Dictionary Server 遠端字典服務)是一個開源的使用ANSI C語言編寫、支援網路、可基於記憶體亦可持久化的日誌型、Key-

java同步系列終結

腦圖 下面是關於同步系列的一份腦圖,列舉了主要的知識點和問題點,看過本系列文章的同學可以根據腦圖自行回顧所學的內容,也可以作為面試前的準備。 如果有需要高清無碼原圖的同學,可以關注公眾號“彤哥讀原始碼”,回覆“sync”領取。 總結 所謂同步,就是保證多執行緒(包括多程序)對共享資源的讀寫能夠安全有效的執