JUC(一)-AQS源碼分析


爲了學習JUC,AQS是基礎中的基礎,所以我們首先深入瞭解下AQS。

一、鎖的介紹

爲了瞭解AQS的源碼,我們需要先大概下鎖中的一些功能

1.1 樂觀鎖/悲觀鎖

樂觀鎖與悲觀鎖是一種廣義上的概念,體現了看待線程同步的不同角度,在java和數據庫中都有應用。

概念來講。對於悲觀鎖,不管當前是否有線程競爭,都會對當前數據進行加鎖,確保數據不會被別的線程修改,在java中,synchronized就是悲觀鎖

而樂觀鎖任務自己在使用數據時,不會有別的線程修改數據,所以不會添加鎖,只會在更新數據的時候去判斷之前有沒有別的線程更新了這個數據。如果這個數據沒有被更新,當前線程將自己修改過的數據成功寫入,如果數據已經被其他線程修改,則自動重試。

1.2 共享鎖/獨佔鎖

獨佔鎖也叫排他鎖,我們在對數據A加排他鎖後,其他線程則不能對A加任何類型的鎖。

共享鎖是指該鎖可以被多個線程持有。如果線程T對數據A加上共享鎖後,則其他線程只能對A再加共享鎖,不能加排他鎖。獲得共享鎖的線程只能讀數據,不能修改數據,可以參考ReadLock。

1.3 公平鎖/非公平鎖

對於線程來講,他最關心什麼?線程最關心的就是啥時候能夠執行,那麼這個線程是否能夠按照先來後到的順序執行就是公平鎖和非公平鎖的區別。

公平鎖是指多個線程按照申請鎖的順序來獲取鎖,線程直接進入隊列中排隊,隊列中的第一個線程能獲取到鎖。公平鎖的優點就是等待鎖的線程不會餓死。缺點就是整體吞吐效率相對非公平鎖要低。

非公平鎖就是多個線程加鎖時直接嘗試獲取鎖,獲取不到鎖纔會到等待隊列的隊尾等待。非公平鎖的優點是可以減少喚起線程的開銷,整體的吞吐效率高,因爲線程有可能不阻塞就可以鎖。缺點是處於等待隊列中的線程有可能餓死,可能這個線程永遠也輪不到他獲取鎖。

1.4 小結

AQS是根據CAS來完成鎖操作的樂觀鎖,鎖是否被持有,根據state值來判斷,內部維護了鎖的等待隊列,單個鎖節點,可以聲明爲獨佔節點或者共享節點,是否爲公平鎖,則要根據不同的獲取鎖的情況來自定義。

二、AQS框架結構介紹

2.1 類圖

在這裏插入圖片描述
exclusiveOwnerThread:保存當前持有獨佔鎖的線程

Node:隊列中的結點

ConditionObject:繼承Node,是Condition條件下使用的Node結點

Node head:隊列的頭部節點

Node tail:隊列的尾部節點

Int State:鎖的狀態值。如果0則代表當前鎖沒有被持有,如果state>0那麼就代表當前鎖已經被其他線程獲取了

SpinForTimeoutThreshold:自旋的超時時間

2.2 AQS數據結構

AQS內部維護了一個隊列,來存儲要獲取鎖的線程(這裏先不考慮Condition)。他們都去監控state的值,如果state值爲0,則代表當前鎖沒有被獲取,如果state>0,則代表當前鎖被獲取了。
在這裏插入圖片描述
隊列:是保存Node節點的雙向鏈表,它是CLH的變形隊列

Node節點的內部結構:

Node pre:當前節點的前一個節點

Node next:當前節點的下一個節點

Thread:節點所需要執行的線程

nextWaiter:鏈接到下一個等待的條件節點。這個節點和Condition有關係。

