多線程8一AbstractQueuedSynchronizer源碼分析二


AQS的源碼分析 <二>

該篇主要分析AQS的ConditionObject,是AQS的內部類,實現等待通知機制。

1、條件隊列

條件隊列與AQS中的同步隊列有所不同,結構圖如下:

在這裏插入圖片描述

兩者區別:

  • 1、鏈表結構不同,條件隊列是單向鏈表,同步隊列是雙向鏈表。
  • 2、兩個隊列中等待條件不同,條件隊列中線程是已經獲取到鎖,主動調用await方法釋放鎖,掛起當前線程,等待某個條件(如IO,mq消息等),同步隊列中的線程是等待獲取鎖,在獲取鎖失敗後掛起等待鎖可用。

兩者聯繫:

當等待的某個條件完成,其他線程調用signal方法,通知掛起在條件隊列中的線程,會將條件隊列中該node移出,加入到同步隊列中,node的ws狀態由Node.CONDITION改爲0 ,開始等待鎖。

2、ConditionObject

ConditionObject 和 Node一樣,都是AQS的內部類, ConditionObject實現Condition接口,主要實現線程調用 await和signal ,實現線程條件阻塞和通知機制,Condition對象通過 Lock子類調用newConditon方法獲取,以

ReentrantLock爲例,代碼如下:

ReentrantLock lock  = new ReentrantLock();
Condition condition =  lock.newCondition();

可見排他鎖的newCondition方法返回的是ConditionObject對象

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

簡單生產者消費示例代碼:

package AQS;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
/**
 * @author zdd
 * 2019/12/30 下午
 * Description: 利用ReentrantLock和Condition實現生產者消費者
 */
public class ConditionTest {
   static ReentrantLock lock  = new ReentrantLock();
   static Condition condition =  lock.newCondition();
    public static void main(String[] args) {
       //資源類
        Apple apple = new Apple();
     //1.開啓生產者線程
        new Thread(()-> {
            for (;;) {
                lock.lock();
                try {
                    //蘋果沒有被消費,喫完通知我,我再生產哦
                    if (apple.getNumber() > 0) {
                        condition.await();
                    }
                    TimeUnit.SECONDS.sleep(1);
                    System.out.println("生產一個蘋果");
                    apple.addNumber();
                    //通知消費線程消費
                    condition.signal();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    lock.unlock();
                }
            }
        },"producer").start();
      //2.開啓消費者線程
        new Thread(()-> {
            for (;;) {
                lock.lock();
                try {
                    //蘋果數量爲0,掛起等待生產蘋果,有蘋果了會通知
                    if(apple.getNumber() == 0) {
                        condition.await();
                    }
                    TimeUnit.SECONDS.sleep(1);
                    System.out.println("消費一個蘋果");
                    apple.decreNumber();
                    //通知生產線程生產
                    condition.signal();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    lock.unlock();
                }
            }
        },"consumer").start();

    }
  //定義蘋果內部類 
   static class Apple {
        //記錄蘋果數量
        private Integer number =0;
        public void addNumber() {
            number++;
            System.out.println(Thread.currentThread().getName() +"當前蘋果數量:"+number );
        }
        public void decreNumber() {
            number--;
            System.out.println(Thread.currentThread().getName() +"當前蘋果數量:"+number);
        }
        public Integer getNumber() {
            return number;
        }
    }
}

執行結果如下圖:
在這裏插入圖片描述

2.1、 await() 方法

當前線程是在已經獲取鎖的情況下,調用await方法主動釋放鎖,掛起當前線程,等待某個條件(IO,mq消息等)喚醒,再去競爭獲取鎖的過程。該方法會將當前線程封裝到node節點中,添加到Condition條件隊列中,釋放鎖資源,並掛起當前線程。

具體執行步驟如下:

1、線程封裝到node中,並添加到Condition條件隊列中,ws =-2 即爲Node.CONDITION。

2、釋放鎖。

3、將自己阻塞掛起,如果線程被喚醒,首先檢查自己是被中斷喚醒的不。如果是被中斷喚醒,跳出while循環;如果是被其他線程signal喚醒,則判斷當前線程所在node是否被加入到同步等待隊列,已在同步隊列中也跳出while循環,否則繼續掛起,signal喚醒邏輯會將condition條件隊列node 移出,加入到同步隊列中,去等待獲取鎖。

4,線程被喚醒,執行acquireQueued方法,線程會嘗試獲取鎖,若失敗則在同步隊列中找到安全位置阻塞,成功則從調用await()方法處繼續向下執行,返回值表示當前線程是否被中斷過。

public final void await() throws InterruptedException {
            if (Thread.interrupted())
                throw new InterruptedException();
            Node node = addConditionWaiter();
            int savedState = fullyRelease(node);
            int interruptMode = 0;
            while (!isOnSyncQueue(node)) {
               //掛起當前線程
                LockSupport.park(this);
              // 被喚醒: 1,被其他線程喚醒,2,中斷喚醒,
                if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
                    break;
            }
           //1,如果被signal正常喚醒執行acquireQueued,返回false,如果獲取到鎖就繼續執行調用await後面的代碼了,未獲取到鎖就在同步隊列中繼續掛起等待鎖執行了
           //2,如果被中斷喚醒的,acquireQueued 返回true
            if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
               //線程在被signal後,再被中斷的
                interruptMode = REINTERRUPT;
         //  後面代碼處理的是被中斷喚醒的情況
            if (node.nextWaiter != null) // clean up if cancelled
              //如果nextWaiter!=null,則表示還在條件隊列中,清理一下所有被取消node
              //什麼情況下會進入該if判斷中,如果是正常被signal的,會將該node從條件隊列移出加入到同步隊列中的, nextWaiter 一定爲null,那就是被異常中斷情況,
                unlinkCancelledWaiters();
            if (interruptMode != 0)
               //響應中斷模式
                reportInterruptAfterWait(interruptMode);
        }

