AQS學習筆記

AQS 學習


AQS就是AbstractQueuedSynchronizer,抽象的隊列同步器,AQS實現了對同步狀態的管理,以及對阻塞線程進行排隊,等待通知等等一些底層的實現處理。AQS的核心也包括了這些方面:同步隊列,獨佔式鎖的獲取和釋放,共享鎖的獲取和釋放以及可中斷鎖,超時等待鎖獲取這些特性的實現

前置知識

//這是AQS的三個很重的變量,也可以說明等待隊列底層其實是一個鏈表  
//頭節點,不保存真實信息,只表示位置,不代表實際的等待線程
private transient volatile Node head;
//尾節點
private transient volatile Node tail;
//狀態
private volatile int state;

可以看一下這個圖,加深對head和tail的理解

img

FairSync的繼承結構,它其實是間接繼承自AQS的

image-20200319163345341

lock加鎖過程

這裏以FairSync(公平鎖)的lock方法爲例,解析一下lock過程,以及AQS中的一些基礎方法

acquire方法

//FairSync的lock方法
final void lock() {
    acquire(1);
}
/*
AQS的accquire方法
1. 會先調用tryAcquire方法,這個方法是在AQS中並沒有用具體的實現,只派出了一個異常,在FairSync中重寫了這方法,
2. tryAcquire會先嚐試獲得鎖,如果返回true,也就是獲得鎖成功,導致邏輯端路,那麼結束了
3. 如果tryAcquire返回false,也就是獲得鎖失敗,就要往阻塞隊列添加當前線程,這是addWaiter方法實現的功能,具體參考下方源碼分析
4. acquireQueued方法可以對排隊中的線程進行“獲鎖”操作。
*/

 public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            //如果返回打斷,就要執行下面的代碼
            //也就是說	獲取了鎖以後還要中斷線程
            selfInterrupt();
    }

tryAcquire方法

