多線程併發 (五) ReentrantLock 使用和源碼

章節:
多線程併發 (一) 瞭解 Java 虛擬機 - JVM
多線程併發 (二) 瞭解 Thread
多線程併發 (三) 鎖 synchronized、volatile
多線程併發 (四) 瞭解原子類 AtomicXX 屬性地址偏移量,CAS機制
多線程併發 (五) ReentrantLock 使用和源碼

對於多線程併發學過了併發產生的原因,併發產生的問題,併發產生問題的解決方式,對於之前介紹的併發問題的解決方式有synchronzied、volatile、原子類型無鎖控制。瞭解最後一個鎖ReentrantLock重入鎖。ReentrantLock的實現其實是利用了CAS + volatile+LockSupport 的方式控制線程安全的,也就是面試經常問道,不用鎖如何控制多線程安全。

1.ReentrantLock簡單使用

ReentrantLock和synchronzied都是獨佔式重入鎖,之前介紹過ReentrantLock是顯示鎖、synchronzied是內部鎖,對於synchronzied的使用十分簡單,能滿足我們工作中的大部分需求。相對於ReentrantLock的使用就比synchronzied略有複雜,但是ReentrantLock能解決業務比較複雜的場景。

1) 對比

  1. synchronzied鎖的是對象(鎖是保存在對象頭裏面的,根據對象頭數據來標識是否有線程獲得鎖/爭搶鎖),ReentrantLock鎖的是線程(根據進入的線程和int類型的state標識鎖的獲得/爭搶)
  2. synchronzied通過Object中的wait()/nofify()方法實現線程間通訊,ReentrantLock通過Condition的await()/signal()方法實現線程間通訊
  3. synchronzied是非公平鎖,ReentrantLock可選擇公平鎖/非公平鎖
  4. synchronzied涉及到鎖的升級無鎖->偏向鎖->自旋鎖->向OS申請重量級鎖,ReentrantLock實現不涉及鎖,利用CAS自旋機制和volatile同步隊列實現鎖的功能
  5. ReentrantLock具有tryLock嘗試獲取鎖以及tryLock timeout,可主動release釋放使用靈活

2) 簡單例子

public class Test {

    static ReentrantLock lock = new ReentrantLock();
    static Condition condition = lock.newCondition();
    public static void main(String[] args) throws InterruptedException {

        lock.lock();
        new Thread(new SignalThread()).start();
        System.out.println("等待通知");
        try {
            condition.await();
        } finally {
            lock.unlock();
        }
        System.out.println("恢復運行");
    }
    static class SignalThread implements Runnable {

        @Override
        public void run() {
            lock.lock();
            try {
                condition.signal();
                System.out.println("通知");
            } finally {
                lock.unlock();
            }
        }
    }
}

使用了Condition + Reentrantlock實現線程間通信,和synchronzied的使用其實差別不大,使用的時候要保證lock()和unLock()方法的調用對應,調用次數保證相同。對於ReentrantLock的使用不做太多介紹,不熟悉的可以搜索用法。

2.ReentrantLock 源碼實現

ReentrantLock其實是對 AbstractQueuedSynchronizer 子類 Sync 的一個封裝,可以把ReentrantLock理解成一個包裝類,主要邏輯都在AbstractQueuedSynchronizer(AQS) 和 Sync 子類裏面,所以首先我們要學習的源碼要從AQS開始。
代碼結構圖:

