StampedLock 讀寫鎖中的最強王者 StampedLock 源碼分析 視圖 三種鎖的獲取和釋放 小結

StampedLock

簡介

我們前面介紹了 ReentrantReadWriteLock可重入讀寫鎖詳解,不過 jdk1.8 引入了性能更好的 StampedLock 讀寫鎖,我願稱之爲最強!

一種基於能力的鎖,具有三種模式用於控制讀/寫訪問。

StampedLock的狀態由版本和模式組成。

鎖定採集方法返回一個表示和控制相對於鎖定狀態的訪問的印記; 這些方法的“嘗試”版本可能會返回特殊值爲零以表示獲取訪問失敗。

鎖定釋放和轉換方法要求stamps作爲參數,如果它們與鎖的狀態不匹配則失敗。

三種模式

這三種模式是:

(1)write

方法writeLock()可能阻止等待獨佔訪問,返回可以在方法unlockWrite(long)中使用的stamps來釋放鎖定。

不定時的和定時版本tryWriteLock,還提供當鎖保持寫入模式時,不能獲得讀取鎖定,並且所有樂觀讀取驗證都將失敗。

(2)read

方法readLock()可能阻止等待非獨佔訪問,返回可用於方法unlockRead(long)釋放鎖的戳記。

還提供不定時的和定時版本tryReadLock。

(3)樂觀讀

方法tryOptimisticRead()只有當鎖當前未保持在寫入模式時才返回非零標記。

方法validate(long)返回true,如果在獲取給定的stamps時尚未在寫入模式中獲取鎖定。

這種模式可以被認爲是一個非常弱的版本的讀鎖,可以隨時由 writer 打破。

對簡單的只讀代碼段使用樂觀模式通常會減少爭用並提高吞吐量。

然而,其使用本質上是脆弱的。 樂觀閱讀部分只能讀取字段並將其保存在局部變量中,以供後驗證使用。

以樂觀模式讀取的字段可能會非常不一致,因此只有在熟悉數據表示以檢查一致性和/或重複調用方法validate()時,使用情況才適用。

例如,當首次讀取對象或數組引用,然後訪問其字段,元素或方法之一時,通常需要這樣的步驟。

核心思想

StampedLock實現了不僅多個讀不互相阻塞,同時在讀操作時不會阻塞寫操作

  • 爲什麼StampedLock這麼神奇?

能夠達到這種效果,它的核心思想在於,在讀的時候如果發生了寫,應該通過重試的方式來獲取新的值,而不應該阻塞寫操作。

這種模式也就是典型的無鎖編程思想,和CAS自旋的思想一樣。

這種操作方式決定了StampedLock在讀線程非常多而寫線程非常少的場景下非常適用,同時還避免了寫飢餓情況的發生。

使用案例

public class Point {

    private double x, y;
    
    private final StampedLock stampedLock = new StampedLock();
    
    //寫鎖的使用
    void move(double deltaX, double deltaY){
        long stamp = stampedLock.writeLock(); //獲取寫鎖
        try {
            x += deltaX;
            y += deltaY;
        } finally {
            stampedLock.unlockWrite(stamp); //釋放寫鎖
        }
    }
    
    //樂觀讀鎖的使用
    double distanceFromOrigin() {
        
        long stamp = stampedLock.tryOptimisticRead(); //獲得一個樂觀讀鎖
        double currentX = x;
        double currentY = y;
        if (!stampedLock.validate(stamp)) { //檢查樂觀讀鎖後是否有其他寫鎖發生,有則返回false
            
            stamp = stampedLock.readLock(); //獲取一個悲觀讀鎖
            
            try {
                currentX = x;
            } finally {
                stampedLock.unlockRead(stamp); //釋放悲觀讀鎖
            }
        } 
        return Math.sqrt(currentX*currentX + currentY*currentY);
    }
    
