JDK源碼——java.util.concurrent(二)

測試代碼:
https://github.com/kevindai007/springboot_houseSearch/tree/master/src/test/java/com/kevindai/juc
 juc中的類太多,大分部又都需要些一個demo才能更好的理解,因此再開一篇


咱們首先開始研究LockSupport這個類,這個類是用來創建鎖和其他同步工具類的基本線程阻塞原語.Java鎖和同步器框架的核心AQS:AbstractQueuedSynchronizer,就是通過調用LockSupport.park()和LockSupport.unpark()實現線程的阻塞和喚醒的.LockSupport通過底層unsafe提供park,unpark操作.簡單點說:底層維護一個二義性的變量來保存一個許可,需要注意的是這個許可是一次性的,unpark操作設置該值爲1,park操作檢查該值是否爲1,爲1直接返回,不爲1,,則阻塞
這個類的代碼都是很簡單的調用unsafe類的方法,沒什麼好分析的,咱們主要看下他的使用

public class LockSupportTest {
    public static void main(String[] args) throws InterruptedException {
        //主線程一直處於阻塞狀態。因爲許可默認是被佔用的,調用park()時獲取不到許可,所以進入阻塞狀態
//        LockSupport.park();
//        System.out.println("block.");

        //多次unpark,只有一次park也不會出現什麼問題,結果是許可處於可用狀態
//        Thread thread = Thread.currentThread();
//        LockSupport.unpark(thread);//釋放許可
//        LockSupport.unpark(thread);//釋放許可
//        LockSupport.park();// 獲取許可
//        System.out.println("b");


//        LockSupport是不可重入的,如果一個線程連續2次調用LockSupport.park(),那麼該線程一定會一直阻塞下去
//        LockSupport.unpark(thread);
//
//        System.out.println("a");
//        LockSupport.park();
//        System.out.println("b");
//        LockSupport.park();
//        System.out.println("c");




        //線程如果因爲調用park而阻塞的話,能夠響應中斷請求(中斷狀態被設置成true),但是不會拋出InterruptedException
        Thread t = new Thread(new Runnable()
        {
            private int count = 0;

            @Override
            public void run()
            {
                long start = System.currentTimeMillis();
                long end = 0;

                while ((end - start) <= 1000)
                {
                    count++;
                    end = System.currentTimeMillis();
                }

                System.out.println("after 1 second.count=" + count);

                //等待或許許可
                LockSupport.park();
                System.out.println("thread over." + Thread.currentThread().isInterrupted());

            }
        });

        t.start();

        Thread.sleep(2000);

        // 中斷線程
        t.interrupt();


        System.out.println("main over");
    }
}

非常重要
下面來研究一下abstractQueuedSynchronizer(簡稱aqs),aqs是一個線程同步的框架,也是整個juc包的基礎(Semaphore、CountDownLatch、ReentrantLock、ReentrantReadWriteLock等類均在其基礎上完成的),下面咱們來看看其實現(爲方便理解,部分邏輯需要引用ReentrantLock的代碼來解釋)

首先很容易發現aqs有三個很重要的屬性:

    //頭結點
    private transient volatile Node head;
    //尾節點
    private transient volatile Node tail;
    /**
     * The synchronization state.
     * 同步狀態
     * state可以理解有多少線程獲取了資源,即有多少線程獲取了鎖,初始時state=0表示沒有線程獲取鎖.
     *獨佔鎖時,這個值通常爲1或者0
     *如果獨佔鎖可重入時,即一個線程可以多次獲取這個鎖時,每獲取一次,state就加1
     *一旦有線程想要獲得鎖,就可以通過對state進行CAS增量操作,即原子性的增加state的值
     *其他線程發現state不爲0,這時線程已經不能獲得鎖(獨佔鎖),就會進入AQS的隊列中等待.
     *釋放鎖是仍然是通過CAS來減小state的值,如果減小到0就表示鎖完全釋放(獨佔鎖)
     */
    private volatile int state;