WaitStatus:節點的狀態

  • 0:初始化的值
  • 1:Cancelled,由於超時或者中斷,該節點被取消,節點永遠不會離開此狀態,特別的是,具有取消節點的線程將永遠不會被再次阻塞
  • -1:Signal,此節點的後繼節點(或即將被阻止)阻塞(通過Park),因此當前節點必須在其釋放或取消時取消其後續節點。爲了避免競爭,獲取方法必須首先表示它們需要一個信號,然後重試原子獲取,然後在失敗時阻塞。
  • -2:condition,該節點當前在條件隊列中,不會作用於同步隊列節點,直到轉移,此時狀態設置爲0。(此時使用此值與字段的其他用途無關,但簡化了機制)
  • -3:Propagate,一個realeaseShared節點應該傳播到其他節點,在(僅用於頭部節點)中設置doReleaseShared以確保傳播繼續,即使有其他操作介入

三、源碼詳解

3.1 acquire源碼詳解

在這裏插入圖片描述
1.調用自定義同步器的tryAcquire()嘗試直接去獲取資源,如果成功則直接返回;

protected final boolean tryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();
            //state爲0代表沒有加鎖
            if (c == 0) {
                //判斷Queue中是否有對象等待獲取鎖,如果對象中沒人獲得鎖則,默認自己獲取鎖
                if (!hasQueuedPredecessors() &&
                    compareAndSetState(0, acquires)) {
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            //state != 0 的情況下,代表當前鎖已經被其他線程佔用了,因爲是可重入鎖,所以需要判斷持有鎖的線程,和當前線程是否相同,如果相同則對state值進行處理
            else if (current == getExclusiveOwnerThread()) {
                int nextc = c + acquires;
                if (nextc < 0)
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
            //在state != 0,且並不是當前線程持有當前鎖,直接返回false
            return false;
        }

2.如果直接獲取鎖失敗,則從隊列中獲取鎖。使用addWaiter()將該線程加入等待隊列的尾部,並標記爲獨佔模式;
3.acquireQueued()使線程在等待隊列中休息,有機會時(輪到自己,會被unpark())會去嘗試獲取資源。獲取到資源後才返回。如果在整個等待過程中被中斷過,則返回true,否則返回false。

final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;
        try {
            //是否中斷默認爲false
            boolean interrupted = false;
            //無限for循環,直到獲得鎖
            for (;;) {
                //獲得當前節點的上節點
                final Node p = node.predecessor();
                //如果上個節點是head節點,那麼就嘗試獲得鎖
                if (p == head && tryAcquire(arg)) {
                    //成功獲得鎖,設置head節點爲當前node
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    //返回中斷狀態值
                    return interrupted;
                }
                //判斷是否要park當前線程,park後再判斷是否被中斷
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    //中斷值,標記爲true
                    interrupted = true;
            }
        } finally {
        	//如果try裏面有異常,那麼根據falied值,判斷是否需要cancelAcquire
            if (failed)
                cancelAcquire(node);
        }
    }

4.如果線程在等待過程中被中斷過,它是不響應的。只是獲取資源後纔再進行自我中斷selfInterrupt(),將中斷補上。

 public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }
static void selfInterrupt() {
        Thread.currentThread().interrupt();
    }

在if判斷滿足後,纔會把當前線程設置爲中斷的狀態。

3.2 release源碼詳解

在這裏插入圖片描述

  1. ReentranLock是獨佔鎖的可重入鎖,如果當前線程不是擁有獨佔鎖的線程的直接報錯,如果是的話,則判斷state的狀態是否爲0,如果爲0的話則設置擁有獨佔鎖的線程爲null,如果不爲0的話,則代表當前鎖還沒有被完全釋放。
	protected final boolean tryRelease(int releases) {
            int c = getState() - releases;
            if (Thread.currentThread() != getExclusiveOwnerThread())
                throw new IllegalMonitorStateException();
            boolean free = false;
            //如果state值爲0,纔算是釋放了,設置當前擁有鎖爲null,否則只能把state減1
            if (c == 0) {
                free = true;
                setExclusiveOwnerThread(null);
            }
            setState(c);
            return free;
        }
  1. 如果釋放成功則判斷是否要喚醒隊列中的線程