可知 ReentrantLock 分爲公平鎖FairSync和非公平鎖NofairSync,這兩種鎖都是繼承自Sync,並且是AQS的子類。
學習源碼我們從兩方面入手:1.數據結構、2.算法代碼

  1. AQS的數據結構
    AQS是一個同步隊列,是以Node類爲一個節點的雙向鏈表並且有首和尾指針。
         // 首指針
         private transient volatile Node head;
         // 尾指針
         private transient volatile Node tail;
         // 是否有線程佔用:0-無,1-有線程佔用,>1-當前線程重入的次數
         private volatile int state;

    AQS中主要有三個參數而且都是被volatile修飾的,其中他們的更新方式是通過CAS機制Unsafe更新的,這塊可以看多線程併發 (四) 瞭解原子類 AtomicXX 屬性地址偏移量,CAS機制 瞭解CAS的參數含義。
    Node內部類:

    static final class Node {
            volatile int waitStatus; //當前線程的等待狀態
            volatile Node prev;        
            volatile Node next;      
            volatile Thread thread;  //當前線程
    }

    1)prev:指向前一個結點的指針
    2)  next:指向後一個結點的指針
    3)  thread:當前結點表示的線程,因爲同步隊列中的結點內部封裝了之前競爭鎖失敗的線程,故而結點內部必然有一個對應線         程實例的引用
    4)  waitStatus:對於重入鎖而言,主要有3個值。
            0:初始化狀態;
           -1(SIGNAL):當前結點表示的線程在釋放鎖後需要喚醒後續節點的線程;
            1(CANCELLED):在同步隊列中等待的線程等待超時或者被中斷,取消繼續等待

    1)隊列中每個Node節點就代表一個等待獲取鎖的線程,其中head指的那個node節點就是當前當用鎖的節點,當n1釋放鎖之後就會喚醒n2一次類推
    2)當有新線程n3加入隊列時候,就會從tail尾部加入,改變tail的指向。
    從上圖容易知道隊列的結構,具體如何被添加進入隊列又是如何釋放的,繼續看算法~

  2. 算法
    1)線程被加鎖/加入隊列
    從簡單使用引入
     public static void main(String[] args) throws InterruptedException {
            ReentrantLock lock = new ReentrantLock();//初始化鎖類型
    
            lock.lock(); //進入加鎖流程
            try {
    
                } finally {
                    lock.unlock(); //釋放鎖
                }
        }
    初始化時候不傳參數就是非公平鎖,傳參數跟據參數類型判斷
       public ReentrantLock(boolean fair) {
            sync = fair ? new FairSync() : new NonfairSync();
        }
    主要以 NonfairSync 非公平鎖代碼爲例,當調用lock()方法之後進入加鎖流程
    final void lock() {
                if (compareAndSetState(0, 1)) //判斷是否有線程獲取了鎖
                    setExclusiveOwnerThread(Thread.currentThread());
                else
                    acquire(1);
            }
         1)compareAndSetState(0, 1) 利用CAS機制判斷state屬性是否被其他線程修改了,state = 0未被其他線程佔用,state > 1被其他線程佔用了
        2)如果沒被其他線程佔用即state = 0,這時把當前線程設置到 AbstractOwnableSynchronizer 內存表示當前佔用的線程
        3)如果state != 0 ,繼續 acquire(1) 把當前線程加入等待隊列
     public final void acquire(int arg) {
            if (!tryAcquire(arg) &&
                acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
                selfInterrupt();
        }
         1)首先會 tryAcquire(arg) 這個方法子類必須實現會引用到
       final boolean nonfairTryAcquire(int acquires) {
                final Thread current = Thread.currentThread();
                int c = getState(); // 當前狀態
                if (c == 0) { // 非公平的這裏會再次嘗試獲取鎖的機會和上面類似
                    if (compareAndSetState(0, acquires)) {
                        setExclusiveOwnerThread(current);
                        return true;
                    }
                }//如果還是當前的線程說明當前線程重入了這個鎖,state +1 
                else if (current == getExclusiveOwnerThread()) {
                    int nextc = c + acquires;
                    if (nextc < 0) // overflow
                        throw new Error("Maximum lock count exceeded");
                    setState(nextc);
                    return true;
                }
                return false; // 不是當前線程並且鎖被其他線程佔用了 返回false
            }
    可以看到這個方法主要分 三部分
        1)第一部分if 中如果state=0了,那就直接佔用這個鎖,這裏也是非公平鎖的體現,並沒有從隊列中取,直接把鎖讓給了當前申請的線程
        2)第二部分else if 中如果還是當前的線程那state +1 ,表示當前線程重入了這個鎖 
        3)三 是個新的線程進入並且鎖被其他線程佔用,返回false
    所以回到上面 當tryAcquire(arg) 返回true 結束,返回false繼續走
     acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
    這裏涉及到了兩個方法,首先是加入隊列然後從隊列中取出線程。主要邏輯處
    首先看addWaiter()方法
     private Node addWaiter(Node mode) {
            Node node = new Node(mode); //創建一個新節點,mode = null
    
            for (;;) { //無線循環
                Node oldTail = tail; //拿到當前的未隊列,
                if (oldTail != null) { //不爲空
                    U.putObject(node, Node.PREV, oldTail);
                    if (compareAndSetTail(oldTail, node)) { //移動尾部指針對象
                        oldTail.next = node; //把當前node加入隊列
                        return node;
                    }
                } else {
                    initializeSyncQueue(); //爲空初始化 看下方
                }
            }
        }
    
      private final void initializeSyncQueue() {
            Node h;
            if (U.compareAndSwapObject(this, HEAD, null, (h = new Node()))) //給head賦值
                tail = h; //給tail賦值
        }
    這塊代碼比較簡單,主要說一下for循環中 oldTail !=null的那塊
          1)U.putObject(node, Node.PREV, oldTail); 這個是Unsafe 中的方法,意思是把oldTail 賦值給node中的 prev。
          2)compareAndSetTail(oldTail, node) if 判斷中的這塊代碼,意思是把tail這個指針從之前的oldTail指向node 看圖

            例如之前 tail = n2(oldTail) ,現在加入了一個線程n3,這時候 tail = n3 
         3)oldTail.next = node;  看圖就是 n2.next = n3
    繼續回到acquireQueued()方法
    acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
    通過addWaiter方法現在隊列中已經加入了一個新的node節點。繼續看acquireQueued方法
       final boolean acquireQueued(final Node node, int arg) {
            try {
                boolean interrupted = false;
                for (;;) { // 死循環
                    final Node p = node.predecessor(); //獲取當前節點的上一個節點
                    if (p == head && tryAcquire(arg)) { //判斷是否是head節點
                        setHead(node); // 把當前節點設置成head
                        p.next = null; // 把之前的head節點從鏈表中釋放,讓內存回收
                        return interrupted;
                    }
                    if (shouldParkAfterFailedAcquire(p, node) && 
                        parkAndCheckInterrupt()) // 暫停當前線程
                        interrupted = true;
                }
            } catch (Throwable t) {
                cancelAcquire(node);
                throw t;
            }
        }
    這塊代碼比較好理解,我們傳如的node是addwaiter方法return回來的,就是我們鏈表中最後一個節點,for循環中先通過node拿到最後一個節點的上一個結點,和head 首節點做比較,相同繼續讓當前線程 tryAcquire 獲取當前鎖,如果成功了那就是說上一個節點已經把鎖釋放了,當前節點就是鏈表中唯一一個節點了,然後把之前的節點p從鏈表中移除等待gc回收。如果獲取沒有成功判斷是否需要暫停當前線程,如果pre節點的線程爲SIGNAL狀態那就調用LockSupport暫停當前線程。不然就一直循環直到前一個節點是head節點並且釋放了鎖。
      private final boolean parkAndCheckInterrupt() {
            LockSupport.park(this); // 暫停當前線程
            return Thread.interrupted();
        }
    在這裏主要把線程暫停,因爲當前node的前一個node釋放鎖之後會通知他。
    2)線程釋放鎖/從隊列中移除
    釋放鎖相對簡單通過主動調用unLock()方法,
     public final boolean release(int arg) {
            if (tryRelease(arg)) { //釋放 state = 0
                Node h = head;
                if (h != null && h.waitStatus != 0)
                    unparkSuccessor(h); // 解除線程的park等待
                return true;
            }
            return false;
        }

    先是釋放state的值,因爲他是鎖是否被佔用的標識。然後unpark線程。

      private void unparkSuccessor(Node node) {
            /*
             * If status is negative (i.e., possibly needing signal) try
             * to clear in anticipation of signalling.  It is OK if this
             * fails or if status is changed by waiting thread.
             */
            int ws = node.waitStatus; 
            if (ws < 0)
                node.compareAndSetWaitStatus(ws, 0);
    
            /*
             * Thread to unpark is held in successor, which is normally
             * just the next node.  But if cancelled or apparently null,
             * traverse backwards from tail to find the actual
             * non-cancelled successor.
             */
            Node s = node.next; // 把取消的線程移除,輪尋直到線程沒有被取消
            if (s == null || s.waitStatus > 0) {
                s = null;
                for (Node p = tail; p != node && p != null; p = p.prev)
                    if (p.waitStatus <= 0)
                        s = p;
            }
            if (s != null)
                LockSupport.unpark(s.thread); //釋放當前節點的線程
        }


    好的博文:
    ReentrantLock應用
    ReentrantLock源碼

翻遍朋友圈,也就這幾張雪人圖有點意思

 

發佈了120 篇原創文章 · 獲贊 147 · 訪問量 19萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章