下面來說一下aqs的大致邏輯

  • AQS維護了一個隊列,並記錄隊列的頭節點和尾節點
  • 隊列中的節點是獲取不到資源而阻塞的線程
  • AQS同樣維護了一個狀態,這個狀態應該是判斷線程能否獲取到鎖的依據,如果不能,就加入到隊列
  • 當某個節點獲取到資源後就移除隊列,然後讓其後面的節點嘗試獲取資源
    下面咱們來看看Node節點是如何實現的
volatile Node prev;//此節點的前一個節點。
volatile Node next;//此節點的後一個節點
volatile Thread thread;//節點綁定的線程。
volatile int waitStatus;//節點的等待狀態
//節點狀態:取消狀態,該狀態表示節點超時或被中斷就會被踢出隊列
static final int CANCELLED =  1;
//節點狀態:等待觸發狀態,只有前一個節點的狀態爲SIGNAL時,當前節點的線程才能被掛起
static final int SIGNAL    = -1;
//節點狀態:等待條件狀態,表明節點對應的線程因爲不滿足一個條件(Condition)而被阻塞。
static final int CONDITION = -2;
//節點狀態:狀態需要向後傳播,使用在共享模式頭結點有可能處於這種狀態,表示鎖的下一次獲取可以無條件傳播
static final int PROPAGATE = -3;
//需要補充的而是0時新節點纔會有的狀態

可以看出Node維護了一個雙向隊列,,並且每個節點都有自己的狀態

再看看AQS中定義的幾個重要的方法:

public final void acquire(int arg);//請求獲取獨佔式資源(鎖)
public final boolean release(int arg);//請求釋放獨佔式資源(鎖)
public final void acquireShared(int arg);//請求獲取共享式資源
public final boolean releaseShared(int arg);//請求釋放共享式資源
//獨佔方式。嘗試獲取資源,成功則返回true,失敗則返回false
protected boolean tryAcquire(int arg) {
  throw new UnsupportedOperationException();
}
//獨佔方式。嘗試釋放資源,成功則返回true,失敗則返回false。
protected boolean tryRelease(int arg) {    
  throw new UnsupportedOperationException();
}
//共享方式。嘗試獲取資源。負數表示失敗;0表示成功,但沒有剩餘可用資源;正數表示成功,且有剩餘資源
protected int tryAcquireShared(int arg) {    
  throw new UnsupportedOperationException();
}
protected int tryReleaseShared(int arg) {
  throw new UnsupportOperationException();
}

可以看到aqs用acquire()和release()方法提供對資源的獲取和釋放
但是try**()結構的方法都是隻拋出了異常,很顯然這類方法是需要子類去實現的.
這也因爲AQS定義了兩種資源共享方式:Exclusive(獨佔,只有一個線程能執行,如ReentrantLock)和Share(共享,多個線程可以同時執行,如Semaphone、CountDownLatch), AQS負責獲取資源(修改state的狀態),而自定義同步器負責就要實現上述方法告訴AQS獲取資源的規則.

下面來分析分析這幾個方法:
1、acquire(int)
此方法是aqs實現獨佔式資源獲取的頂層方法,這個方法和ReentrantLock.lock()等有着相同的語義.下面我們開始看源碼

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

這個函數共調用了4個方法, 其中tryAcquire(arg)是在子類Sync中實現, 其餘在均是AQS中提供.
而這個方法的流程比較簡單:
- tryAcquire()嘗試獲取資源,如果成功, 則方法結束
- addWaiter()方法以獨佔方式將線程加入隊列的尾部
- acquireQueued()方法是線程在等待隊列中等待獲取資源
- selfInterrupt(), 如果線程在等待過程中被中斷過,在這裏相應中斷.(線程在等待過程中是不響應中斷的,只有獲取資源後才能自我中斷)

下面來一一解讀這些方法:
(1)、tryAcquire()
此方法嘗試去獲取獨佔資源.如果獲取成功,則返回true,否則返回false。tryAcquire()方法前面已經說過,這個方法是在子類中是實現的. 而在ReentrantLock中,這個方法也正是tryLock()的語義.如下是ReentrantLock對tryAcquire()實現的源碼(ReentranLock中tryAcquire()與nonfairTryAcquire()一致):

