更新時間:2022-10-28 10:34:49 來源:動力節點 瀏覽1494次
首先認識一下Java CAS多線程:CAS是支持并發的第一個處理器提供原子的測試并設置操作,通常在單位上運行這項操作。操作數為V,A,B。
CAS 操作包含三個操作數 —— 內存位置(V)、預期原值(A)和新值(B)。如果內存位置的值與預期原值相匹配,那么處理器會自動將該位置值更新為新值。否則,處理器不做任何操作。無論哪種情況,它都會在 CAS 指令之前返回該位置的值。(在 CAS 的一些特殊情況下將僅返回 CAS 是否成功,而不提取當前值。)CAS 有效地說明了“我認為位置 V 應該包含值 A;如果包含該值,則將 B 放到這個位置;否則,不要更改該位置,只告訴我這個位置的值即可。”
CAS 是處理器級別的原語,也就是說單核情況下處理器可以保證 CAS 操作的原子性。
但事實是多核環境下,CAS 的原子性也是可以得到保證的。在單核環境下保證原子性的基礎上,多核情況下 CAS 通過以下方式保證操作的原子性:
1. 通過總線鎖,保證其修改動作的線程排他性。
2. 通過緩存一致性協議,保證處理器緩存中的值對其它核心的可見性。
3. 多核環境下通過 lock 內存屏障,保證多線程下的有序性,并保證其值立即從寫緩沖區刷新到緩存,結合2,保證了操作結果對其它核心的可見性。
我們看一下 Intel X86 架構下 CAS 的一段實現:
// Adding a lock prefix to an instruction on MP machine
// VC++ doesn't like the lock prefix to be on a single line
// so we can't insert a label after the lock prefix.
// By emitting a lock prefix, we can define a label after it.
#define LOCK_IF_MP(mp) __asm cmp mp, 0 \
__asm je L0 \
__asm _emit 0xF0 \
__asm L0:
inline jint Atomic::cmpxchg (jint exchange_value, volatile jint* dest, jint compare_value) {
// alternative for InterlockedCompareExchange
int mp = os::is_MP();
__asm {
mov edx, dest
mov ecx, exchange_value
mov eax, compare_value
LOCK_IF_MP(mp)
cmpxchg dword ptr [edx], ecx
}
}
程序會根據當前處理器的類型來決定是否為cmpxchg指令添加lock前綴。如果程序是在多處理器上運行,就為cmpxchg指令加上lock前綴(lock cmpxchg)。反之,如果程序是在單處理器上運行,就省略lock前綴。
因為但處理器情況下,有序性有天然的保證。同時不需要強制將結果從緩沖區刷新入緩存,因為處理器在讀取值時會先從寫緩沖區中尋找,找不到才回去緩存中尋找,所以單核情況下無論是否將結果刷入緩存,操作結果都是對所有線程可見的。
可以看到 CAS 保證可見性與有序性的原理與 volatile 一樣,都是通過處理器提供的內存屏障,盡管它們不在一個抽象層級的,這樣比較并不合適。
CAS 效率的一個重要影響因素是cache miss,如果 CAS 操作的內存地址不在當前處理器的緩存中的話,需要通過緩存一致性協議在其它處理器的緩存中尋找,如果都找不到則去內存加載。最好情況下,也就是沒有發生 cache miss 的話,CAS 大概需要 60 個時鐘周期,而鎖操作在最好情況下需要大約 100 個時鐘周期(一個“round trip 對”包括獲取鎖和隨后的釋放鎖),通常情況下 CAS 的效率是高于鎖操作的。
CAS 是一種樂觀鎖,再補充一下樂觀鎖與悲觀鎖:
悲觀鎖,正如其名,具有強烈的獨占和排他特性。它指的是對數據被外界(包括本系統當前的其他事務,以及來自外部系統的事務處理)修改持保守態度。因此,在整個數據處理過程中,將數據處于鎖定狀態。悲觀鎖的實現,往往依靠數據庫提供的鎖機制(也只有數據庫層提供的鎖機制才能真正保證數據訪問的排他性,否則,即使在本系統中實現了加鎖機制,也無法保證外部系統不會修改數據)。
之所以叫做悲觀鎖,是因為這是一種對數據的修改抱有悲觀態度的并發控制方式。我們一般認為數據被并發修改的概率比較大,所以需要在修改之前先加鎖。悲觀并發控制實際上是“先取鎖再訪問”的保守策略,為數據處理的安全提供了保證。
樂觀鎖機制采取了更加寬松的加鎖機制。樂觀鎖是相對悲觀鎖而言,也是為了避免數據庫幻讀、業務處理時間過長等原因引起數據處理錯誤的一種機制,但樂觀鎖不會刻意使用數據庫本身的鎖機制,而是依據數據本身來保證數據的正確性。
相對于悲觀鎖,在對數據庫進行處理的時候,樂觀鎖并不會使用數據庫提供的鎖機制。一般的實現樂觀鎖的方式就是記錄數據版本。樂觀并發控制相信事務之間的數據競爭(data race)的概率是比較小的,因此盡可能直接做下去,直到提交的時候才去鎖定,所以不會產生任何鎖和死鎖。
每種機制都不是十全十美的,有優點必然也有缺點,我們來看一下 CAS 的缺點:
1.cpu開銷大,在高并發下,許多線程,更新一變量,多次更新不成功,循環反復,給cpu帶來大量壓力。
2.只是一個變量的原子性操作,不能保證代碼塊的原子性。
3.ABA問題
ABA問題:內存值V=100;
threadA 將50,改為0;
threadB 將50,改為0;
threadC 將0,改為50
正常情況下 A 執行完 B 應該失敗,但是如果 B 阻塞住了,期間 C 執行成功,那么 A 與 B 都可以執行成功,一次本應該失敗的操作成功了。因為不是先上鎖再操作,所以我們不能保證線程對共享變量訪問的有序性(這可不是三大特性里的有序性),這樣在實際生產過程中,因為每個操作都有自己的意義,亂序的訪問共享變量會導致操作錯誤。
比如一杯水,A喝完了,B倒滿了,C又喝完了。C無法分辨杯子里的水有沒有被人喝過,項目中的邏輯很可能是如果杯子被人用過,C就不喝了,但是B將其倒滿導致C無法對該條件進行正確的判斷。在特定情況下,我們不能只看數據的當前值,也應該看數據的狀態,我們可以為數據增加一個狀態--版本號來解決這個問題。
其實悲觀鎖也存在ABA問題,碰到這種情況也需要為數據增加一個狀態來監控數據。但是悲觀鎖并不是根據數據的當前狀態來判斷操作是否執行的,而是有鎖的邏輯,所以我們在編寫程序時本身就不會出現原值被改過又被改回來所以我能執行這種邏輯漏洞,我們會有其它控制位來調度線程的執行。
在JDK1.5之后,Java程序中才可以使用CAS操作,該操作由sun.misc.Unsafe類里面的compareAndSwapInt()和compareAndSwapLong()等幾個方法包裝提供,虛擬機在內部對這些方法做了特殊處理,即時編譯出來的結果就是一條平臺相關的處理器CAS指令,沒有方法調用的過程,或者可以認為是無條件內聯進去了。
由于Unsafe類不是提供給用戶提供調用的類(Unsafe.getUnsafe()的代碼中限制了只有啟動類加載器(Bootstrap ClassLoader)加載的Class才能訪問它),因此,如果不采用反射手段,我們只能通過其他的Java API來間接使用它,如J.U.C包里面的整數原子類,其中的 compareAndSet()和 getAndIncrement()等方法都使用了 Unsafe 類的 CAS 操作。如果大家想了解更多相關知識,不妨來關注一下本站的Java多線程編程,里面還有更豐富的知識等著大家去學習,希望對大家能夠有所幫助。
0基礎 0學費 15天面授
有基礎 直達就業
業余時間 高薪轉行
工作1~3年,加薪神器
工作3~5年,晉升架構
提交申請后,顧問老師會電話與您溝通安排學習