高併發學習之11ReentrantLock的實現原理分析

1. 簡介

鎖的認識中我們提到了ReentrantLock 的基本使用,以及在上篇文章AQS中,也重點說明了AQS的通過同步狀態來給Lock提供鎖的狀態同步以及隊列等待、線程喚醒/阻塞等操作。其中ReentrantLock就是基於AQS方式實現的一種鎖。
重入鎖ReentrantLock,顧名思義,就是支持重進入的鎖,它表示該鎖能夠支持一個線程對資源的重複加鎖。除此之外,該鎖的還支持獲取鎖時的公平和非公平性選擇。

怎麼理解公平和非公平呢? 如果在絕對時間上,先對鎖進行獲取的請求一定先被滿足獲得鎖,那麼這個鎖就是公平鎖,反之,就是不公平的。
簡單來說公平鎖就是等待時間最長的線程最優先獲取鎖。
ReentrantLock 源碼中,有兩種實現方式:

abstract static class Sync extends AbstractQueuedSynchronizer {......}
//非公平鎖
static final class NonfairSync extends Sync {
        private static final long serialVersionUID = 7316153563782823691L;
        //加鎖
        final void lock() {
            if (compareAndSetState(0, 1))
                setExclusiveOwnerThread(Thread.currentThread());//獲取到同步狀態則記錄當前線程持有狀態
            else
                acquire(1);//獨佔式獲取同步狀態,如果獲取失敗則插入同步隊列進行等待
        }
        //獲取同步狀態
        protected final boolean tryAcquire(int acquires) {
        	//非公平鎖獲取同步狀態
            return nonfairTryAcquire(acquires);
        }
    }
 //公平鎖
static final class FairSync extends Sync {
        private static final long serialVersionUID = -3000897897090466540L;
        final void lock() {
            acquire(1);
        }
        //公平鎖獲取同步狀態
        protected final boolean tryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();
            if (c == 0) {
            	//如果自己是當前隊列中最早進入的,則更新當前持有爲自己
                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;
        }
    }

在上篇文章AQS中,我們寫了一個獨佔鎖AQSDemo ,同時考慮如下場景:當一個線程調用AQSDemo 的lock()方法獲取鎖之後,如果再次調用lock()方法,則該線程將會被自己所阻塞,原因是AQSDemo在實現tryAcquire(int acquires)方法時沒有考慮佔有鎖的線程再次獲取鎖的場景,而在調用tryAcquire(int acquires)方法時返回了false,導致該線程被阻塞。簡單地說,Mutex是一個不支持重進入的鎖。而synchronized關鍵字隱式的支持重進入,比如一個synchronized修飾的遞歸方法,在方法執行時,執行線程在獲取了鎖之後仍能連續多次地獲得該鎖,而不像AQSDemo由於獲取了鎖,而在下一次獲取鎖時出現阻塞自己的情況。
ReentrantLock雖然沒能像synchronized關鍵字一樣支持隱式的重進入,但是在調用lock()方法時,已經獲取到鎖的線程,能夠再次調用lock()方法獲取鎖而不被阻塞。

2. 重進入的實現方式

重進入是指任意線程在獲取到鎖之後能夠再次獲取該鎖而不會被鎖所阻塞,該特性的實現需要解決以下兩個問題:

  • 線程再次獲取鎖。鎖需要去識別獲取鎖的線程是否爲當前佔據鎖的線程,如果是,則再次成功獲取。
  • 鎖的最終釋放。線程重複n次獲取了鎖,隨後在第n次釋放該鎖後,其他線程能夠獲取到該鎖。鎖的最終釋放要求鎖對於獲取進行計數自增,計數表示當前鎖被重複獲取的次數,而鎖被釋放時,計數自減,當計數等於0時表示鎖已經成功釋放。
    ReentrantLock是通過組合自定義同步器來實現鎖的獲取與釋放,以非公平性(默認的)實現爲例,獲取同步狀態的代碼如代碼如下所示,另外補充下,ReentrantLock通過構造函數的方式來選擇是實現公平鎖還是非公平鎖。