protected final boolean tryAcquire(int acquires) {
    final Thread current = Thread.currentThread();    
    int c = getState();   
    if (c == 0) {//等於0表示當前鎖未被其他線程獲取到
      if (!hasQueuedPredecessors() //檢查隊列中是否有線程在當前線程的前面
                && compareAndSetState(0, acquires)) {//CAS操作state,鎖獲取成功
        setExclusiveOwnerThread(current); //設置當前線程爲佔有鎖的線程
        return true;
      }   
    } else if (current == getExclusiveOwnerThread()) {//非0,鎖已經被獲取,並且是當前線程獲取.支持可重入鎖
      int nextc = c + acquires;       
      if (nextc < 0)
        throw new Error("Maximum lock count exceeded");    
      setState(nextc);  //更改狀態位,
      return true;   
    }
    return false;//未能獲取鎖
}
public final boolean hasQueuedPredecessors() {    
  Node t = tail; 
  Node h = head;   
  Node s;    
  /**
   *如果h=t,則隊列未被初始化,返回false
   *如果隊列中沒有線程正在等待, 返回true
   *如果當前線程是隊列中的第一個元素, 返回true,否則返回false
   **/
  return h != t &&  ((s = h.next) == null || s.thread != Thread.currentThread());
}

(2)、sqc中addWaiter(int)
再看acquire()的第二個流程,獲取鎖失敗, 則將線程加入隊列尾部, 返回新加入的節點

private Node addWaiter(Node mode) {
  //以獨佔模式構建節點,節點有共享和獨佔兩種模式
    Node node = new Node(Thread.currentThread(), mode);    
    Node pred = tail;  
    //如果pred不爲空,說明有線程在等待
    //嘗試使用CAS入列,如果入列失敗,則調用enq採用自旋的方式入列
    //該邏輯在無競爭的情況下才會成功,快速入列  
    if (pred != null) {
        node.prev = pred;    //雙向隊列    
        if (compareAndSetTail(pred, node)) {//CAS更新尾部節點
           //將原tail節點的後節點設置爲新tail節點
           //由於CAS和設置next不是原子操作,因此可能出現更新tail節點成功,但是未執行pred.next = node,導致無法從head遍歷節點;
           //但是由於前面已經設置了prev屬性,因此可以從尾部遍歷;
           //像getSharedQueuedThreads、getExclusiveQueuedThreads都是從尾部開始遍歷
          pred.next = node;  //雙向隊列
          return node;        
        }    
    }    
    enq(node);  //如果隊列沒有初始化活更新尾部節點失敗,程序就會到這一步,通過自旋入列
    return node;
}
private Node enq(final Node node) {
    for (;;) {//自旋+CAS配合使用方式,一直循環知道CAS更新成功.
        Node t = tail; 
        if (t == null) {//隊列爲空, 沒有初始化,必須初始化
            if (compareAndSetHead(new Node())) 
                tail = head;      
        } else { 
            node.prev = t; 
            if (compareAndSetTail(t, node)) { //設置尾節點,此時的head是頭節點,不存放數據
                t.next = node; 
                return t;            
            }        
        }   
    }
}

(3)、sqc中acquireQueued()
addWaiter()完成後返回新加入隊列的節點, 緊接着進入下一個流程acquireQueued(), 在這個方法中, 會實現線程節點的阻塞和喚醒. 所有節點在這個方法的處理下,等待資源

