【Java核心基礎知識】07 - 多線程并發(fā)(6)

多線程知識點目錄

多線程并發(fā)(1)- http://www.itdecent.cn/p/8fcfcac74033
多線程并發(fā)(2)-http://www.itdecent.cn/p/a0c5095ad103
多線程并發(fā)(3)-http://www.itdecent.cn/p/c5c3bbd42c35
多線程并發(fā)(4)-http://www.itdecent.cn/p/e45807a9853e
多線程并發(fā)(5)-http://www.itdecent.cn/p/5217588d82ba
多線程并發(fā)(6)-http://www.itdecent.cn/p/d7c888a9c03c

十九、CAS(比較并交換-樂觀鎖機制-鎖自旋)

19.1 概念及特性

CAS(Compare And Swap/Set)比較并交換。
CAS算法的過程:包含3個參數(shù)CAS(V,E,N),分別為:

  • V表示要更新的變量(內(nèi)存值)
  • E表示預期值(舊的)
  • N表示當前值(新的)

當且僅當V值等于E值時,才會將V的值設為N,如果V值和E值不同,則說明已經(jīng)有其他線程做了更新,則當前線程什么都不做,最后,CAS返回當前V的真實值。
CAS操作是抱著樂觀的態(tài)度進行的(樂觀鎖),它總是認為自己可以成功完成操作。當多個線程同時使用CAS操作一個變量時,只有一個會勝出,并成功更新,其余均會失敗。失敗的線程不會被掛起,僅被告知失敗,并且允許再次嘗試,當然也允許失敗的線程放棄操作?;谶@樣的原理,CAS操作即使沒有鎖,也可以發(fā)現(xiàn)其他線程對當前線程的干擾,并進行恰當?shù)奶幚怼?/p>

19.2 原子包 java.util.concurrent.atomic(鎖自旋)

JDK1.5的原子包:java.util.concurrent.atomic這個包里面提供了一組原子類。其基本的特性就是在多線程環(huán)境下,當有多個線程同時執(zhí)行這些類的實力包含的方法時,具有排他性,即當某個線程進入方法,執(zhí)行其中的指令時,不會被其他現(xiàn)成打斷,而別的現(xiàn)成就像自旋鎖一樣,一直等到該方法執(zhí)行完成,才由JVM從等待隊列中選擇另一個線程進入,這只是一種邏輯上的理解。

相對于synchronized這種阻塞算法,CAS是非阻塞算法的一種常見實現(xiàn)(它在多線程環(huán)境中無需使用阻塞鎖,從而減少了線程的阻塞和上下文切換,進而提高了程序的性能。)。由于一般CPU切換時間比CPU指令集操作更長,所以J.U.C在性能上有了很大的提升。

19.3 ABA問題

CAS的ABA問題是指在并發(fā)編程中,使用CAS算法時可能會出現(xiàn)的一種問題。

ABA問題的原因是,CAS算法需要在操作值的時候,檢查某地址的內(nèi)容有沒有發(fā)生變化(和舊值進行比較),如果沒有發(fā)生變化則更新為新的值。但是,如果一個值原來是A,變成了B,又變成了A,那么使用CAS進行檢查時會發(fā)現(xiàn)它的值沒有發(fā)生變化,但是實際上卻變化了。即:這個線程在操作的時候,其他線程也在進行操作。

例如,有兩個線程T1和T2,T1從內(nèi)存地址X中取出A,這時另一個線程T2也從內(nèi)存地址X中取出A,并且線程T2進行了一系列操作將值改變成B,寫回主物理內(nèi)存。然后線程T2又將內(nèi)存地址為X的數(shù)據(jù)變?yōu)锳,這個時候線程T1進行了CAS操作發(fā)現(xiàn)內(nèi)存中仍然是A,這時線程T1進行CAS操作成功。盡管線程T1的CAS操作成功,但并不代表這個過程就沒有問題,這就是ABA問題。