//非公平方式獲取同步狀態
final boolean nonfairTryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();
            //第一次進入
            if (c == 0) {
            	//自旋設置狀態
                if (compareAndSetState(0, acquires)) {
                	//設置當前擁有獨佔訪問權的線程。
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            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;
        }

該方法增加了再次獲取同步狀態的處理邏輯:通過判斷當前線程是否爲獲取鎖的線程來決定獲取操作是否成功,如果是獲取鎖的線程再次請求,則將同步狀態值進行增加並返回true,表示獲取同步狀態成功。
成功獲取鎖的線程再次獲取鎖,只是增加了同步狀態值,這也就要求ReentrantLock在釋放
同步狀態時減少同步狀態值,代碼如下:

	//釋放同步狀態
  protected final boolean tryRelease(int releases) {
  			//減去持有狀態次數
            int c = getState() - releases;
            //不是當前線程持有的話是不能釋放的
            if (Thread.currentThread() != getExclusiveOwnerThread())
                throw new IllegalMonitorStateException();
            boolean free = false;
            //持有次數爲0代表同步狀態釋放成功
            if (c == 0) {
                free = true;
                setExclusiveOwnerThread(null);
            }
            setState(c);
            return free;
        }

如果該鎖被獲取了n次,那麼前(n-1)次tryRelease(int releases)方法必須返回false,而只有同步狀態完全釋放了,才能返回true。可以看到,該方法將同步狀態是否爲0作爲最終釋放的條件,當同步狀態爲0時,將佔有線程設置爲null,並返回true,表示釋放成功。

3. 公平與非公平獲取鎖的區別

公平性與否是針對獲取鎖而言的,如果一個鎖是公平的,那麼鎖的獲取順序就應該符合
請求的絕對時間順序,也就是FIFO。在前面介紹的nonfairTryAcquire(int acquires)方法,對於非公平鎖,只要CAS設置同步狀態成功,則表示當前線程獲取了鎖,而公平鎖則不同,如ReentrantLock.FairSync.tryAcquire所示:

//公平鎖獲取同步狀態
protected final boolean tryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();
            //第一次進入
            if (c == 0) {
            	//如果自己是當前隊列中最早進入的,則更新當前持有爲自己
                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;
        }

該方法與nonfairTryAcquire(int acquires)比較,唯一不同的位置爲判斷條件多了,
hasQueuedPredecessors()方法,即加入了同步隊列中當前節點是否有前驅節點的判斷,如果該方法返回true,則表示有線程比當前線程更早地請求獲取鎖,因此需要等待前驅線程獲取並釋放鎖之後才能繼續獲取鎖。
hasQueuedPredecessors是AQS實現,起源碼如下:

  //判斷是否有線程比當前線程更早請求
  public final boolean hasQueuedPredecessors() {
        // The correctness of this depends on head being initialized
        // before tail and on head.next being accurate if the current
        // thread is first in queue.
        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());
    }

總結一下:

  • 公平鎖每次獲取到鎖爲同步隊列中的第一個節點,保證請求資源時間上的絕對順序,而非公平鎖有可能剛釋放鎖的線程下次繼續獲取該鎖,則有可能導致其他線程永遠無法獲取到鎖,造成“飢餓”現象。

  • 公平鎖爲了保證時間上的絕對順序,需要頻繁的上下文切換,而非公平鎖會降低一定的上下文切換,降低性能開銷。因此,ReentrantLock默認選擇的是非公平鎖,則是爲了減少一部分上下文切換,保證了系統更大的吞吐量。

4. 總結

  • 重入鎖實說如果當前線程獲取鎖之後再次進入仍可以拿到鎖,每次拿到鎖都會自增同步狀態次數(setState),釋放鎖時也是自減,直到state爲0時才表示釋放成功。
  • 同步鎖支持公平、非公平兩種模式:非公平是指當線程釋放鎖之後,其它現在大家一起重新搶鎖,而公平模式是指當前線程釋放狀態之後排隊時間最長的獲取鎖。
  • 重入鎖是獨佔鎖。使用的是AQS獨佔模式,通過acquire獲取同步狀態。

參考文獻 《Java併發編程的藝術》

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