final boolean acquireQueued(final Node node, int arg) { 
    boolean failed = true;  //是否拿到資源
    try {        
        boolean interrupted = false;  //等待過程中是否被中斷過
        for (;;) {        //又是一個自旋配合CAS設置變量
            final Node p = node.predecessor();       //當前節點的前驅節點  
            if (p == head && tryAcquire(arg)) {//如果前驅節點是頭節點, 則當前節點已經具有資格嘗試獲取資源
                setHead(node);    //獲取資源後,設置當前節點爲頭節點   
                p.next = null; // help GC                
                failed = false;   
                return interrupted;   
            }            
              //如果不能獲取資源,就進入waiting狀態
            if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt())                
                interrupted = true;        
        }    
    } finally {        
        if (failed)            
            cancelAcquire(node);    
    }
}
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {    
    int ws = pred.waitStatus; //獲取前一個節點的狀態
    if (ws == Node.SIGNAL)
      /*
      *此時前驅節點完成任務後能夠喚醒當前節
      *記住,喚醒當前節點的任務是前驅節點完成
      */
        return true;    
    if (ws > 0) { //ws大於0表示節點已經被取消,應該移出隊列.               
        do {            
            //節點的前驅引用指向更前面的沒有被取消的節點.所以被取消的節點沒有引用之後會被GC
            node.prev = pred = pred.prev;        
        } while (pred.waitStatus > 0);        
        pred.next = node;    
    } else {      
        //找到了合適的前驅節點後,將其狀態設置爲SIGNAL
        compareAndSetWaitStatus(pred, ws, Node.SIGNAL);    
    }    
    return false;
}

接下來是 parkAndCheckInterrupt() 方法, 真正讓節點進入waiting狀態的方法,是在這個方法中調用的.

private final boolean parkAndCheckInterrupt() {  
    LockSupport.park(this);    //使線程進入waiting狀態,查看上面的LockSupport類介紹
    return Thread.interrupted(); //檢查是否被中斷
}

(4)、selfInterrupt()
acquire()方法不是立即響應中斷的. 由於線程獲取同步狀態失敗加入到同步隊列中,後續對線程進行中斷操作時,線程不會從同步隊列中移除,而是在獲取資源後進行自我中斷處理

private static void selfInterrupt() {
    Thread.currentThread().interrupt();
}

至此獨佔鎖獲取資源的過程已經分析完了,理解流程只有也並不十分複雜,簡單來說就是嘗試獲取資源, 如果獲取不到就進入等待隊列變成等待狀態

2、release(int)
講了如何獲取到資源,接下來就應該如何釋放資源.這個方法會在獨佔的模式下釋放指定的資源(減小state),此方法與ReentrantLock.unlock()有相同的語意

public final boolean release(int arg) {    
    if (tryRelease(arg)) {    //嘗試釋放資源
        Node h = head;        
        if (h != null && h.waitStatus != 0)            
            unparkSuccessor(h);   //喚醒隊列的下一個節點
        return true;    
    }    
    return false;
}

分析釋放資源流程
(1)、tryRelease()這個方法是在子類中實現的.我們以ReentrantLock.unlock()爲例解讀資源釋放的過程

protected final boolean tryRelease(int releases) {    
    int c = getState() - releases;   //state減去指定的量, 
    if (Thread.currentThread() != getExclusiveOwnerThread())       
        throw new IllegalMonitorStateException();    
    boolean free = false;    
    if (c == 0) {  //獨佔鎖模式下,state爲0時表示沒有線程獲取鎖,這時纔算是當前線程完全釋放鎖
        free = true;        
        setExclusiveOwnerThread(null);    
    }    
    setState(c);    
    return free;
}

(2)、unparkSuccessor()
此方法用於喚醒後繼節點

private void unparkSuccessor(Node node) { 
    int ws = node.waitStatus;    
    if (ws < 0)        
        compareAndSetWaitStatus(node, ws, 0);      
    Node s = node.next;    
    if (s == null || s.waitStatus > 0) {   //waitStatus表示節點已經被取消,應該踢出隊列
        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);
}

至此獨佔鎖的獲取、釋放資源流程都已經完了,我也是查的不少資料才把這個流程捋清楚,快給我點贊

上面分析了獨佔鎖的流程,下面咱們接着類分析共享鎖的過程
1、acquireShared()
此方法是aqs實現共享式資源獲取的頂層方法.下面我們開始看源碼

    public final void acquireShared(int arg) {
        if (tryAcquireShared(arg) < 0)
            doAcquireShared(arg);
    }