為了解決ABA問題,可以使用版本號。那么A→B→A就會變成1A→2B→3A。具體來說,可以在每個值前面加上一個版本號,每次更新時將版本號加一。當進行CAS操作時,除了比較值之外,還需要比較版本號。只有當版本號不變時,才說明值沒有發(fā)生變化。這種方法可以確保在并發(fā)環(huán)境下正確地更新值。

二十、AQS(抽象的隊列同步器)

20.1 概念及特性

AbstractQueuedSynchronized類如其名,抽象的隊列式的同步器,AQS定義了一套多線程訪問共享資源的同步器框架,許多同步類實現(xiàn)都依賴于它,如常用的ReenTrantLock、Semaphore、CountDownLatch。

AbstractQueuedSynchronized底層結構

它維護了一個volatile int state(代表共享資源)和一個FIFO線程等待隊列(多線程爭用資源被阻塞時會進入此隊列)。

這里的 volatile 是關鍵字,它確保了 state 的可見性。具體來說,volatile 關鍵字可以確保每個線程在讀取 state 時都能得到最新的值,而不是讀取到已經(jīng)被其他線程修改過的舊值。

state 的訪問方式主要有三種:getState()、setState() 和 compareAndSetState():

  1. getState():這個方法用于獲取當前的狀態(tài)值。
  2. setState():這個方法用于設置新的狀態(tài)值。
  3. compareAndSetState():這個方法比較當前狀態(tài)值和期望的狀態(tài)值,如果兩者相等,則設置新的狀態(tài)值。這是一種原子操作,可以確保在多線程環(huán)境下,狀態(tài)的改變是原子的。

當一個線程需要獲取資源時,它首先會調(diào)用 getState() 方法來查看當前的狀態(tài)。如果狀態(tài)指示資源可用,那么線程就可以獲取資源并進行操作。如果狀態(tài)指示資源不可用,那么線程就會被放入等待隊列中,直到狀態(tài)變?yōu)榭捎谩?/p>

當線程釋放資源時,它會調(diào)用 setState() 方法來改變狀態(tài)值。如果此時沒有其他線程在等待該資源,那么狀態(tài)改變后,等待隊列中的線程就可以按順序獲取資源并進行操作。如果有其他線程在等待該資源,那么狀態(tài)改變后,等待隊列中的線程將再次被放入隊列中等待。

20.2 AQS定義兩種資源共享方式

Exclusive 獨占資源(ReenTrantLock)

只有一個線程能執(zhí)行,如 ReentrantLock。它又可分為公平鎖和非公平鎖:

  • 公平鎖:按照線程在隊列中的排隊順序,先到者先拿到鎖。
  • 非公平鎖:當線程要獲取鎖時,無視隊列順序直接去搶鎖,誰搶到就是誰的。

資源共享和獲取/釋放的接口:tryAcquire() 和 tryRelease() 方法。當一個線程嘗試獲取資源時,如果資源當前不可用,那么該線程會被放入等待隊列,直到資源可用。當線程釋放資源時,會檢查是否有其他線程在等待該資源。

Share共享資源(Semaphore/CountDownLatch)

多個線程可同時執(zhí)行,如 CountDownLatch、Semaphore、CyclicBarrier、ReadWriteLock。例如,ReentrantReadWriteLock 可以看成是組合式,因為 ReentrantReadWriteLock 也就是讀寫鎖允許多個線程同時對某一資源進行讀。

資源共享和獲取/釋放的接口:tryAcquireShared() 和 tryReleaseShared() 方法。在共享模式下,多個線程可以同時獲取資源,但通常會有一個限制,比如同時最多有幾個線程可以獲取資源。當一個線程嘗試獲取資源時,如果資源當前不可用,那么該線程會被放入等待隊列,直到資源可用。當線程釋放資源時,會喚醒等待隊列中的一個線程。


由于獨占模式和共享模式的接口方法不同,因此 AQS 本身并沒有定義成抽象類,而是定義了一個接口。這樣可以允許實現(xiàn)類根據(jù)具體的需求來實現(xiàn)不同的接口方法。