/*
這個方法的作用是嘗試獲得鎖,如果獲得成功就返回true,失敗則返回false
*/
protected final boolean tryAcquire(int acquires) {
    		//獲得當前線程	
            final Thread current = Thread.currentThread();
    		//state是AQS一個很重要的變量,爲0表示鎖還沒有被獲取
            int c = getState();
            if (c == 0) {
                //如果返回了false, 就說明可以嘗試獲得鎖
                if (!hasQueuedPredecessors() &&
                    //compareAndSetState方法就是以CAS的方式設置state,保證原子性
                    compareAndSetState(0, acquires)) {
                    //設置當前擁有獨佔訪問的線程,這個方法內部就一句話,應該不需要解釋了吧
                    //clusiveOwnerThread = thread;
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
    		//如果c==1,就是鎖已被獲得,那就查看獲得鎖的線程是不是當前線程
            else if (current == getExclusiveOwnerThread()) {
                //一般就是+1,可重入鎖的表現
                int nextc = c + acquires;
                //int溢出爲負數,也就是超過最大鎖數
                if (nextc < 0)
                    throw new Error("Maximum lock count exceeded");
                //那就增加可重入鎖的參數
                //另外說一句,這邊爲什麼沒有采用CAS的方式設置state,我覺得是因爲這裏滅有併發差生的問					//因爲最多隻有一個線程能夠進入到這個代碼塊
                setState(nextc);
				//返回true表示獲得鎖成功
                return true;
            }
    		//要麼就是c=0,但是自旋設置鎖,要麼就是鎖已被佔用
            return false;
        }

hasQueuedPredecessors方法

/*
判斷阻塞隊列是否有等待的線程,如果有返回true,沒有返回false
1. 如果h==t 說明還沒有初始化,h和t都是null,或者是初始化了,但是隊列中還沒有元素
2. (s = h.next) == null,這個判斷是爲了在併發情況下可能會產生的問題,詳細問題可以看下面的enq方法
3. s.thread != Thread.currentThread()說明阻塞隊列的第一個有效節點線程與當前線程不同,當前線程必須加入進等待隊列,也就是有比當前線程優先級高的線程
*/
public final boolean hasQueuedPredecessors() {
    Node t = tail; // Read fields in reverse initialization order
    Node h = head;
    Node s;
    return h != t &&
        ((s = h.next) == null || s.thread != Thread.currentThread());
}

enq方法

/*
入隊操作並不是一個原子操作,所以就會存在初始化完成,也就是進入else代碼塊,將node節點設置爲tail,使得head!=tail,但是t.next=node並沒有執行,也就是導致hasQueuedPredecessors中(s = h.next) == null的問題,這是需要判斷出阻塞隊列中有元素的,只是因爲在併發環境下,設置了tail指向head節點的信息,但沒有設置head指向tail指針。
這段話解釋hasQueuedPredecessors方法中的問題
——————————————————————————————————————————————————————————————————————————————————
我們由下面的分析可以知道,進入enq有兩個可能一個是沒有初始化,一個是CAS競爭失敗,都會在這個得到體現
*/
private Node enq(final Node node) {
        for (;;) {
            Node t = tail;
         	//沒有初始化
            if (t == null) { // Must initialize
                if (compareAndSetHead(new Node()))
                    tail = head;
            } 
            /*進到這裏有兩種可能
            1. 沒有初始化,那麼初始化完成之後,會同時添加node到阻塞隊列
            2. CAS競爭失敗進入到這裏,那麼就會再次嘗試CAS的方式添加到隊尾,注意這是個死循環,只有當自旋				成功的時候,纔會return
            */
            //addWaiter一樣的操作,不過是變量名換了一下
            else {
                node.prev = t;
                if (compareAndSetTail(t, node)) {
                    t.next = node;
                    return t;
                }
            }
        }
    }

compareAndSetTail方法

就是CAS的實現。

compareAndSetTail方法,完成尾節點的設置。這個方法主要是對tailOffset和Expect進行比較,如果tailOffset的Node和Expect的Node地址是相同的,那麼設置Tail的值爲Update的值。

addWaiter方法

/*
1. 傳入的參數mode是Node.EXCLUSIVE,是AQS中Node內部類的一個常量
 static final Node EXCLUSIVE = null;

*/
private Node addWaiter(Node mode) {
 		   //	獲得當前線程封裝成Node
        Node node = new Node(Thread.currentThread(), mode);
        // 獲得tail尾節點,嘗試往阻塞裏添加節點
        Node pred = tail;
    	//如果pred也就是tail!=null,說明已經初始化過了
    	//PS:這下面的代碼其實和enq的那段代碼邏輯基本一樣
        if (pred != null) {
            //設置當前node節點的前驅
            node.prev = pred;
            //CAS設置tail
            if (compareAndSetTail(pred, node)) {
                //設置pred的後繼節點
                pred.next = node;
                //返回新加的節點
                return node;
            }
        }
    //到了這一步,只能說明阻塞隊列沒有初始化,或色是CAS失敗,也就是有線程在競爭這個資源
    //enq方法可以回顧上面的分析
        enq(node);
        return node;
    }

acquireQueued方法

//返回是否打斷
final boolean acquireQueued(final Node node, int arg) {
 	//標誌是否獲得成功,默認失敗
    boolean failed = true;
    try {
        //標誌是否被打斷,默認沒有
        boolean interrupted = false;
        //死循環
        for (;;) {
            //獲得node節點的前驅
            final Node p = node.predecessor();
            //如果node的前驅p是head,說明可以嘗試去獲得鎖
            if (p == head && tryAcquire(arg)) {
              //進入這裏,獲得成功,設置head
                  setHead(node);
                p.next = null; // help GC
                failed = false;
                return interrupted;
            }
            //獲得鎖失敗要麼是p是不是頭節點,沒有資格獲得鎖,要麼就是p是頭節點,但是被別的線程搶先獲得鎖了(可能是非公平鎖被搶佔了),就判斷是否要掛起線程
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

shouldParkAfterFailedAcquire方法

/*
傳進來2個參數,一個node的前驅結點pred,一個node節點
*/
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
   	//獲得pred的waitStatus的狀態
    int ws = pred.waitStatus;
    //如果是Node.SIGNAL,也就是-1,那麼就說明當前節點可以被阻塞
    if (ws == Node.SIGNAL)
        return true;
    // >0 就是取消狀態,說明當前節點前面的節點已經取消了,就要往前找到一個沒有被取消的節點
    if (ws > 0) {
        do {
            node.prev = pred = pred.prev;
        } while (pred.waitStatus > 0);
        pred.next = node;
    //設置前一個節點爲SIGNAL狀態,下一次進來的時候就會return
    } else {
        compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
    }
    return false;
}

parkAndCheckInterrupt方法

private final boolean parkAndCheckInterrupt() {
    LockSupport.park(this);//阻塞線程
    //線程被重新喚醒後執行
    return Thread.interrupted();
}

關於加鎖的流程圖

  1. 線程A進入,發現可以獲得鎖,那就獲得鎖,並沒有對阻塞隊列做任何處理

  2. 線程B進入,不能獲得鎖,那麼就要加入到阻塞隊列中

    發現阻塞隊列還沒有初始化,那就先初始化,如下圖

aqs-1

阻塞隊列初始化完成,就將線程B加入到阻塞隊列中,同時設置前面節點的waitStatus爲-1,表示後繼結點需要 被喚醒。

aqs-2
  1. 線程C加入,不能獲得鎖,那就加入到阻塞隊列,發現阻塞隊列已經初始化,那就直接加到隊尾,比設置前驅節點的waitStatus爲-1,表示線程C需要被喚醒
aqs-3

unlock解鎖過程

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

release方法

public final boolean release(int arg) {
    //鎖被完全釋放了,喚醒阻塞隊列中的節點
    if (tryRelease(arg)) {
        Node h = head;
        /*
        h == null Head還沒初始化。初始情況下,head == null,第一個節點入隊,Head會被初始化一個虛擬節		點。所以說,這裏如果還沒來得及入隊,就會出現head == null 的情況。
		h != null && waitStatus == 0 表明後繼節點對應的線程仍在運行中,不需要喚醒。
		h != null && waitStatus < 0 表明後繼節點可能被阻塞了,需要喚醒。
        */
        if (h != null && h.waitStatus != 0)
          	//解除阻塞
            unparkSuccessor(h);
        return true;
    }
    return false;
}

tryRelease方法

//返回true,完全釋放鎖,返回false,沒有完全釋放鎖
protected final boolean tryRelease(int releases) {
  	//主要是針對可重入鎖的次數
    int c = getState() - releases;
	//當前線程不是持有鎖的線程,拋出異常
    if (Thread.currentThread() != getExclusiveOwnerThread())
        throw new IllegalMonitorStateException();
   //因爲是可重入鎖,判斷是否完全釋放鎖
    boolean free = false;
    //c==0 鎖完全釋放了,打標記,設置擁有線程爲null
    if (c == 0) {
        free = true;
        setExclusiveOwnerThread(null);
    }
    //設置state
    setState(c);
    return free;
}

unparkSuccessor方法

private void unparkSuccessor(Node node) {

    int ws = node.waitStatus;
    //即-1 ,那就設置爲0
    if (ws < 0)
        compareAndSetWaitStatus(node, ws, 0);

    Node s = node.next;
    //如果下個節點是null或者下個節點被cancelled,就找到隊列最開始的非cancelled的節點
    if (s == null || s.waitStatus > 0) {
        s = null;
        /*
        倒着遍歷是因爲,在addWaiter方法中,由於是雙向鏈表,那麼必然會有1->2,2->1的過程
       		 node.prev = pred;
             pred.next = node;
             上面兩句是來自於addWaiter方法,我們可以看到是先設置前驅指針,然後設置後繼指針,
             也就是在併發情況下,可能存在只設置了前驅指針,當腰設置後繼的時候,線程就被切換了,導致還沒有設			置.
             那麼,這裏的倒着遍歷就可以理解了,防止沒有遍歷到所有的元素.
        */
        for (Node t = tail; t != null && t != node; t = t.prev)
            if (t.waitStatus <= 0)
                s = t;
    }
    //解除一個阻塞線程
    if (s != null)
        LockSupport.unpark(s.thread);
}

Condition相關

操作系統中有一個關於生產者-消費者的例子,在java中可以利用condition實現

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

class BoundedBuffer {
    final Lock lock = new ReentrantLock();
    // condition 依賴於 lock 來產生
    final Condition notFull = lock.newCondition();
    final Condition notEmpty = lock.newCondition();

    final Object[] items = new Object[100];
    int putptr, takeptr, count;

    // 生產
    public void put(Object x) throws InterruptedException {
        lock.lock();
        try {
            while (count == items.length)
                notFull.await();  // 隊列已滿,等待,直到 not full 才能繼續生產
            items[putptr] = x;
            if (++putptr == items.length) putptr = 0;
            ++count;
            notEmpty.signal(); // 生產成功,隊列已經 not empty 了,發個通知出去
        } finally {
            lock.unlock();
        }
    }

    // 消費
    public Object take() throws InterruptedException {
        lock.lock();
        try {
            while (count == 0)
                notEmpty.await(); // 隊列爲空,等待,直到隊列 not empty,才能繼續消費
            Object x = items[takeptr];
            if (++takeptr == items.length) takeptr = 0;
            --count;
            notFull	.signal(); // 被我消費掉一個,隊列 not full 了,發個通知出去
            return x;
        } finally {
            lock.unlock();
        }
    }
}

這其中的newCondition方法如下

final ConditionObject newCondition() {
    return new ConditionObject();
}

其中ConditionObject是AQS的一個內部類,有兩個比較重要的常量

private transient Node firstWaiter;
private transient Node lastWaiter;

我們知道在AQS中存在着一個阻塞隊列(本質上是雙向鏈表),裏面存放想要獲得鎖但是還沒有獲得鎖的線程。

在我們使用condition的時候,引入一個新的隊列,叫條件隊列,這是一個單向鏈表(可以查看Node類,裏面有一個nextWaiter屬性從側面印證了這一點)。

關於nextWaiter屬性

其實nextWaiter的作用並不只是表示條件隊列,在AQS的獨佔鎖和共享鎖的模式下,用於表示這兩種不同的節點。

// 共享模式
static final Node SHARED = new Node();
// 獨佔模式
static final Node EXCLUSIVE = null;
// 其他模式
// 其他非空值:條件等待節點(調用Condition的await方法的時候)

下圖的condition1和condition2都是條件隊列。

condition-2

大致流程

  1. 調用await,將一個線程放入條件隊列。
  2. 某一個線程調用signal,喚醒一個線程,從條件隊列中取出隊首的放入阻塞隊列(其實在java中稱爲Sync隊列)。

await方法

public final void await() throws InterruptedException {
    //清除中斷信息,如果當前已經中斷,則拋出異常
    if (Thread.interrupted())
        throw new InterruptedException();
    //將當前節點加入到等待隊列
    Node node = addConditionWaiter();
    //這個方法的作用就是完全釋放鎖,返回state次數
    int savedState = fullyRelease(node);
    int interruptMode = 0;
    //比較重要的一個while,下面方法的分析
    while (!isOnSyncQueue(node)) {
        /*
        掛起線程
       	在第一次進入的時候,isOnSyncQueue是false,表示node還在條件隊列中,那麼需要喚醒,所以從
       	流程上來說,只有喚醒了才能進行下面的代碼,所以下面分析signal方法
        */
        LockSupport.park(this);
        /*
        在經過signal之後,同步隊列中被放入一個線程,這個線程總會有被重新激活的時候,如果重新激活了,			就會執行下面代碼
        */
        if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
            break;
    }
    //之前釋放了鎖,現在要重新獲得
    if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
        interruptMode = REINTERRUPT;
    /*checkInterruptWhileWaiting中,我們判斷如果中斷髮生在signal前也會將節點加入到阻塞隊列,但這個是		沒有設置nextWaiter=null的,在doSignal中是設置了的
    */
    if (node.nextWaiter != null) // clean up if cancelled
        unlinkCancelledWaiters();
    if (interruptMode != 0)
        reportInterruptAfterWait(interruptMode);
}

帶超時機制的await

/*
加入了time,引入了超時機制,意思是如果超時了,就會主動加入到阻塞隊列
*/
public final boolean await(long time, TimeUnit unit)
        throws InterruptedException {
    long nanosTimeout = unit.toNanos(time);
    if (Thread.interrupted())
        throw new InterruptedException();
    Node node = addConditionWaiter();
    int savedState = fullyRelease(node);
    final long deadline = System.nanoTime() + nanosTimeout;
    boolean timedout = false;
    int interruptMode = 0;
    while (!isOnSyncQueue(node)) {
        if (nanosTimeout <= 0L) {
            timedout = transferAfterCancelledWait(node);
            break;
        }
        if (nanosTimeout >= spinForTimeoutThreshold)
            LockSupport.parkNanos(this, nanosTimeout);
        if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
            break;
        nanosTimeout = deadline - System.nanoTime();
    }
    if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
        interruptMode = REINTERRUPT;
    if (node.nextWaiter != null)
        unlinkCancelledWaiters();
    if (interruptMode != 0)
        reportInterruptAfterWait(interruptMode);
    return !timedout;
}

addConditionWaiter方法

private Node addConditionWaiter() {
    Node t = lastWaiter;
    /*
    如果t==null 說明條件隊列還沒有元素
     t.waitStatus != Node.CONDITION,可能存在cancel的節點,那就要清理cancel節點
    */
    if (t != null && t.waitStatus != Node.CONDITION) {
        unlinkCancelledWaiters();
      	//lastWaiter可能有變化,重新獲得
        t = lastWaiter;
    }
    //封裝當前線程節點
    Node node = new Node(Thread.currentThread(), Node.CONDITION);
   	//t==null 說明條件隊列還沒有元素,那就設置第一個節點元素
    if (t == null)
        firstWaiter = node;
    //否則就在尾部接一個上去
    else
        t.nextWaiter = node;
    //更新尾節點
    lastWaiter = node;
    return node;
}

unlinkCancelledWaiters方法

//這個就是清理無效節點的操作,就是一個普通鏈表的刪除操作
private void unlinkCancelledWaiters() {
    Node t = firstWaiter;
    Node trail = null;
    while (t != null) {
        Node next = t.nextWaiter;
        if (t.waitStatus != Node.CONDITION) {
            t.nextWaiter = null;
            if (trail == null)
                firstWaiter = next;
            else
                trail.nextWaiter = next;
            if (next == null)
                lastWaiter = trail;
        }
        else
            trail = t;
        t = next;
    }
}

fullyRelease方法

final int fullyRelease(Node node) {
    //標記是否釋放失敗,默認爲失敗
    boolean failed = true;
    try {
        //獲得state的值
        int savedState = getState();
        //一次性釋放,不再每次減1,這次是直接嘗試減state
        if (release(savedState)) {
            failed = false;
            return savedState;
        } 
        //如果當前是沒有獲得鎖的,就拋出異常
        else {
            throw new IllegalMonitorStateException();
        }
    } finally {
        //設置節點狀態時CANCELLED
        if (failed)
            node.waitStatus = Node.CANCELLED;
    }
}

isOnSyncQueuef方法

/*
這個方法是判斷node是否在同步隊列(也就是我一直說的阻塞隊列)
*/
final boolean isOnSyncQueue(Node node) {
    /*node.waitStatus == Node.CONDITION ,很明顯還是條件隊列中節點
    node.prev == null,pre屬性是阻塞隊列的一個屬性,我們在前面說過,將一個節點加入到阻塞隊列這個雙		向鏈表,是個非原子操作,是先設置prev,再設置next,所以只需要判斷prev是否爲null,也能判斷是否進入阻	 塞隊列
   */
   if (node.waitStatus == Node.CONDITION || node.prev == null)
        return false;
    //同上面所說,如果next都有值了,那麼就進入阻塞隊列了
    if (node.next != null) 
        return true;
    return findNodeFromTail(node);
}

findNodeFromTail方法

//這個方法就是從尾節點開始遍歷阻塞隊列,判斷node是否在阻塞隊列中
private boolean findNodeFromTail(Node node) {
    Node t = tail;
    for (;;) {
        if (t == node)
            return true;
        if (t == null)
            return false;
        t = t.prev;
    }
}

signal方法

public final void signal() {
    //要調用signal就肯定要持有鎖纔行,就在這裏判斷,如果沒有鎖,就拋出異常
    if (!isHeldExclusively())
        throw new IllegalMonitorStateException();
    Node first = firstWaiter;
    //如果firstWaiter==null,也就是條件隊列沒有內容,自然不用喚醒
    if (first != null)
        doSignal(first);
}

isHeldExclusively方法

//方法很簡單,就是判斷當前線程是不是持有鎖的線程
protected final boolean isHeldExclusively() {
    return getExclusiveOwnerThread() == Thread.currentThread();
}

doSignal方法

private void doSignal(Node first) {
    do {
        //如果當前條件隊列只有一個元素,那就清空
        if ( (firstWaiter = first.nextWaiter) == null)
            lastWaiter = null;
		//斷開節點
        first.nextWaiter = null;
    } while (!transferForSignal(first) &&
             (first = firstWaiter) != null);
}

順帶說一下doSignalAll方法

//可以看到與doSignal方法最大的區別就是doSignalAll是將所有能夠加入到阻塞隊列中的節點都加進去
private void doSignalAll(Node first) {
    lastWaiter = firstWaiter = null;
    do {
        Node next = first.nextWaiter;
        first.nextWaiter = null;
        transferForSignal(first);
        first = next;
    } while (first != null);
}

transferForSignal方法

final boolean transferForSignal(Node node) {
    //如果將當前節點從CONDITION設置爲0失敗,那麼就會返回while語句,繼續尋找一個新的節點喚醒,也就是說如果失敗,那麼就放棄這個節點
    if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
        return false;
    //進入到這裏說明說明已經設置條件隊列中的第一個元素的status是0,那麼就把這個節點後加入到阻塞隊列
    //注意,返回的p是尾節點的前驅節點,並不是尾節點
    Node p = enq(node);
    
    int ws = p.waitStatus;
	//大於0就說明是CACAELLED,或者就需要把waitStatus設置爲SIGNAL,以方便在阻塞隊列中的喚醒
    if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
      	//喚醒當前線程
        LockSupport.unpark(node.thread);
    return true;
}

關於這段代碼的 LockSupport.unpark(node.thread);多說一句。

這邊的兩個判斷條件一定概率上處於優化作用,因爲就算不執行,那麼在signal之後,也會將一個等待隊列中的節點加入到阻塞隊列。在阻塞隊列中的節點同樣有希望解除掛起狀態,重新獲得鎖,繼續執行await中剩下的代碼。這裏就是直接喚醒,減少了再去阻塞隊列中激活線程的一步。

checkInterruptWhileWaiting方法

//判斷是否被中斷
/*
三種不同的返回值
REINTERRUPT: 代表 await 返回的時候,需要重新設置中斷狀態
THROW_IE:	 代表 await 返回的時候,需要拋出 InterruptedException 異常
0 :			 說明在 await 期間,沒有發生中斷
*/
private int checkInterruptWhileWaiting(Node node) {
    return Thread.interrupted() ?
        (transferAfterCancelledWait(node) ? THROW_IE : REINTERRUPT) :
        0;
}

transferAfterCancelledWait方法

final boolean transferAfterCancelledWait(Node node) {
  	//如果設置成功,說明中斷是在signal前發生的,因爲signal會將waitStatus設爲0
    if (compareAndSetWaitStatus(node, Node.CONDITION, 0)) {
       //也會將節點加入到阻塞隊列
        enq(node);
        return true;
    }
    //要等到node加入到阻塞隊列,才產生返回
    while (!isOnSyncQueue(node))
        Thread.yield();
    return false;
}

transferAfterCancelledWait方法並不在ConditionObject中定義,而是由AQS提供。這個方法根據是否中斷髮生時,是否有signal操作來“摻和”來返回結果。方法調用CAS操作將node的waitStatus從CONDITION設置爲0,如果成功,說明當中斷髮生時,說明沒有signal發生(signal的第一步是將node的waitStatus設置爲0),在調用enq將線程放入Sync隊列後直接返回true,表示中斷先於signal發生,即中斷在await等待過程中發生,根據await的語義,在遇到中斷時需要拋出中斷異常,返回true告訴上層方法返回THROW_IT,後續會根據這個返回值做拋出中斷異常的處理。

如果CAS操作失敗,是否說明中斷後於signal發生呢?只能說這時候我們不能確定中斷和signal到底誰先發生,只是在我們做CAS操作的時候,他們倆已經都發生了(中斷->interrupted檢測->signal->CAS,或者signal->中斷->interrupted檢測->CAS都有可能),這時候我們無法判斷到底順序是怎樣,這裏的處理是不管怎樣都返回false告訴上層方法返回REINTERRUPT,當做是signal先發生(線程被signal喚醒)來處理,後續根據這個返回值做“補上”中斷的處理。在返回false之前,我們要先做一下等待,直到當前線程被成功放入Sync鎖等待隊列。

源自:https://www.cnblogs.com/go2sea/p/5630355.html

reportInterruptAfterWait方法

//按照之前定義的返回,選擇拋異常還是處理中斷
private void reportInterruptAfterWait(int interruptMode)
    throws InterruptedException {
    if (interruptMode == THROW_IE)
        throw new InterruptedException();
    else if (interruptMode == REINTERRUPT)
        selfInterrupt();
}

共享鎖

上面的關於ReentrantLock的內容和condition都是關於獨佔鎖的。AQS中還存在着共享鎖,下面會簡單分析一下共享鎖。

先說明一下共享鎖的特點(個人看法)

  1. 一開始我有點無法理解共享鎖,但是可以類比概念上的讀寫鎖,這樣就好理解了。我們知道讀鎖是可以多次訪問的,也就是說是個共享鎖。**注意:state的值不會改變。**那麼AQS判斷是否可以擁有共享鎖是通過tryAcquireShared方法來判斷的,如果>=0,那麼久允許獲取。在CountDownLatch中。這個方法一個簡單的實現就是直接判斷state是不是爲0。我們可以想象只有讀鎖的環境下,state的值永遠是1,那麼也就對應了任何讀操作都能獲得鎖。

下面簡單分析一下共享鎖,這裏面的代碼寫的非常巧妙,有很多不同判斷,有點難以理解。

共享鎖的內容其實和獨佔鎖非常類似,可以從方法名上就可以看出。

獨佔鎖 共享鎖
tryAcquire(int arg) tryAcquireShared(int arg)
tryAcquireNanos(int arg, long nanosTimeout) tryAcquireSharedNanos(int arg, long nanosTimeout)
acquire(int arg) acquireShared(int arg)
acquireQueued(final Node node, int arg) doAcquireShared(int arg)
acquireInterruptibly(int arg) acquireSharedInterruptibly(int arg)
doAcquireInterruptibly(int arg) doAcquireSharedInterruptibly(int arg)
doAcquireNanos(int arg, long nanosTimeout) doAcquireSharedNanos(int arg, long nanosTimeout)
release(int arg) releaseShared(int arg)
tryRelease(int arg) tryReleaseShared(int arg)
------------- doReleaseShared()

acquireShared方法

//獲得鎖
public final void acquireShared(int arg) {
   /* tryAcquireShared在AQS中並沒有具體的實現,只是拋出了一個異常,在CountDownLatch,Semaphore,	ReentrantReadWriteLock中有實現,部分類後面後介紹
    說明一下tryAcquireShared的返回值
    如果該值小於0,則代表當前線程獲取共享鎖失敗
    如果該值大於0,則代表當前線程獲取共享鎖成功,並且接下來其他線程嘗試獲取共享鎖的行爲很可能成功
    如果該值等於0,則代表當前線程獲取共享鎖成功,但是接下來其他線程嘗試獲取共享鎖的行爲會失敗
    */
    if (tryAcquireShared(arg) < 0)
        doAcquireShared(arg);
}

doAcquireShared方法

/*
這個方法的作用就是獲取共享鎖
*/
private void doAcquireShared(int arg) {
    //與獨享鎖的第一個不同,Node.SHARED和Node.EXCLUSIVE的區別,本質上是區分兩種模式
    final Node node = addWaiter(Node.SHARED);
    boolean failed = true;
    try {
        boolean interrupted = false;
        for (;;) {
            final Node p = node.predecessor();
            //第二個不同
            if (p == head) {
                int r = tryAcquireShared(arg);
                if (r >= 0) {
                    //這個方法很重要
                    setHeadAndPropagate(node, r);
                    p.next = null; // help GC
                    if (interrupted)
                        selfInterrupt();
                    failed = false;
                    return;
                }
            }
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

setHeadAndPropagate方法

/*
共享鎖的特點就是能有多個線程擁有鎖,所以如果你當前線程能夠擁有鎖,那麼就需要喚醒阻塞隊列中同同樣能獲得鎖的系節點
*/
private void setHeadAndPropagate(Node node, int propagate) {
    Node h = head; // Record old head for check below
    //只有這個地方會修改head,與doReleaseShared中的跳出循環條件有關聯
    setHead(node);
/*
1.propagate > 0 表示調用方指明瞭後繼節點需要被喚醒
2.頭節點後面的節點需要被喚醒(waitStatus<0),不論是老的頭結點還是新的頭結點
*/
    if (propagate > 0 || h == null || h.waitStatus < 0 ||
        (h = head) == null || h.waitStatus < 0) {
        Node s = node.next;
        //如果當前節點的後繼節點是共享類型或者沒有後繼節點,則進行喚醒
        //這裏可以理解爲除非明確指明不需要喚醒(後繼等待節點是獨佔類型),否則都要喚醒
        if (s == null || s.isShared())
            doReleaseShared();
    }
}

doReleaseShared方法

/*
真正的喚醒線程的地方
這個方法有兩個地方會運行
1. acquireShared -> doAcquireShared -> setHeadAndPropagate 也就是獲得鎖的時候,會將阻塞隊列中同樣有機會的線程喚醒
2.releaseShared 釋放鎖的時候,同樣會喚醒其他所有能夠獲得鎖的線程
*/
private void doReleaseShared() {
    for (;;) {
        Node h = head;
        //喚醒前提,阻塞隊列中還有元素
        if (h != null && h != tail) {
            int ws = h.waitStatus;
            if (ws == Node.SIGNAL) {
                if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                    continue;            // loop to recheck cases
                unparkSuccessor(h);
            }
            /*
            	關於這個,太細了
            	我們可以看到這個語句由兩部分
            	1. 判斷ws是否等於0,等於0有2種情況,head=tail下ws是0,但這不可能
        			那就只可能是head後面剛添加節點,本該調用shouldParkAfterFailedAcquire修改head					的ws爲-1,但是還沒有執行的時候,會出現短暫的ws==0
        		2. 設置ws爲Node.PROPAGATE,只有失敗了纔會continue,那麼什麼時候會失敗?
        			唯一的解釋就是後面的節點剛好執行了本該調用shouldParkAfterFailedAcquire修改						head的ws爲-1
        		這種真的是隻有極端的併發情況下才會產生的結果
            */
            else if (ws == 0 &&
                     !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                continue;                // loop on failed CAS
        }
        /*
        這個是判斷當前的head節點有沒有改變,只有setHeadAndPropagate中才會改變,如果沒有改變就會返回
        這邊可以理解爲一個優化,因爲我們在上面已經喚醒了一個線程,那麼本該可以返回,因爲已經喚醒了一個線		程,那麼喚醒第二個線程可以不用再操心了,由新喚醒的節點接替喚醒下一個節點的任務,但是這裏判斷了			head,那麼如果新喚醒的線程已經變成了新的head節點,那麼就會導致再次循環,也就是會有多個線程一起執		行喚醒線程操作,加快了喚醒速度。
        */if (h == head)                   // loop if head changed
            break;
    }
}

releaseShared方法

//這個方法就是釋放鎖的方法,具體的內部的兩個方面上面都討論過了
public final boolean releaseShared(int arg) {
    if (tryReleaseShared(arg)) {
        doReleaseShared();
        return true;
    }
    return false;
}

共享鎖和獨佔鎖的區別

  • 當AQS的子類實現獨佔功能時,如ReentrantLock,資源是否可以被訪問被定義爲:只要AQS的state變量不爲0,並且持有鎖的線程不是當前線程,那麼代表資源不可訪問。
  • 當AQS的子類實現共享功能時,如CountDownLatch,資源是否可以被訪問被定義爲:只要AQS的state變量不爲0,那麼代表資源不可以爲訪問。

CountDownLatch

CountDownLatch是一個同步工具類,用來協調多個線程之間的同步,或者說起到線程之間的通信。

CountDownLatch 這個類是比較典型的 AQS 的共享模式的使用,下面會對這個類具體的說明,如果看懂了上面的共享鎖,CountDownLatch應該還是比較好理解的。

具體例子


class Driver2 { // ...
    void main() throws InterruptedException {
        //創建
        CountDownLatch doneSignal = new CountDownLatch(N);
        //使用線程池
        Executor e = Executors.newFixedThreadPool(8);
        // 創建 N 個任務,提交給線程池來執行
        for (int i = 0; i < N; ++i) // create and start threads
            e.execute(new WorkerRunnable(doneSignal, i));

        // 等待所有的任務完成,這個方法纔會返回
        doneSignal.await();           // wait for all to finish
    }
}

class WorkerRunnable implements Runnable {
    private final CountDownLatch doneSignal;
    private final int i;

    WorkerRunnable(CountDownLatch doneSignal, int i) {
        this.doneSignal = doneSignal;
        this.i = i;
    }

    public void run() {
        try {
            doWork(i);
            // 這個線程的任務完成了,調用 countDown 方法
            doneSignal.countDown();
        } catch (InterruptedException ex) {
        } // return;
    }

    void doWork() { ...}
}

這個類的核心方法就兩個countDown和await

public void countDown() {
    sync.releaseShared(1);
}
/*****************************************/
  public final boolean releaseShared(int arg) {
        if (tryReleaseShared(arg)) {
        	//上面分析過了
            doReleaseShared();
            return true;
        }
        return false;
    }
/*
重寫了tryReleaseShared可以看到每次countDown就state--
直到state減爲0,就會喚醒所有await的線程
*/
protected boolean tryReleaseShared(int releases) {
            // Decrement count; signal when transition to zero
            for (;;) {
                int c = getState();
                if (c == 0)
                    return false;
                int nextc = c-1;
                if (compareAndSetState(c, nextc))
                    return nextc == 0;
            }
        }
public void await() throws InterruptedException {
    sync.acquireSharedInterruptibly(1);
}
/*******************這些代碼前面都已經分析過了**************************/
  public final void acquireSharedInterruptibly(int arg)
            throws InterruptedException {
        if (Thread.interrupted())
            throw new InterruptedException();
        if (tryAcquireShared(arg) < 0)
            //doAcquireSharedInterruptibly就是doAcquireShared方法,只不過處理了中斷
            doAcquireSharedInterruptibly(arg);
    }

最後再看一下構造函數

//傳入了一個count,也就是需要執行count次countDown之後,纔會喚醒
public CountDownLatch(int count) {
    if (count < 0) throw new IllegalArgumentException("count < 0");
    this.sync = new Sync(count);
}

CyclicBarrier

CyclicBarrier是一個同步輔助類,允許一組線程互相等待,直到到達某個公共屏障點 (下面用柵欄代替)。因爲該 barrier 在釋放等待線程後可以重用,所以稱它爲循環 的 barrier。

CyclicBarrier和CountDownLatch的實現不同,主要是通過ReentrantLock和Condition條件隊列實現的。對於這兩個概念不懂的,可以看上面的介紹。

一個例子

package thread;
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;
/**
* 模擬運動員
**/
public class MyThread extends Thread {
    private CyclicBarrier cyclicBarrier;
    private String name;

    public MyThread(CyclicBarrier cyclicBarrier, String name) {
        super();
        this.cyclicBarrier = cyclicBarrier;
        this.name = name;
    }

    @Override
    public void run() {
        System.out.println(name + "開始準備");
        try {
            Thread.currentThread().sleep(5000);
            System.out.println(name + "準備完畢!等待發令槍");
            try {
                cyclicBarrier.await();
            } catch (BrokenBarrierException e) {            
                e.printStackTrace();
            }
        } catch (InterruptedException e) {

            e.printStackTrace();
        }
    }
}
//測試類
public class Test {
    public static void main(String[] args) {
        CyclicBarrier barrier = new CyclicBarrier(5, new Runnable() {

            @Override
            public void run() {
                System.out.println("發令槍響了,跑!");

            }
        });
        for (int i = 0; i < 5; i++) {
            new MyThread(barrier, "運動員" + i + "號").start();

        }

    }

}

介紹一下屬性

//設置是否打破柵欄
private static class Generation {
    boolean broken = false;
}
//獨佔鎖
private final ReentrantLock lock = new ReentrantLock();
//條件隊列
private final Condition trip = lock.newCondition();
//設置同有parties個線程要同步
private final int parties;
//到達柵欄後要執行的操作
private final Runnable barrierCommand;
//利用這個實現重複利用
private Generation generation = new Generation();
//還有count個未達到柵欄
private int count;

主要方法是await,這裏面有兩個,一個是帶超時參數的,一個是不帶超時參數的

//區別就是傳入dowait的參數不同
public int await() throws InterruptedException, BrokenBarrierException {
    try {
        return dowait(false, 0L);
    } catch (TimeoutException toe) {
        throw new Error(toe); // cannot happen
    }
}
/***********************************************************************/
  public int await(long timeout, TimeUnit unit)
        throws InterruptedException,
               BrokenBarrierException,
               TimeoutException {
        return dowait(true, unit.toNanos(timeout));
    }

breakBarrier方法

private void breakBarrier() {
    generation.broken = true;
    count = parties;
    trip.signalAll();
}

nextGeneration方法

/*
這裏主要做了3件事
1. 喚醒所有的線程
2. 重置count爲所有線程的值,方便開啓下一個generation
3. 再次初始化generation,這麼做的原因是區分不同的generation
*/private void nextGeneration() {
    trip.signalAll();
    count = parties;
    generation = new Generation();
}

核心方法dowait

private int dowait(boolean timed, long nanos)
    throws InterruptedException, BrokenBarrierException,
           TimeoutException {
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        //獲得當前generation
        final Generation g = generation;
		//如果柵欄被打破,拋出異常
        if (g.broken)
            throw new BrokenBarrierException();
		//如果有中斷,打破柵欄,拋出異常
        if (Thread.interrupted()) {
            breakBarrier();
            throw new InterruptedException();
        }
		//說明有一個線程到達柵欄,那就count減1
        int index = --count;
        //index == 0說明所有的線程都已經到達柵欄了,那麼就可以喚醒所有的線程,執行任務了 
        if (index == 0) {  
            //判斷是否執行任務成功
            boolean ranAction = false;
            try {
                //獲得任務
                final Runnable command = barrierCommand;
                if (command != null)
                    command.run();
                ranAction = true;
                //開啓新的generation
                nextGeneration();
                return 0;
            } finally {
              //如果ranAction = false,說明在執行command發生了問題,那就要打破柵欄,下面會拋出異常
                if (!ranAction)
                    breakBarrier();
            }
        }
		//如果index!=0 ,說明還有線程未到達柵欄,就會進入這個死循環
        for (;;) {
            try {
                //如果沒有超時標誌,那就普通的await
                if (!timed)
                    trip.await();
                //如果帶超時標誌,那就調用超時機制的await
                else if (nanos > 0L)
                    nanos = trip.awaitNanos(nanos);
            } catch (InterruptedException ie) {
                //進入到這裏說明線程已經被重新喚醒,發現是被打斷喚醒的,那就打破柵欄,重新拋出異常
                if (g == generation && ! g.broken) {
                    breakBarrier();
                    throw ie;
                } else {
                    //不然就設置打段標誌
                    Thread.currentThread().interrupt();
                }
            }
            //柵欄被打破,拋出異常
            if (g.broken)
                throw new BrokenBarrierException();
			//這個方法在我看來就是提供一個正常返回的途徑,因爲在使用中,好像並不會接收這個返回值
            //因爲已經已經開啓了一個新的generation,那就可以返回了
            if (g != generation)
                return index;
			//超時,同樣拋出異常
            if (timed && nanos <= 0L) {
                breakBarrier();
                throw new TimeoutException();
            }
        }
    } finally {a
        lock.unlock();
    }
}

然後對於這個方法,打破柵欄一共有3種可能

  1. 中斷,某個等待的線程發生了中斷,那麼會打破柵欄,同時拋出 InterruptedException 異常;
  2. 超時,打破柵欄,同時拋出 TimeoutException 異常;
  3. 指定執行的操作拋出了異常,這個我們前面也說過。

CountDownLatch和CyclicBarrier的區別

CountDownLatch: 一個或者多個線程,等待其他多個線程完成某件事情之後才能執行。

CyclicBarrier : 多個線程互相等待,直到到達同一個同步點,再繼續一起執行。

應用場景

比如說跑步比賽,有5個運動員

在出發前,只有5個運動員都準備好了,纔開始比賽,這就是CyclicBarrier,5個線程互相等待。

當比賽開始後,只有5個運動員都到達終點了,才能進行頒獎,這就是CountDownLatch,頒獎線程需要等待5個運動員線程。

Semaphore

Semaphore也叫信號量,在JDK1.5被引入,可以用來控制同時訪問特定資源的線程數量,通過協調各個線程,以保證合理的使用資源。
Semaphore內部維護了一組虛擬的許可,許可的數量可以通過構造函數的參數指定。

在我的理解中,Semaphore有點像線程池,它規定最大的資源數量。每次acquire就會減少資源數量,release會增加資源數量。

Semaphore是基於AQS共享鎖的一種實現,同時也有像ReentrantLock一樣的公平鎖和非公平鎖。

//公平策略
protected int tryAcquireShared(int acquires) {
    for (;;) {
        //區別就在這,公平策略先判斷阻塞隊列是否有元素,如果有,就會直接進入阻塞隊列,準備掛起線程
        if (hasQueuedPredecessors())
            return -1;
        int available = getState();
        int remaining = available - acquires;
        if (remaining < 0 ||
            compareAndSetState(available, remaining))
            return remaining;
    }
}
//非公平策略
final int nonfairTryAcquireShared(int acquires) {
    for (;;) {
        int available = getState();
        int remaining = available - acquires;
        if (remaining < 0 ||
            compareAndSetState(available, remaining))
            return remaining;
    }
}

其他感覺也沒什麼好分析的了,如果看懂了前面所有,那麼Semaphore就很容易理解,可以自己去閱讀。

參考內容

一行一行源碼分析清楚AbstractQue uedSynchronizer

一行一行源碼分析清楚AbstractQue uedSynchronizer(二)

一行一行源碼分析清楚AbstractQue uedSynchronizer(三)

從ReentrantLock的實現看AQS的原理及應用

逐行分析AQS源碼(3)——共享鎖的獲取與釋放

AbstractQueuedSynchronizer源碼解讀

Java併發之CyclicBarrier

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