這個函數共調用了2個方法, 其中tryAcquireShared(arg)是在子類Sync中實現, doAcquireShared則是AQS中提供.
方法流程很簡單,首先嚐試獲取資源,如果狀態小於0(未獲取成功),則調用doAcquireShared()方法加入阻塞隊列.下面咱們分別來看看這兩個方法
(1)、tryAcquireShared()
tryAcquireShared()在aqs中僅是一個抽象方法,具體實現在子類中,這裏我以CountdownLatch爲例進行分析

        //等於0表示當前鎖未被其他線程獲取到,即當前線程獲取到鎖時返回1,否則返回-1
        protected int tryAcquireShared(int acquires) {
            return (getState() == 0) ? 1 : -1;
        }

2、doAcquireShared()
共享模式獲取的核心公共方法,咱們看看源碼

 private void doAcquireShared(int arg) {
        //添加當前線程爲一個共享模式的節點,addWaiter()方法在獨佔模式是分析過,在此不做重複分析
        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);
                   //此時當state值大於0則認爲獲取成功
                    if (r >= 0) {
                        setHeadAndPropagate(node, r);
                        p.next = null; // help GC
                        if (interrupted)
                            selfInterrupt();
                        failed = false;
                        return;
                    }
                }
                //如果前驅節點不是頭節點則不能能獲取資源,就進入waiting狀態.判斷當前節點是否應該被阻塞,是則阻塞等待其他線程release
                //此處的方法前面也分析過,在此不做研究
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            //如果出異常,沒有完成當前節點的出隊,則取消當前節點
            if (failed)
                cancelAcquire(node);
        }
    }
private void setHeadAndPropagate(Node node, int propagate) {
        Node h = head; // Record old head for check below
        setHead(node);//把當前節點設爲頭結點
        if (propagate > 0 || h == null || h.waitStatus < 0) {
            Node s = node.next;
            if (s == null || s.isShared())//如果後繼節點爲共享模式且參數propagate是否大於0或者PROPAGATE是否已被設置,則喚醒後繼節點
                doReleaseShared();
        }
    }

這樣共享鎖的基本流程就結束了,簡單來說就是嘗試獲取資源,如果獲取不到就加入隊列中等待.與獨佔鎖不同的是,獨佔鎖嘗試獲取資源時會檢查隊列中是否有其他線程,如果沒有就設置當前線程爲佔有鎖的線程,即只有一個線程持有資源;而共享模式當調用doAcquireShared時,會看後續的節點是否是共享模式,如果是,會通過doReleaseShared()喚醒後續節點,讓所有等待的共享節點獲取資源

下面來分析一下釋放資源的過程
1、releaseShared()

    public final boolean releaseShared(int arg) {
        if (tryReleaseShared(arg)) {//嘗試釋放資源
            doReleaseShared();
            return true;
        }
        return false;
    }

咱們直接去CountdownLatch中看看tryReleaseShared()方法

        protected boolean tryReleaseShared(int releases) {
            for (;;) {//自旋+cas改變狀態
                int c = getState();
                if (c == 0)
                    return false;
                int nextc = c-1;
                if (compareAndSetState(c, nextc))
                    return nextc == 0;
            }
        }

再來看看doReleaseShared()方法,這是共享模式釋放資源的核心方法

   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);
                }
                else if (ws == 0 &&
                         !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                    continue;                // loop on failed CAS
            }
            if (h == head)                   // loop if head changed
                break;
        }
    }

此方法邏輯也比較簡單,就是喚醒第一個等待節點.但需要注意的是,根據前面acquireShared的邏輯,被喚醒的線程會通過setHeadAndPropagate繼續喚醒後續等待的線程

到這裏AQS就分析完了,到這裏應該對獨佔鎖、共享鎖有一個認識,不清楚沒關係,後續咱們會在其實現類中結合實際情況,進行更加深入的分析,如果有什麼想討論的歡迎留言一起討論

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