第1步,執行addConditionWaiter方法,主要邏輯是將線程封裝爲Node,並添加到條件隊列中

        private Node addConditionWaiter() {
          //1.獲取隊列中最後一個節點
            Node t = lastWaiter;
            //2.如果最後一個節點被取消,清除出隊
            if (t != null && t.waitStatus != Node.CONDITION) {
                unlinkCancelledWaiters();
                t = lastWaiter;
            }
            //3. t 指向最新有效的節點,也可能條件隊列爲空,t==null
            Node node = new Node(Thread.currentThread(), Node.CONDITION);
            if (t == null)
                firstWaiter = node;
            else
                t.nextWaiter = node;
            lastWaiter = node;
            return node;
        }

第2步,完全釋放鎖 fullyRelease,將同步狀態state 設置爲初始值0,這裏考慮到有多次重入獲取鎖情況,state >1,這時需完全釋放鎖。

  final int fullyRelease(Node node) {
        boolean failed = true;
        try {
            int savedState = getState();
            //1,釋放鎖
            if (release(savedState)) {
                failed = false;
                return savedState;
            } else {
                throw new IllegalMonitorStateException();
            }
        } finally {
            if (failed)
            //2,釋放鎖失敗,將條件隊列中的節點標記爲取消
                node.waitStatus = Node.CANCELLED;
        }
    }

isOnSyncQueue 判斷node是否在同步隊列中

 final boolean isOnSyncQueue(Node node) {
       //1,這2種情況肯定沒有在同步隊列中
        if (node.waitStatus == Node.CONDITION || node.prev == null)
            return false;
        if (node.next != null) // If has successor, it must be on queue
            return true;
       //3.從同步隊列尾節點開始對比,看是否在同步隊列中
        return findNodeFromTail(node);
    }

findNodeFromTail 從後向前尋找

  private boolean findNodeFromTail(Node node) {
        Node t = tail;
        for (;;) {
            if (t == node)
                return true;
            if (t == null)
                return false;
            t = t.prev;
        }
    }

在線程被喚醒後,檢查掛起期間是否被中斷

private int checkInterruptWhileWaiting(Node node) {
    return Thread.interrupted() ?
        (transferAfterCancelledWait(node) ? THROW_IE : REINTERRUPT) :
        0;
}

如果線程被中斷了,那就需要將在條件隊列中等待的該節點執行 transferAfterCancelledWait

 final boolean transferAfterCancelledWait(Node node) {
       // 判斷是否是被signal通知喚醒的,會更新爲0,更新成功,執行入隊操作(加入同步隊列)
        if (compareAndSetWaitStatus(node, Node.CONDITION, 0)) {
            enq(node);
            return true;
        }
        while (!isOnSyncQueue(node))
          //未在同步隊列中,讓出處理器,線程回到就緒態,等待下一次分配cpu調度
            Thread.yield();
        return false;
    }

最後根據不同的中斷值做出相應處理

private void reportInterruptAfterWait(int interruptMode)
    throws InterruptedException {
    if (interruptMode == THROW_IE)
        //1,直接拋出中斷異常
        throw new InterruptedException();
    else if (interruptMode == REINTERRUPT)
        //2,中斷標誌
        selfInterrupt();
}
2.2、signal方法

就是將條件隊列中的node移出,加入到同步隊列等待獲取鎖的過程。

流程圖如下:
在這裏插入圖片描述

  public final void signal() {
            if (!isHeldExclusively())
                throw new IllegalMonitorStateException();
            Node first = firstWaiter;
            if (first != null)
                doSignal(first);
        }
   private void doSignal(Node first) {
            do {
                if ( (firstWaiter = first.nextWaiter) == null)
                    lastWaiter = null;
              // 1、將first節點執行出隊操作
                first.nextWaiter = null;
            } while (!transferForSignal(first) &&
                     (first = firstWaiter) != null);
             //2,如果條件隊列中有ws =-2的節點,肯定會移出一個到同步隊列中
        }
final boolean transferForSignal(Node node) {
       //1,將node ws更新爲0 ,如果node 狀態不等於CONDITION,一定是被取消了
        if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
            return false;
       //2,加入到同步隊列中,返回的p是node的pre 
        Node p = enq(node);
        int ws = p.waitStatus;
       //3,如果前置節點被取消,或者更新p的 ws =-1 失敗,直接喚醒線程,否則等待前置節點喚醒自己
        if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
           //喚醒線程
            LockSupport.unpark(node.thread);
        return true;
    }

3、總結

1、Condition提供的阻塞通知機制與Object類兩者對比:

  • 方法不同,Condition提供方法有 await(), signal(),signalAll(), Object類提供的是wait(),notify() , notifyAll()
  • 配合使用對象不同,Condition條件需要和Lock配合使用,Object類需和Synchronized關鍵字配合。
  • 多條件, Condition可實現多個條件,即創建多個Condition對象,可以每個Condition對象對應一種條件,從而有選擇的實現喚醒通知,Object類的要喚醒一個阻塞線程,只能在一個條件隊列中,喚醒是隨機的,沒有Condition使用靈活。

2、注意區別Condition條件隊列與同步隊列兩者的區別,2個隊列中線程等待條件不同


道阻且長,且歌且行!

每天一小步,踏踏實實走好腳下的路,文章爲自己學習總結,不復制黏貼,就是想讓自己的知識沉澱一下,也希望與更多的人交流,如有錯誤,請批評指正!

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