樂觀鎖優化併發性能就該這麼用!

什麼是樂觀鎖?

​ 樂觀鎖,顧名思義,就是說在操做共享資源時,它老是抱着樂觀的態度進行,它認爲本身能夠成功地完成操做。但實際上,當多個線程同時操做一個共享資源時,只有一個線程會成功,那麼失敗的線程呢?樂觀鎖不會像悲觀鎖同樣在操做系統中掛起,而僅僅是返回,而且系統容許失敗的線程重試,也容許自動放棄退出操做。java

​ 因此,樂觀鎖相比悲觀鎖來講,不會帶來死鎖、飢餓等活性故障問題,線程間的相互影響也遠遠比悲觀鎖要小。更爲重要的是,樂觀鎖沒有因競爭形成的系統開銷,因此在性能上也是更勝一籌。算法

樂觀鎖的實現原理

​ CAS 是實現樂觀鎖的核心算法,它包含了 3 個參數:V(須要更新的變量)、E(預期值)和 N(最新值)。安全

​ 只有當須要更新的變量等於預期值時,須要更新的變量纔會被設置爲最新值,若是更新值和預期值不一樣,則說明已經有其它線程更新了須要更新的變量,此時當前線程不作操做,返回 V 的真實值。併發

CAS 如何實現原子操做

​ 在 JDK 中的 concurrent 包中,atomic 路徑下的類都是基於 CAS 實現的。AtomicInteger 就是基於 CAS 實現的一個線程安全的整型類。下面咱們經過源碼來了解下如何使用 CAS 實現原子操做。ide

​ 咱們能夠看到 AtomicInteger 的自增方法 getAndIncrement 是用了 Unsafe 的 getAndAddInt 方法,顯然 AtomicInteger 依賴於本地方法 Unsafe 類,Unsafe 類中的操做方法會調用 CPU 底層指令實現原子操做。
高併發

//基於CAS操做更新值
public final boolean compareAndSet(int expect, int update) {
    return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
//基於CAS操做增1
public final int getAndIncrement() {
    return unsafe.getAndAddInt(this, valueOffset, 1);
}
//基於CAS操做減1
public final int getAndDecrement() {
    return unsafe.getAndAddInt(this, valueOffset, -1);
}

優化 CAS 樂觀鎖

​ 雖然樂觀鎖在併發性能上要比悲觀鎖優越,可是在寫大於讀的操做場景下,CAS 失敗的可能性會增大,若是不放棄這次 CAS 操做,就須要循環作 CAS 重試,這無疑會長時間地佔用 CPU。性能

​ 在 Java7 中,經過如下代碼咱們能夠看到:AtomicInteger 的 getAndSet 方法中使用了 for 循環不斷重試 CAS 操做,若是長時間不成功,就會給 CPU 帶來很是大的執行開銷。到了 Java8,for 循環雖然被去掉了,但咱們反編譯 Unsafe 類時就能夠發現該循環實際上是被封裝在了 Unsafe 類中,CPU 的執行開銷依然存在。測試

public final int getAndSet(int newValue) {
        while (true) {
            int current = get();
            if (compareAndSet(current, newValue))
                return current;
        }
    }

​ 在 JDK1.8 中,Java 提供了一個新的原子類 LongAdder。LongAdder 在高併發場景下會比 AtomicInteger 和 AtomicLong 的性能更好,代價就是會消耗更多的內存空間。優化

測試對比

​ 咱們分別在「讀多寫少」、「讀少寫多」、「讀寫差很少」這三種場景下進行測試。this

​ 對三種模式下的五個鎖 Synchronized、ReentrantLock、ReentrantReadWriteLock、StampedLock 以及樂觀鎖 LongAdder 進行壓測。壓測結果以下圖:

img

​ 經過以上結果,咱們能夠發現:

在寫大於讀的場景下,樂觀鎖的性能是最好的,其它 4 種鎖的性能則相差很少;

在讀和寫差很少的場景下,兩種讀寫鎖以及樂觀鎖的性能要優於 Synchronized 和 ReentrantLock。在讀大於寫的場景下,讀寫鎖 ReentrantReadWriteLock、StampedLock 以及樂觀鎖的讀寫性能是最好的。

相關文章
相關標籤/搜索