對于自定義同步器來說,只需要實現(xiàn) state 的獲取和釋放方式即可。具體的線程等待隊列的維護(如獲取資源失敗時的入隊操作、喚醒出隊等)已經(jīng)由 AQS 在頂層實現(xiàn)了。這樣可以讓自定義同步器專注于實現(xiàn)具體的資源獲取和釋放策略,而不需要關心線程等待隊列的維護細節(jié)。

20.3 同步器的實現(xiàn)

ReentrantLock為例,它的實現(xiàn)確實是基于 AQS(AbstractQueuedSynchronized)的,state 變量用來表示資源的狀態(tài)。在 ReentrantLock 的 lock()方法中,會調(diào)用 tryAcquire()方法嘗試獲取資源,如果 state 為 0,表示資源未被占用,這時線程會將 state 加 1,并設置自己的狀態(tài)為擁有資源狀態(tài)。此后,其他線程再嘗試 lock()時就會失敗,直到當前擁有資源的線程調(diào)用 unlock()方法釋放資源,將 state 設置為 0,其他線程才有機會獲取該鎖。

CountDownLatch的例子中,state 變量也用來表示資源的狀態(tài),但它的作用是記錄需要等待的子線程數(shù)量。在 CountDownLatch 的構造函數(shù)中,會將 state 初始化為 N,表示有 N 個子線程需要執(zhí)行。然后,這 N 個子線程會并行執(zhí)行任務,每個子線程執(zhí)行完后會調(diào)用 countDown()方法將 state 減 1。當 state 減為 0 時,表示所有子線程都已執(zhí)行完畢,這時就會喚醒主調(diào)用線程,使其從 await()方法返回,繼續(xù)后續(xù)操作。

無論是 ReentrantLock 還是 CountDownLatch,它們都利用了 AQS 提供的隊列和 state 變量等核心機制來實現(xiàn)資源的獲取和釋放。通過這種方式,可以將同步器的實現(xiàn)與具體的資源獲取和釋放策略分離,從而提供更大的靈活性。

20.4 ReentrantReadWriteLock 實現(xiàn)獨占和共享兩種方式

ReentrantReadWriteLock 是 Java 中的一個讀寫鎖,它允許多個線程同時讀取共享資源,但只允許一個線程寫入共享資源。在 ReentrantReadWriteLock 中,讀鎖和寫鎖是互斥的,因此 ReentrantReadWriteLock 實現(xiàn)的是獨占式的資源訪問。

在 ReentrantReadWriteLock 中,讀鎖和寫鎖的獲取和釋放都是通過內(nèi)部的鎖來實現(xiàn)的。讀鎖和寫鎖的狀態(tài)都是通過 state 變量來維護的。當一個線程獲取寫鎖時,它會先將 state 加 1,表示有一個寫線程正在等待獲取鎖。如果此時還有讀線程正在等待獲取讀鎖,那么這些讀線程可以繼續(xù)等待獲取鎖。但是,如果此時沒有讀線程正在等待獲取讀鎖,那么獲取寫鎖的線程就可以獲取到鎖并執(zhí)行。

在實現(xiàn) ReentrantReadWriteLock 時,需要同時實現(xiàn)獨占和共享兩種方式。具體來說,需要實現(xiàn) tryAcquire()和 tryRelease()方法來支持獨占式的資源訪問,同時還需要實現(xiàn) tryAcquireShared()和 tryReleaseShared()方法來支持共享式的資源訪問。在 ReentrantReadWriteLock 中,獲取讀鎖和寫鎖的過程是原子性的,因此可以實現(xiàn)精確的控制。

總之,ReentrantReadWriteLock 是一種支持獨占和共享兩種方式的同步器,它通過內(nèi)部的鎖來實現(xiàn)精確的控制,允許多個線程同時讀取共享資源,但只允許一個線程寫入共享資源。

最后編輯于
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容