private void unparkSuccessor(Node node) {
        /*
         *如果狀態爲負(即可能需要信號),請嘗試
         *清除預期的信號。
         */
        int ws = node.waitStatus;
        if (ws < 0)
            compareAndSetWaitStatus(node, ws, 0);

        /*
         * 如果後繼節點取消了,那麼就從尾結點一直往上找,直到節點的watistatus爲非取消節點
         */
        Node s = node.next;
        if (s == null || s.waitStatus > 0) {
            s = null;
            for (Node t = tail; t != null && t != node; t = t.prev)
                if (t.waitStatus <= 0)
                    s = t;
        }
        if (s != null)
            //喚醒該線程
            LockSupport.unpark(s.thread);
    }

四、從ReentranLock看公平鎖和非公平鎖的實現區別

先看ReentranLock的類圖
在這裏插入圖片描述
Sync是ReentranLock的抽象內部類,FairSync和NonfairSync則是Sync的實現類,他們同樣也是ReentranLock的內部類。

4.1 公平鎖

公平鎖主要體現在加鎖過程

final void lock() {
            acquire(1);
        }

具體獲取鎖的時候先從隊列中獲取。

protected final boolean tryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();
            //state爲0代表沒有加鎖
            if (c == 0) {
                //判斷Queue中是否有對象等待獲取鎖
                if (!hasQueuedPredecessors() &&
                    compareAndSetState(0, acquires)) {
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            else if (current == getExclusiveOwnerThread()) {
                int nextc = c + acquires;
                if (nextc < 0)
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
            return false;
        }

4.2 非公平鎖

而非公平鎖的獲取鎖流程如下:

final void lock() {
            //嘗試CAS獲得鎖,如果獲取鎖成功,則將當前線程設置爲獨佔線程。
            if (compareAndSetState(0, 1))
                setExclusiveOwnerThread(Thread.currentThread());
            else
                acquire(1);
        }

先嚐試自己獲取鎖,如果獲取鎖失敗,最終調用nonfairTryAcquire獲取鎖

final boolean nonfairTryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();
            //0是代表沒有鎖的狀態
            if (c == 0) {
                //CAS比較獲取鎖
                if (compareAndSetState(0, acquires)) {
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            //如果有鎖,需要判斷是不是當前持有鎖的線程
            else if (current == getExclusiveOwnerThread()) {
                //在當前state上加1
                int nextc = c + acquires;
                if (nextc < 0) // overflow
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
            return false;
        }

如果這裏獲取鎖失敗,那麼纔會把節點追加到鏈表尾部。在鏈表尾部中,這個節點仍然會無限for循環獲得鎖,獲得鎖的代碼走的仍然是上面nonfairTryAcqires。

4.3 小結

公平鎖和非公平鎖在獲取鎖的差別主要是,公平鎖每次獲取鎖,都會先從隊列頭部獲取,而非公平鎖每次都是搶佔式的,自己先搶佔獲取鎖。

五、實戰

簡單使用ReentranLock寫一段代碼

public class ReenterLock implements Runnable {
    //使用ReentranLock加鎖
    private static ReentrantLock lock = new ReentrantLock();
    public static int i = 0;

    @Override
    public void run() {
        for (int j = 0; j < 10; j++) {
            lock.lock();
            //支持重入鎖
            //斷點位置
            lock.lock();
            try {
                i++;
            } finally {
                //執行兩次解鎖
                lock.unlock();
                lock.unlock();
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        ReenterLock tl = new ReenterLock();
        Thread t1 = new Thread(tl);
        Thread t2 = new Thread(tl);
        System.out.println(t1);
        System.out.println(t2);
        t1.start();t2.start();
        t1.join();t2.join();
        //輸出結果
        System.out.println(i);
    }
}

如上面的源代碼,我們將斷點打在斷點位置
在這裏插入圖片描述
0:可見t1是Thread-0,t2是Thread-1
1:當前持有鎖的是Thread-0
2:head節點的下一個節點指向Thread-1
3:tail節點是Thread-1
4:因爲是可重入鎖,當前已經加了2次鎖了,所以state的值爲2

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