    //悲觀讀鎖以及讀鎖升級寫鎖的使用
    void moveIfAtOrigin(double newX,double newY) {
        
        long stamp = stampedLock.readLock(); //悲觀讀鎖
        try {
            while (x == 0.0 && y == 0.0) {
                long ws = stampedLock.tryConvertToWriteLock(stamp); //讀鎖轉換爲寫鎖
                if (ws != 0L) { //轉換成功
                    
                    stamp = ws; //票據更新
                    x = newX;
                    y = newY;
                    break;
                } else {
                    stampedLock.unlockRead(stamp); //轉換失敗釋放讀鎖
                    stamp = stampedLock.writeLock(); //強制獲取寫鎖
                }
            }
        } finally {
            stampedLock.unlock(stamp); //釋放所有鎖
        }
    }
}

首先看看第一個方法move,可以看到它和ReentrantReadWriteLock寫鎖的使用基本一樣,都是簡單的獲取釋放,可以猜測這裏也是一個獨佔鎖的實現。

需要注意的是在獲取寫鎖是會返回個只long類型的stamp,然後在釋放寫鎖時會將stamp傳入進去。

這個stamp是做什麼用的呢?如果我們在中間改變了這個值又會發生什麼呢?這裏先暫時不做解釋,後面分析源碼時會解答這個問題。

第二個方法distanceFromOrigin就比較特別了,它調用了tryOptimisticRead,根據名字判斷這是一個樂觀讀鎖。首先什麼是樂觀鎖?樂觀鎖的意思就是先假定在樂觀鎖獲取期間,共享變量不會被改變,既然假定不會被改變,那就不需要上鎖。在獲取樂觀讀鎖之後進行了一些操作,然後又調用了validate方法,這個方法就是用來驗證tryOptimisticRead之後,是否有寫操作執行過,如果有,則獲取一個讀鎖,這裏的讀鎖和ReentrantReadWriteLock中的讀鎖類似,猜測也是個共享鎖。

第三個方法moveIfAtOrigin,它做了一個鎖升級的操作,通過調用tryConvertToWriteLock嘗試將讀鎖轉換爲寫鎖,轉換成功後相當於獲取了寫鎖,轉換失敗相當於有寫鎖被佔用,這時通過調用writeLock來獲取寫鎖進行操作。

看過了上面的三個方法,估計大家對怎麼使用StampedLock有了一個初步的印象。

下面就通過對StampedLock源碼的分析來一步步瞭解它背後是怎麼解決鎖飢餓問題的。

源碼分析

類定義

/*
 * @since 1.8
 * @author Doug Lea
 */
public class StampedLock implements java.io.Serializable {}

這個鎖也是李大狗實現的,讓我們一起來學習下源碼吧。

算法筆記

有一段很核心的算法,沒有出現在文檔中。

我們簡單翻譯如下:

