摘要:在的包中,大神大量使用此技術(shù),實現(xiàn)了多線程的安全性。我們將變量用修飾,保證線程間的可見性。線程也通過此方法獲取當(dāng)前值,進(jìn)行操作,比較內(nèi)存值相等進(jìn)行修改。我們通過保證了對的并發(fā)線程安全,其安全的保證是通過調(diào)用的代碼實現(xiàn)的。
前言
研究java并發(fā)編程有一段時間了, 在并發(fā)編程中cas出現(xiàn)的次數(shù)極為頻繁。cas的英文全名叫做compare and swap,意思很簡單就是比較并交換。在jdk的conurrent包中,Doug Lea大神大量使用此技術(shù),實現(xiàn)了多線程的安全性。
cas的核心思想就是獲取當(dāng)前的內(nèi)存偏移值、期望值和更新值,如果根據(jù)內(nèi)存偏移值得到的變量等于期望值,則進(jìn)行更新。
總有面試官喜歡問你i++和++i,以及經(jīng)典的字符串問題,其實這些問題只要你試用javap -c這個命令反編譯一下,就一目了然。當(dāng)然今天的主題是cas,我首先來研究下a++:
//@RunWith(SpringRunner.class) //@SpringBootTest public class SblearnApplicationTests { public static volatile int a; public static void main(String[] args) { a++; } }
通過javac SblearnApplicationTests.java,javap -c SblearnApplicationTests.class可以得到:
Compiled from "SblearnApplicationTests.java" public class com.example.sblearn.SblearnApplicationTests { public static volatile int a; public com.example.sblearn.SblearnApplicationTests(); Code: 0: aload_0 1: invokespecial #1 // Method java/lang/Object."":()V 4: return public static void main(java.lang.String[]); Code: 0: getstatic #2 // Field a:I 3: iconst_1 //當(dāng)int取值-1~5采用iconst指令,取值-128~127采用bipush指令,取值-32768~32767采用sipush指令,取值-2147483648~2147483647采用 ldc 指令 4: iadd 5: putstatic #2 // Field a:I 8: return }
通過反編譯得出如上的結(jié)果,都是一些jvm的指令,百度一下就能知道意思。我們將變量a用violate修飾,保證線程間的可見性。通過jvm指令可知a++不是一個原子動作,如果多個線程同事對a進(jìn)行操作,無法保證線程安全,那怎么解決呢?
解決方案java給我們提供了一個關(guān)鍵字synchronized,可以對成員方法、靜態(tài)方法、代碼塊進(jìn)行加鎖,從而保證操作的原子性。但效率不高,還有其他辦法嗎?當(dāng)然有了,就是我們今天的主角cas。接下來我們再來看看concurrent包下的AtomicInteger:
public class AtomicInteger extends Number implements java.io.Serializable { private static final long serialVersionUID = 6214790243416807050L; // setup to use Unsafe.compareAndSwapInt for updates private static final Unsafe unsafe = Unsafe.getUnsafe(); private static final long valueOffset; static { try { valueOffset = unsafe.objectFieldOffset (AtomicInteger.class.getDeclaredField("value")); } catch (Exception ex) { throw new Error(ex); } } private volatile int value; //效果等同于a++,但保證了原子性 public final int getAndIncrement() { return unsafe.getAndAddInt(this, valueOffset, 1); } }
public final class Unsafe { public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5); public native int getIntVolatile(Object var1, long var2); //object var1:當(dāng)前AtomicInteger對象,long var2Integer對象的內(nèi)存偏移值,int var4 增加的值 public final int getAndAddInt(Object var1, long var2, int var4) { int var5; do { //從方法名字就可以看出,獲取線程可見的值 var5 = this.getIntVolatile(var1, var2); } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4)); return var5; } }
cas機(jī)制的核心類就是Unsafe,valueOffset 是其內(nèi)存偏移值。由于java語言無法直接操作底層,需要本地方法(native method)來訪問,unsafe這個類中存在大量本地方法,就是在調(diào)用c去操作特定內(nèi)存的數(shù)據(jù)。我們先假設(shè)unsafe幫我們保證了原子性,先來分析下AtomicInteger.getAndIncrement(),在jdk1.8中,其實現(xiàn)就是Unsafe.getAndAddInt()
現(xiàn)在我們假設(shè)有A、B線程同時來操作AtomicInteger,其初始值為1,根據(jù)java內(nèi)存模型,當(dāng)前主內(nèi)存AtomicInteger值為1,線程A、線程B各自的工作內(nèi)存也為1.
線程A獲得通過getIntVolatile獲取當(dāng)前值,被掛起。線程B也通過此方法獲取當(dāng)前值,進(jìn)行操作,比較內(nèi)存值相等進(jìn)行修改。
這時線程A恢復(fù),執(zhí)行compareAndSwapInt發(fā)現(xiàn)與內(nèi)存期望值不相等,重新獲取var5變量(因為被violate修飾,所以工作內(nèi)存和主內(nèi)存變量一致),再次比較與內(nèi)存期望值相等,進(jìn)行更新。
我們通過cas保證了對value的并發(fā)線程安全,其安全的保證是CAS通過調(diào)用JNI的代碼實現(xiàn)的。JNI:Java Native Interface為JAVA本地調(diào)用,允許java調(diào)用其他語言。而compareAndSwapInt就是借助C來調(diào)用CPU底層指令實現(xiàn)的。下面從分析比較常用的CPU(intel x86)來解釋CAS的實現(xiàn)原理。compareAndSwapInt方法在openjdk中依次調(diào)用的c++代碼為:unsafe.cpp,atomic.cpp和atomicwindowsx86.inline.hpp。這個本地方法的最終實現(xiàn)在openjdk的如下位置:openjdk-7-fcs-src-b147-27jun2011openjdkhotspotsrcoscpuwindowsx86vm atomicwindowsx86.inline.hpp(對應(yīng)于windows操作系統(tǒng),X86處理器)。下面是對應(yīng)于intel x86處理器的源代碼的片段:
// 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 } }
如上面源代碼所示,程序會根據(jù)當(dāng)前處理器的類型來決定是否為cmpxchg指令添加lock前綴。如果程序是在多處理器上運(yùn)行,就為cmpxchg指令加上lock前綴(lock cmpxchg)。反之,如果程序是在單處理器上運(yùn)行,就省略lock前綴(單處理器自身會維護(hù)單處理器內(nèi)的順序一致性,不需要lock前綴提供的內(nèi)存屏障效果)。
intel的手冊對lock前綴的說明如下:
1.確保對內(nèi)存的讀-改-寫操作原子執(zhí)行。在Pentium及Pentium之前的處理器中,帶有l(wèi)ock前綴的指令在執(zhí)行期間會鎖住總線,使得其他處理器暫時無法通過總線訪問內(nèi)存。很顯然,這會帶來昂貴的開銷。從Pentium 4,Intel Xeon及P6處理器開始,intel在原有總線鎖的基礎(chǔ)上做了一個很有意義的優(yōu)化:如果要訪問的內(nèi)存區(qū)域(area of memory)在lock前綴指令執(zhí)行期間已經(jīng)在處理器內(nèi)部的緩存中被鎖定(即包含該內(nèi)存區(qū)域的緩存行當(dāng)前處于獨(dú)占或以修改狀態(tài)),并且該內(nèi)存區(qū)域被完全包含在單個緩存行(cache line)中,那么處理器將直接執(zhí)行該指令。由于在指令執(zhí)行期間該緩存行會一直被鎖定,其它處理器無法讀/寫該指令要訪問的內(nèi)存區(qū)域,因此能保證指令執(zhí)行的原子性。這個操作過程叫做緩存鎖定(cache locking),緩存鎖定將大大降低lock前綴指令的執(zhí)行開銷,但是當(dāng)多處理器之間的競爭程度很高或者指令訪問的內(nèi)存地址未對齊時,仍然會鎖住總線。
2.禁止該指令與之前和之后的讀和寫指令重排序。
3.把寫緩沖區(qū)中的所有數(shù)據(jù)刷新到內(nèi)存中。
cas的缺點就是會出現(xiàn)aba問題,假如一個字母為a,它經(jīng)歷a->b->a的過程,實際已經(jīng)改變兩次,但值相同。部分業(yè)務(wù)場景是不允許出現(xiàn)這種情況的(比如銀行轉(zhuǎn)賬..).解決辦法就是添加版本號,他就變成了1a->2b>3a。jdk1.5之后也提供了AtomicStampedReference來解決aba問題。
總結(jié)自旋cas如果長時間不成功,將會對cpu帶來非常大的開銷。cas只能保證一個共享變量的原子操作。所以非常簡單的操作又不想引入鎖,cas是一個非常好的選擇。
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://systransis.cn/yun/68372.html
摘要:第一個字被稱為。經(jīng)量級鎖的加鎖過程當(dāng)一個對象被鎖定時,被復(fù)制到當(dāng)前嘗試獲取鎖的線程的線程棧的鎖記錄空間被復(fù)制的官方稱為。根據(jù)鎖對象目前是否處于被鎖定狀態(tài),撤銷偏向后恢復(fù)到未鎖定或經(jīng)量級鎖定狀態(tài)。 Synchronized關(guān)鍵字 synchronized的鎖機(jī)制的主要優(yōu)勢是Java語言內(nèi)置的鎖機(jī)制,因此,JVM可以自由的優(yōu)化而不影響已存在的代碼。 任何對象都擁有對象頭這一數(shù)據(jù)結(jié)構(gòu)來支持鎖...
摘要:并發(fā)需要解決的問題功能性問題線程同步面臨兩個問題,想象下有兩個線程在協(xié)作工作完成某項任務(wù)。鎖可用于規(guī)定一個臨界區(qū),同一時間臨界區(qū)內(nèi)僅能由一個線程訪問。并發(fā)的數(shù)據(jù)結(jié)構(gòu)線程安全的容器,如等。 并發(fā)指在宏觀上的同一時間內(nèi)同時執(zhí)行多個任務(wù)。為了滿足這一需求,現(xiàn)代的操作系統(tǒng)都抽象出 線程 的概念,供上層應(yīng)用使用。 這篇博文不打算詳細(xì)展開分析,而是對java并發(fā)中的概念和工具做一個梳理。沿著并發(fā)模...
摘要:耐心看完的你或多或少會有收獲并發(fā)的核心就是包,而的核心是抽象隊列同步器,簡稱,一些鎖啊信號量啊循環(huán)屏障啊都是基于。 耐心看完的你或多或少會有收獲! Java并發(fā)的核心就是 java.util.concurrent 包,而 j.u.c 的核心是AbstractQueuedSynchronizer抽象隊列同步器,簡稱 AQS,一些鎖啊!信號量??!循環(huán)屏障??!都是基于AQS。而 AQS 又是...
摘要:這兩種策略的區(qū)別就在于,公平策略會讓等待時間長的線程優(yōu)先執(zhí)行,非公平策略則是等待時間長的線程不一定會執(zhí)行,存在一個搶占資源的問題。 之前有一篇文章我們簡單的談到了Java中同步的問題,但是可能在平常的開發(fā)中,有些理論甚至是某些方式是用不到的,但是從程序的角度看,這些理論思想我們可以運(yùn)用到我們的開發(fā)中,比如是不是應(yīng)該一談到同步問題,就應(yīng)該想到用synchronized?,什么時候應(yīng)該用R...
摘要:在線程處理任務(wù)期間,其它線程要么循環(huán)訪問,要么一直阻塞等著線程喚醒,再不濟(jì)就真的如我所說,放棄鎖的競爭,去處理別的任務(wù)。寫鎖的話,獨(dú)占寫計數(shù),排除一切其他線程。 回顧 在上一篇 Java并發(fā)核心淺談 我們大概了解到了Lock和synchronized的共同點,再簡單總結(jié)下: Lock主要是自定義一個 counter,從而利用CAS對其實現(xiàn)原子操作,而synchronized是c++...
閱讀 2835·2023-04-26 01:00
閱讀 767·2021-10-11 10:59
閱讀 2990·2019-08-30 11:18
閱讀 2691·2019-08-29 11:18
閱讀 1025·2019-08-28 18:28
閱讀 3024·2019-08-26 18:36
閱讀 2142·2019-08-23 18:16
閱讀 1075·2019-08-23 15:56