天天看點

一張圖讀懂非公平鎖與公平鎖

在Java并發程式設計中,公平鎖與非公平鎖是很常見的概念,ReentrantLock、ReadWriteLock預設都是非公平模式。

非公平鎖的效率為何高于公平鎖呢?究竟公平與非公平有何差別呢?

首先,先簡單從名字上來了解,公平鎖就是保障了多線程下各線程擷取鎖的順序,先到的線程優先擷取鎖,而非公平鎖則無法提供這個保障。

看到網上很多說法說非公平鎖擷取鎖時各線程的的機率是随機的,這也是一種很不确切的說法。

非公平鎖并非真正随機,其擷取鎖還是有一定順序的,但其順序究竟是怎樣呢?先看畫了半天的圖:

一張圖讀懂非公平鎖與公平鎖
公平鎖與非公平鎖的一個重要差別就在于上圖中的2、6、10那個步驟,對應源碼如下:

//非公平鎖
 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;
        }

  //公平鎖
  protected final boolean tryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();
            if (c == 0) {
              //hasQueuedPredecessors這個方法就是最大差別所在
                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;
        }      

分析以上代碼,我們可以看到公平鎖就是在擷取鎖之前會先判斷等待隊列是否為空或者自己是否位于隊列頭部,該條件通過才能繼續擷取鎖。

在結合兔子喝水的圖分析,非公平鎖擷取所得順序基本決定在9、10、11這三個事件發生的先後順序,

1、若在釋放鎖的時候總是沒有新的兔子來打擾,則非公平鎖等于公平鎖;

2、若釋放鎖的時候,正好一個兔子來喝水,而此時位于隊列頭的兔子還沒有被喚醒(因為線程上下文切換是需要不少開銷的),此時後來的兔子則優先獲得鎖,成功打破公平,成為非公平鎖;

其實對于非公平鎖,隻要線程進入了等待隊列,隊列裡面依然是FIFO的原則,跟公平鎖的順序是一樣的。因為公平鎖與非公平鎖的release()部分代碼是共用AQS的代碼。

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) {
            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);
    }      

  

上文說到的線程切換的開銷,其實就是非公平鎖效率高于公平鎖的原因,因為非公平鎖減少了線程挂起的幾率,後來的線程有一定幾率逃離被挂起的開銷。

同步控制是并發程式必不可少的重要手段,synchronized關鍵字就是一種簡單的控制方式。

除此之外,JDK内部并發包中也提供了Lock接口,該接口中提供了lock()方法和unLock()方法對顯式加鎖和顯式釋放鎖操作進行支援。

重入鎖可以完全替代synchronized關鍵字,在jdk5早期版本中重入鎖的性能遠遠好于synchronized,但從JDK6開始JDK在synchronized中做了大量的優化,使得兩者的性能差距不大。

與synchronized相比,重入鎖有着顯示的操作過程,我們需要手動定義何時加鎖,何時釋放鎖。但也就是因為這樣,重入鎖對邏輯的控制靈活性要好于synchronized。

大多數情況下鎖的申請都是非公平的。如一個線程1先請求了鎖A,然後線程2頁也請求了鎖A,那麼當鎖A可用時,是線程1可以獲得鎖還是線程2是不一定的。

重入鎖允許我們對其公平性進行設定。公平鎖的一大特點是:它不會産生饑餓現象。隻要排隊,最終你就可以獲得資源。

作者:徐志毅

歡迎關注微信公衆号:大資料從業者

繼續閱讀