該設計採用了序列鎖的元素(在Linux內核中使用;請參閱Lameter的 http://www.lameter.com/gelato2005.pdf 和其他地方;

請參閱Boehm的http://www.hpl.hp.com/techreports/2012/HPL-2012-68.html)和訂購的RW鎖(請參見Shirako等人的 http://dl.acm.org/citation.cfm?id=2312015

從概念上講,鎖的主要狀態包括一個序列號,該序列號在寫鎖定時是奇數,在其他情況下甚至是奇數。

但是,當讀取鎖定時,讀取器計數將偏移非零值。

驗證“樂觀” seqlock-reader樣式標記時,將忽略讀取計數。

因爲我們必須爲讀者使用少量的位數(目前爲7),當閱讀器的數量超過計數字段時,將使用補充閱讀器溢出字。

爲此,我們將最大讀取器計數值(RBITS)視爲保護溢出更新的自旋鎖。

等待者使用在AbstractQueuedSynchronizer中使用的修改形式的CLH鎖(有關完整帳戶,請參閱其內部文檔), 其中每個節點都被標記(字段模式)爲讀取器或寫入器。

等待讀取器的集合在一個公共節點(field cowait)下分組(鏈接),因此相對於大多數CLH機制而言,它充當單個節點。

由於隊列結構的原因,等待節點實際上不需要攜帶序列號。我們知道每一個都比其前任更大。

這將調度策略簡化爲一個主要的FIFO方案,該方案包含了階段公平鎖定的元素(請參閱Brandenburg&Anderson,尤其是http://www.cs.unc.edu/~bbb/diss/)。

特別是,我們使用相公平的反駁規則:

如果在保持讀取鎖定的同時傳入的讀取器到達,但是有排隊的寫入器,則此傳入的讀取器處於排隊狀態。

(此規則負責方法acquireRead的某些複雜性,但是如果沒有它,鎖將變得非常不公平。)

方法釋放本身不會(有時不能)本身喚醒等待者。

這是由主線程完成的,但是得到了其他任何線程的幫助,在方法acquireRead和acquireWrite中沒有更好的事情要做。

這些規則適用於實際排隊的線程。

不管偏好規則如何,所有tryLock形式都會嘗試獲取鎖,因此可能會“插入”它們的方式。

獲取方法中使用了隨機旋轉,以減少(越來越昂貴)上下文切換,同時還避免了許多線程之間持續的內存抖動。

我們將旋轉限制在隊列的開頭。

線程在阻塞之前最多等待SPINS次(每次迭代以50%的概率減少自旋計數)。

如果喚醒後它未能獲得鎖定,並且仍然是(或成爲)第一個等待線程(指示其他一些線程被鎖定並獲得了鎖定),它將升級自旋(最高可達MAX_HEAD_SPINS),目的是減少連續丟失到獲取線程的可能性(reduce the likelihood of continually losing to barging threads.)。

幾乎所有這些機制都是在方法acquireWrite和acquireRead中執行的,它們是此類代碼的典型代表,因爲動作和重試依賴於本地緩存的讀取的一致集合,因此它們會蔓延開來。

如Boehm的論文(上文)所述,序列驗證(主要是方法validate() )要求的排序規則比適用於普通易失性讀取(“狀態”)的排序規則更嚴格。

爲了在驗證之前強制執行讀取順序以及在尚未強制執行驗證的情況下強制執行驗證本身,我們使用Unsafe.loadFence。

內存佈局將鎖定狀態和隊列指針保持在一起(通常在同一高速緩存行上)。這通常適用於大多數讀負載。

在大多數其他情況下,自適應旋轉CLH鎖減少內存爭用的自然趨勢會降低進一步分散競爭位置的動力,但可能會在將來得到改進。

ps: 這裏可以看出,這個設計實際上是取自序列鎖的設計思想,linux 內核中也有使用。

https://www.hpl.hp.com/techreports/2012/HPL-2012-68.pdf 論文地址

Effective Synchronization on Linux/NUMA Systems

ps: 這兩本書我本人也翻譯了一遍,但是內容過於枯燥(水平有限,翻譯的枯燥),且內容較多,這裏就不做展開了。

一些常量定義

/** Number of processors, for spin control
計算機核數:用於旋轉控制
 */
private static final int NCPU = Runtime.getRuntime().availableProcessors();

/** Maximum number of retries before enqueuing on acquisition 
    入隊前最大重試次數
*/
private static final int SPINS = (NCPU > 1) ? 1 << 6 : 0;

/** Maximum number of retries before blocking at head on acquisition 
捕獲前最大重試次數
*/
private static final int HEAD_SPINS = (NCPU > 1) ? 1 << 10 : 0;

/** Maximum number of retries before re-blocking 
重封鎖前的最大重試次數
*/
private static final int MAX_HEAD_SPINS = (NCPU > 1) ? 1 << 16 : 0;

/** The period for yielding when waiting for overflow spinlock 
等待溢出自旋鎖時的屈服時間
*/
private static final int OVERFLOW_YIELD_RATE = 7; // must be power 2 - 1
/** The number of bits to use for reader count before overflowing 
溢出前用於讀取器計數的位數
*/
private static final int LG_READERS = 7;

讀寫鎖共享的狀態量

從上面的使用示例中我們看到,在 StampedLock 中,除了提供了類似ReentrantReadWriteLock讀寫鎖的獲取釋放方法,還提供了一個樂觀讀鎖的獲取方式。

那麼這三種方式是如何交互的呢?

根據AQS的經驗,StampedLock中應該也是使用了一個狀態量來標誌鎖的狀態。

通過下面的源碼可以證明這點:

// 用於操作state後獲取stamp的值
private static final int LG_READERS = 7;
private static final long RUNIT = 1L;               //0000 0000 0001
private static final long WBIT  = 1L << LG_READERS; //0000 1000 0000
private static final long RBITS = WBIT - 1L;        //0000 0111 1111
private static final long RFULL = RBITS - 1L;       //0000 0111 1110
private static final long ABITS = RBITS | WBIT;     //0000 1111 1111
private static final long SBITS = ~RBITS;           //1111 1000 0000

//初始化時state的值
private static final long ORIGIN = WBIT << 1;       //0001 0000 0000

//鎖共享變量state
private transient volatile long state;
//讀鎖溢出時用來存儲多出的毒素哦
private transient int readerOverflow;

上面的源碼中除了定義state變量外,還提供了一系列變量用來操作state,用來表示讀鎖和寫鎖的各種狀態。

爲了方便理解,我將他們都表示成二進制的值,長度有限,這裏用低12位來表示64的long,高位自動用0補齊。

要理解這些狀態的作用,就需要具體分析三種鎖操作方式是怎麼通過state這一個變量來表示的,首先來看看獲取寫鎖和釋放寫鎖。

其他常量

// Initial value for lock state; avoid failure value zero
// 鎖定狀態的初始值; 避免故障值爲零
private static final long ORIGIN = WBIT << 1;

// Special value from cancelled acquire methods so caller can throw IE
// 取消get方法中的特殊值,以便調用方可以拋出中斷異常
private static final long INTERRUPTED = 1L;

// Values for node status; order matters
// 節點狀態的值; 順序很重要
private static final int WAITING   = -1;
private static final int CANCELLED =  1;

// Modes for nodes (int not boolean to allow arithmetic)
// 節點的模式(不爲布爾值以允許算術計算)
private static final int RMODE = 0;
private static final int WMODE = 1;

等待節點定義

這裏的節點定義,應該是爲了後面雙向鏈表使用。

所有的值都是通過 volatile 保證易變性,和線程間的可變性。

mode 使用 final 修飾,一旦創建就是不可變的,所以是線程安全的。

/** Wait nodes */
static final class WNode {
    volatile WNode prev;
    volatile WNode next;
    volatile WNode cowait;    // list of linked readers
    volatile Thread thread;   // non-null while possibly parked
    volatile int status;      // 0, WAITING, or CANCELLED
    final int mode;           // RMODE or WMODE
    WNode(int m, WNode p) { mode = m; prev = p; }
}

/** Head of CLH queue */
private transient volatile WNode whead;
/** Tail (last) of CLH queue */
private transient volatile WNode wtail;

視圖

這裏還引入了視圖的概念,不曉得和我們數據庫中常見的視圖有沒有關聯。

// views
transient ReadLockView readLockView;
transient WriteLockView writeLockView;
transient ReadWriteLockView readWriteLockView;

ReadLockView 定義

這個是對 Lock 接口的簡單實現。

視圖中調用的方法,我們後面會介紹。

final class ReadLockView implements Lock {
    public void lock() { readLock(); }
    public void lockInterruptibly() throws InterruptedException {
        readLockInterruptibly();
    }
    public boolean tryLock() { return tryReadLock() != 0L; }
    public boolean tryLock(long time, TimeUnit unit)
        throws InterruptedException {
        return tryReadLock(time, unit) != 0L;
    }
    public void unlock() { unstampedUnlockRead(); }
    public Condition newCondition() {
        throw new UnsupportedOperationException();
    }
}

寫鎖視圖

也是對於 Lock 接口的實現,對應的實現都是寫鎖。

final class WriteLockView implements Lock {
    public void lock() { writeLock(); }
    public void lockInterruptibly() throws InterruptedException {
        writeLockInterruptibly();
    }
    public boolean tryLock() { return tryWriteLock() != 0L; }
    public boolean tryLock(long time, TimeUnit unit)
        throws InterruptedException {
        return tryWriteLock(time, unit) != 0L;
    }
    public void unlock() { unstampedUnlockWrite(); }
    public Condition newCondition() {
        throw new UnsupportedOperationException();
    }
}

讀寫鎖視圖

final class ReadWriteLockView implements ReadWriteLock {
    public Lock readLock() { return asReadLock(); }
    public Lock writeLock() { return asWriteLock(); }
}

這裏主要是轉換返回讀寫鎖實現。

讀鎖實現

有讀鎖視圖就直接返回,沒有就創建一個實例。

public Lock asReadLock() {
    ReadLockView v;
    return ((v = readLockView) != null ? v :
            (readLockView = new ReadLockView()));
}

寫鎖實現

類似的返回寫鎖視圖。

public Lock asWriteLock() {
    WriteLockView v;
    return ((v = writeLockView) != null ? v :
            (writeLockView = new WriteLockView()));
}

三種鎖的獲取和釋放

後續的內容,已經有人整理的非常好了,我這裏直接放在文中,便於大家學習。

Java併發(8)- 讀寫鎖中的性能之王:StampedLock

寫鎖的釋放和獲取

源碼

public StampedLock() {
    state = ORIGIN; //初始化state爲 0001 0000 0000
}

public long writeLock() {
    long s, next; 
    return ((((s = state) & ABITS) == 0L && //沒有讀寫鎖
                U.compareAndSwapLong(this, STATE, s, next = s + WBIT)) ? //cas操作嘗試獲取寫鎖
            next : acquireWrite(false, 0L));    //獲取成功後返回next,失敗則進行後續處理,排隊也在後續處理中
}

public void unlockWrite(long stamp) {
    WNode h;
    if (state != stamp || (stamp & WBIT) == 0L) //stamp值被修改,或者寫鎖已經被釋放,拋出錯誤
        throw new IllegalMonitorStateException();
    state = (stamp += WBIT) == 0L ? ORIGIN : stamp; //加0000 1000 0000來記錄寫鎖的變化,同時改變寫鎖狀態
    if ((h = whead) != null && h.status != 0)
        release(h);
}

這裏先說明兩點結論:讀鎖通過前7位來表示,每獲取一個讀鎖,則加1。

寫鎖通過除前7位後剩下的位來表示,每獲取一次寫鎖,則加1000 0000,這兩點在後面的源碼中都可以得到證明。

初始化時將state變量設置爲0001 0000 0000。

寫鎖獲取通過((s = state) & ABITS)操作等於0時默認沒有讀鎖和寫鎖。

寫鎖獲取的情況

寫鎖獲取分三種情況:

(1)沒有讀鎖和寫鎖時,state爲0001 0000 0000

0001 0000 0000 & 0000 1111 1111 = 0000 0000 0000 // 等於0L,可以嘗試獲取寫鎖

(2)有一個讀鎖時,state爲0001 0000 0001

0001 0000 0001 & 0000 1111 1111 = 0000 0000 0001 // 不等於0L

(3)有一個寫鎖,state爲0001 1000 0000

0001 1000 0000 & 0000 1111 1111 = 0000 1000 0000 // 不等於0L

獲取到寫鎖,需要將s + WBIT設置到state,也就是說每次獲取寫鎖,都需要加0000 1000 0000。

同時返回s + WBIT的值

0001 0000 0000 + 0000 1000 0000 = 0001 1000 0000

寫鎖釋放

釋放寫鎖首先判斷stamp的值有沒有被修改過或者多次釋放,之後通過state = (stamp += WBIT) == 0L ? ORIGIN : stamp來釋放寫鎖,位操作表示如下:
stamp += WBIT

0010 0000 0000 = 0001 1000 0000 + 0000 1000 0000

這一步操作是重點!!!

寫鎖的釋放並不是像ReentrantReadWriteLock一樣+1然後-1,而是通過再次加0000 1000 0000來使高位每次都產生變化,爲什麼要這樣做?

直接減掉0000 1000 0000不就可以了嗎?

這就是爲了後面樂觀鎖做鋪墊,讓每次寫鎖都留下痕跡。

大家可以想象這樣一個場景,字母A變化爲B能看到變化,如果在一段時間內從A變到B然後又變到A,在內存中自會顯示A,而不能記錄變化的過程,這也就是CAS中的ABA問題。

在StampedLock中就是通過每次對高位加0000 1000 0000來達到記錄寫鎖操作的過程,可以通過下面的步驟理解:

第一次獲取寫鎖:

0001 0000 0000 + 0000 1000 0000 = 0001 1000 0000

第一次釋放寫鎖:

0001 1000 0000 + 0000 1000 0000 = 0010 0000 0000

第二次獲取寫鎖:

0010 0000 0000 + 0000 1000 0000 = 0010 1000 0000

第二次釋放寫鎖:

0010 1000 0000 + 0000 1000 0000 = 0011 0000 0000

第n次獲取寫鎖:

1110 0000 0000 + 0000 1000 0000 = 1110 1000 0000

第n次釋放寫鎖:

1110 1000 0000 + 0000 1000 0000 = 1111 0000 0000

可以看到第8位在獲取和釋放寫鎖時會產生變化,也就是說第8位是用來表示寫鎖狀態的,前7位是用來表示讀鎖狀態的,8位之後是用來表示寫鎖的獲取次數的。這樣就有效的解決了ABA問題,留下了每次寫鎖的記錄,也爲後面樂觀鎖檢查變化提供了基礎。

關於acquireWrite方法這裏不做具體分析,方法非常複雜,感興趣的同學可以網上搜索相關資料。

這裏只對該方法做下簡單總結,該方法分兩步來進行線程排隊,首先通過隨機探測的方式多次自旋嘗試獲取鎖,然後自旋一定次數失敗後再初始化節點進行插入。

悲觀讀鎖的釋放和獲取

源碼

public long readLock() {
    long s = state, next;  
    return ((whead == wtail && (s & ABITS) < RFULL && //隊列爲空,無寫鎖,同時讀鎖未溢出,嘗試獲取讀鎖
                U.compareAndSwapLong(this, STATE, s, next = s + RUNIT)) ?   //cas嘗試獲取讀鎖+1
            next : acquireRead(false, 0L));     //獲取讀鎖成功,返回s + RUNIT,失敗進入後續處理,類似acquireWrite
}

public void unlockRead(long stamp) {
    long s, m; WNode h;
    for (;;) {
        if (((s = state) & SBITS) != (stamp & SBITS) ||
            (stamp & ABITS) == 0L || (m = s & ABITS) == 0L || m == WBIT)
            throw new IllegalMonitorStateException();
        if (m < RFULL) {    //小於最大記錄值(最大記錄值127超過後放在readerOverflow變量中)
            if (U.compareAndSwapLong(this, STATE, s, s - RUNIT)) {  //cas嘗試釋放讀鎖-1
                if (m == RUNIT && (h = whead) != null && h.status != 0)
                    release(h);
                break;
            }
        }
        else if (tryDecReaderOverflow(s) != 0L) //readerOverflow - 1
            break;
    }
}

悲觀讀鎖的獲取和ReentrantReadWriteLock類似,不同在於StampedLock的讀鎖很容易溢出,最大隻有127,超過後通過一個額外的變量readerOverflow來存儲,這是爲了給寫鎖留下更大的空間,因爲寫鎖是在不停增加的。

鎖獲取

悲觀讀鎖獲取分下面四種情況:

沒有讀鎖和寫鎖時,state爲0001 0000 0000

// 小於 0000 0111 1110,可以嘗試獲取讀鎖
0001 0000 0000 & 0000 1111 1111 = 0000 0000 0000

有一個讀鎖時,state爲0001 0000 0001

// 小於 0000 0111 1110,可以嘗試獲取讀鎖
0001 0000 0001 & 0000 1111 1111 = 0000 0000 0001

有一個寫鎖,state爲0001 1000 0000

// 大於 0000 0111 1110,不可以獲取讀鎖
0001 1000 0000 & 0000 1111 1111 = 0000 1000 0000

讀鎖溢出,state爲0001 0111 1110

// 等於 0000 0111 1110,不可以獲取讀鎖
0001 0111 1110 & 0000 1111 1111 = 0000 0111 1110

鎖釋放

讀鎖的釋放過程在沒有溢出的情況下是通過s - RUNIT操作也就是-1來釋放的,當溢出後則將readerOverflow變量-1。

樂觀讀鎖的獲取和驗證

樂觀讀鎖因爲實際上沒有獲取過鎖,所以也就沒有釋放鎖的過程,只是在操作後通過驗證檢查和獲取前的變化。

源碼

源碼如下:

//嘗試獲取樂觀鎖
public long tryOptimisticRead() {
    long s;
    return (((s = state) & WBIT) == 0L) ? (s & SBITS) : 0L;
}

//驗證樂觀鎖獲取之後是否有過寫操作
public boolean validate(long stamp) {
    //該方法之前的所有load操作在內存屏障之前完成,對應的還有storeFence()及fullFence()
    U.loadFence();  
    return (stamp & SBITS) == (state & SBITS);  //比較是否有過寫操作
}

樂觀鎖基本原理就時獲取鎖時記錄state的寫狀態,然後在操作完成之後檢查寫狀態是否有變化,因爲寫狀態每次都會在高位留下記錄,這樣就避免了寫鎖獲取又釋放後得不到準確數據。

鎖獲取

獲取寫鎖記錄有三種情況:

(1)沒有讀鎖和寫鎖時,state爲0001 0000 0000

//((s = state) & WBIT) == 0L) true
0001 0000 0000 & 0000 1000 0000 = 0000 0000 0000
//(s & SBITS)
0001 0000 0000 & 1111 1000 0000 = 0001 0000 0000

(2)有一個讀鎖時,state爲0001 0000 0001

//((s = state) & WBIT) == 0L) true
0001 0000 0001 & 0000 1000 0000 = 0000 0000 0000
//(s & SBITS)
0001 0000 0001 & 1111 1000 0000 = 0001 0000 0000

(3)有一個寫鎖,state爲0001 1000 0000

//((s = state) & WBIT) == 0L) false
0001 1000 0000 & 0000 1000 0000 = 0000 1000 0000
//0L
0000 0000 0000

驗證是否有寫操作

驗證過程中是否有過寫操作,分四種情況

(1)寫過一次

0001 0000 0000 & 1111 1000 0000 = 0001 0000 0000
0010 0000 0000 & 1111 1000 0000 = 0010 0000 0000   //false

(2)未寫過,但讀過

0001 0000 0000 & 1111 1000 0000 = 0001 0000 0000
0001 0000 1111 & 1111 1000 0000 = 0001 0000 0000   //true

(3)正在寫

0001 0000 0000 & 1111 1000 0000 = 0001 0000 0000
0001 1000 0000 & 1111 1000 0000 = 0001 1000 0000   //false

(4)之前正在寫,無論是否寫完都不會爲0L

0000 0000 0000 & 1111 1000 0000 = 0000 0000 0000   //false

小結

我們縱觀整個併發鎖的發展史,實際上一種思想的發展史

(1)synchronized 最常用的悲觀鎖,使用方便,但是爲非公平鎖。

(2)ReentrantLock 可重入鎖,提供了公平鎖等豐富特性。

(3)ReentrantReadWriteLock 可重入讀寫鎖 支持讀寫分離,在讀多的場景中表現優異。

(4)本文的讀不阻塞寫,是一種更加優異的思想。類似思想的實現還有 CopyOnWriteList 等。

希望本文對你有幫助,如果有其他想法的話,也可以評論區和大家分享哦。

各位極客的點贊收藏轉發,是老馬寫作的最大動力!

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