掌握這些套路,你也能順利解決并發(fā)問題
大家好,我是冰河~~
“原來我之前寫的代碼存在嚴(yán)重的并發(fā)問題,這下我可要好好學(xué)學(xué)并發(fā)編程了,經(jīng)過老大的耐心講解,我已經(jīng)知道了之前代碼出現(xiàn)并發(fā)問題的原因了,也就是多個線程同時讀寫共享變量時,會將共享變量復(fù)制到各自的工作內(nèi)存中進行處理,這樣就會導(dǎo)致緩存不一致的問題。那怎么解決問題呢?看來還是要向老大請教才行呀!”,小菜認(rèn)真的思考著。
一、情景再現(xiàn)
小菜開發(fā)的統(tǒng)計調(diào)用商品詳情接口次數(shù)的功能代碼存在嚴(yán)重的線程安全問題,會導(dǎo)致統(tǒng)計出來的結(jié)果數(shù)據(jù)遠遠低于預(yù)期結(jié)果,這個問題困擾了小菜很長時間,經(jīng)過老王的耐心講解,小菜已經(jīng)明白了出現(xiàn)線程安全問題的原因。但是,作為211、985畢業(yè)的高材生,小菜并不會止步于此,他可是立志要成為像老王一樣的牛人。所以,他也在思考著解決這些線程安全問題的方案。
二、尋求幫助
盡管小菜思想上很積極,也很主動,但是對于一個剛剛畢業(yè)的應(yīng)屆生來說,很多知識不夠系統(tǒng),也不夠全面,在網(wǎng)上搜索對應(yīng)的解決方案時,也不知道哪些信息是正確的,哪些是模棱兩可的。于是,小菜決定還是要請教自己的直屬領(lǐng)導(dǎo)老王。
這天,小菜還是早早的來到了公司等老王的到來。過了一會兒,他看到老王來到了公司,便主動走到老王的工位說:“老大,我現(xiàn)在知道我寫的代碼為什么會出現(xiàn)線程安全的問題了,但是有哪些方案可以解決這些問題,我現(xiàn)在還不太清楚,可以給我講講嗎?”。
“可以,你拿上筆和本子,我們還是到會議室說吧”,說著,老王便拿起了電腦,與小菜一起向會議室走去。
三、并發(fā)問題解決方案
“我們先來從整體上了解下解決并發(fā)問題存在哪些方案,其實,總體上來說,解決并發(fā)問題可以分為有鎖方案和無鎖方案”,說著老王便打開電腦畫了一張解決并發(fā)問題解決方案的圖,如圖3-1所示。
老王接著說:“看這張圖,解決并發(fā)問題的方案總體上可以分成有鎖方案和無鎖方案,有鎖方案可以分成synchronized鎖和Lock鎖兩種方案,無鎖方案可以分成局部變量、CAS原子類、ThreadLocal和不可變對象等幾種方案。小菜你先把這張圖記一下,接下來,我們再一個個講一下這些方案”。
“好的”,小菜回應(yīng)道。
四、加鎖方案
“好了,我們繼續(xù)講,這里,我們一起講synchronized鎖和Lock鎖,它們統(tǒng)稱為加鎖方案”,老王說道,“像synchronized鎖和Lock鎖,都是采用了悲觀鎖策略,實現(xiàn)的功能類似,只不過synchronized鎖是通過JVM層面來實現(xiàn)加鎖和釋放鎖,在使用時,不需要我們自己手動釋放鎖。而Lock鎖是通過編碼方式實現(xiàn)加鎖和釋放鎖,在使用時,需要我們自己在finally代碼塊中釋放鎖,我們先來看一段代碼”。說著,老王便在IDEA中噼里啪啦的敲了一段代碼,這段代碼的類是SynchronizedLockCounter。
SynchronizedLockCounter類的源碼詳見:concurrent-design-patterns-immutable工程下的io.binghe.concurrent.design.right.SynchronizedLockCounter。
public class SynchronizedLockCounter {
private int count;
private Lock lock = new ReentrantLock();
public void lockMethod(){
lock.lock();
try{
this.add();
}finally {
lock.unlock();
}
}
public synchronized void synchronizedMethod(){
this.add();
}
private void add(){
count++;
}
}
“看這個類,lockMethod()使用了Lock加鎖和釋放鎖,并且是我們自己在finally代碼塊中手動釋放了鎖。而使用synchronized加鎖時,并沒有手動釋放鎖,兩個方法都具備原子性。這點明白嗎?”。
“明白”,小菜說道。
“好,那接下來,我們再分析下上面的代碼,其實,在執(zhí)行count++操作時,還是會分成三個步驟”。
- 1.從主內(nèi)存讀取count的值。
- 2.將count的值進行加1操作。
- 3.將count的值寫回主內(nèi)存。
“使用synchronized和Lock對方法加鎖,都會保證上面三個步驟的原子性,那是怎么保證的呢?我們再來看一張圖”,說著老王又畫了一張圖,如圖3-2所示。
“我們結(jié)合這張圖來講”,老王畫完圖對小菜說道:“假設(shè)現(xiàn)在有線程1和線程2兩個線程同時搶占鎖資源,假設(shè)線程1搶占鎖成功后執(zhí)行代碼邏輯,而線程2由于搶占鎖失敗,就會進入等待隊列,當(dāng)線程1執(zhí)行完代碼邏輯釋放鎖之后,就會通知等待隊列中的線程去嘗試重新獲取鎖,如果此時線程2成功獲取到鎖,就會執(zhí)行代碼邏輯”。
小菜也是邊聽邊記。
接著老王又說到:“synchronized鎖和Lock能夠保證原子性的原理了解了吧?”。
“了解了”,小菜回應(yīng)道。
“好,你先簡單消化下,我們接下來簡單講講局部變量”。
“好的”,小菜在本子上快速的記錄著。
五、局部變量
“好了,我們繼續(xù)講講局部變量吧”,老王說道。
“好的”,小菜回應(yīng)道。
“其實說起局部變量,它只會存在于每個線程的工作線程中,不會在多個線程之前共享,所以不會有線程安全的問題,我們還是看一個代碼片段”,說著老王又寫了一個LocalVariable類。
源碼詳見:concurrent-design-patterns-immutable工程下的io.binghe.concurrent.design.right.LocalVariable。
public class LocalVariable {
public void localVariableMethod(){
int count = 0;
count++;
System.out.println(count);
}
}
“假設(shè)多個線程執(zhí)行LocalVariable類的localVariableMethod()方法,只有當(dāng)每個線程執(zhí)行到int count = 1; 這行代碼時,才會在各自線程的工作內(nèi)存中創(chuàng)建count局部變量,并且這個count變量不會在多個線程之間共享”。老王一邊說,一邊畫圖,如圖3-3所示。
“看到圖我明白了”,這個時候,小菜說話了:“局部變量只會存在于每個線程的工作內(nèi)存中,多個線程之間根本不會共享局部變量的值,所以,局部變量是線程安全的”。
“很好,看來對于局部變量是理解透徹了”,老王微笑著說,“那我們再來看看CAS原子類”。
六、CAS原子類
“在講CAS原子類之前,我們先來看看什么是CAS,CAS的英文全稱是Compare And Swap,中文就是比較并交換”。
“CAS我知道是怎么回事”,小菜說道:“CAS使用了3個基本操作數(shù),需要讀寫的內(nèi)存值 V,進行比較的值 A和要寫入的新值 B,當(dāng)且僅當(dāng) V 的值等于 A 時, CAS 通過原子方式用新值 B 來更新 V 的值,否則不會執(zhí)行任何操作,并且CAS中的比較和交換是一個原子操作,一般情況下是一個自旋操作,也就是會不斷的重試”。
“很好,小菜,看來你對CAS已經(jīng)有所了解了”,老王說道。
“嘿嘿,前幾天看過相關(guān)的知識點”,小菜撓了撓頭發(fā)。
“好,那我們再講講Java中的CAS原子類”,老王繼續(xù)道。
“Java中提供了一系列以Atomic開頭的CAS原子類,它們的并發(fā)性能比較高,可以多個線程同時執(zhí)行,并且不會出現(xiàn)線程安全問題”,說著,老王又寫了一段代碼。
源碼詳見:concurrent-design-patterns-immutable工程下的io.binghe.concurrent.design.right.AtomicIntegerTest。
public class AtomicIntegerTest {
private AtomicInteger atomicIntegerCount = new AtomicInteger(0);
public void add(){
atomicIntegerCount.incrementAndGet();
}
}
“在這段代碼中,聲明了一個AtomicInteger類型的成員變量atomicIntegerCount,并且在add()方法中調(diào)用了atomicIntegerCount的incrementAndGet()方法,此時無論多少個線程調(diào)用add()方法,都不會出現(xiàn)線程安全的問題”。
“這是為什么呢?”,此時的小菜有點不解,“atomicIntegerCount也是成員變量呀,它會在多個線程之前共享,為什么就沒有線程安全問題呢?”。
“別急,我們慢慢來”,老王說道:“其實答案就在AtomicInteger類的源碼里”,說著老王打開了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;
“我們先來看這部分代碼”,老王繼續(xù)說,“在AtomicInteger代碼中,會有一個Unsafe類的實例對象,Unsafe類是JDK中提供的一個硬件級別的原子操作類,底層是通過native方法調(diào)用C++代碼實現(xiàn)的功能,提供了內(nèi)存分配和釋放、線程掛起和恢復(fù),定位對象字段的內(nèi)存地址和修改對象在內(nèi)存地址里的字段值等等一系列的操作,Java中也基于Unsafe類實現(xiàn)了CAS操作”。
“Unsafe類我在學(xué)校的時候了解一點,但是具體有點忘記了,今天又想起來了”,小菜說道。
“很好”,老王繼續(xù)說,“我們再使用AtomicInteger類時,主要是使用里面的CAS操作,就拿AtomicIntegerTest類中,在add()方法里調(diào)用AtomicInteger的incrementAndGet()方法來說吧,最終會調(diào)用到AtomicInteger類的getAndAddInt()方法”。
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;
}
“在getAndAddInt()方法中,首先會獲取內(nèi)存中的舊值,然后賦值給var5變量,接著會調(diào)用compareAndSwapInt()方法通過CAS的方式進行比較并交換操作,如果操作失敗,就會進入while循環(huán),直到操作成功。其中,compareAndSwapInt()方法底層調(diào)用的是C++代碼實現(xiàn)的功能,它能夠保證比較并交換操作的原子性,這樣就能夠避免并發(fā)問題”。老王繼續(xù)說。
“我們再來看看你昨天寫的代碼,如果使用AtomicInteger類實現(xiàn)的話,就不會出現(xiàn)線程安全問題了”,說著老王又在IDEA中寫下了RightCounter類。
源碼詳見:concurrent-design-patterns-immutable工程下的io.binghe.concurrent.design.right.RightCounter。
public class RightCounter {
private AtomicInteger atomicIntegerCounter = new AtomicInteger(0);
public void accessVisit(){
atomicIntegerCounter.incrementAndGet();
}
public int getVisitCount(){
return atomicIntegerCounter.get();
}
}
“這段代碼就不會出現(xiàn)線程安全問題了,那這段代碼的執(zhí)行流程是啥呢?我們繼續(xù)看一張圖”,說著,老王大手一揮,又畫了一張圖,如圖3-4所示。
“假設(shè)此時有兩個線程,分別是線程1和線程2同時訪問RightCounter類的accessVisit()方法,此時主內(nèi)存中的visitCount值為0,線程1和線程2同時對visitCount的值進行累加操作。此時假設(shè)線程1和線程2都讀取到的visitCount的值都是0,線程1成功執(zhí)行了CAS操作,將visitCount的值由0變更為1。而線程2在執(zhí)行CAS操作時,發(fā)現(xiàn)此時內(nèi)存中的visitCount的值是1不是0,所以,線程2會重新讀取內(nèi)存中的visitCount的值,此時從內(nèi)存中讀取到的visitCount的值就為1,接下來,就會將visitCount的值由1變更為2,這樣就得出了正確的結(jié)果。這里,明白了嗎”?老王問小菜。
“明白了”,小菜回答道。
“好,我們再來講講ThreadLocal”。
七、ThreadLocal
“ThreadLocal其實很簡單,沒有想象的那么復(fù)雜。ThreadLocal本質(zhì)上也是在每個線程里存儲一份數(shù)據(jù)的副本,這個數(shù)據(jù)副本不會在多個線程之間共享,互不影響,還是來看圖”。老王是真牛,又要畫圖了,如圖3-5所示。
畫完圖,老王繼續(xù)說:“按照圖來說,假設(shè)我們現(xiàn)在定義了一個名字為count的ThreadLocal類,它會在每個線程中復(fù)制一份Integer對象,但是每個線程復(fù)制的Integer對象,并不是同一個對象,每個對象只會被一個線程操作。在多個線程之間不存在共享變量,自然就不會有線程安全問題”。
“噢,ThreadLocal理解起來確實比較簡單,這個我學(xué)會了”,小菜興奮的說。
“很好,小菜,那我們再講講不可變對象?能消化吧?”。
“好的,能消化”。。。
八、不可變對象
“不可變對象,從其名字就可以看出,說的是這個對象一經(jīng)創(chuàng)建,對外的一些狀態(tài)就不會再發(fā)生變化了,如果一個對象是不變的,無論有多少個線程來訪問它,它也不會變化。連對象都不變了,那它肯定就是線程安全的了”。
“這里有點聽不懂”,小菜說道。
“不急,我們來舉個例子”,老王說道,“比如,我們在開發(fā)過程中,經(jīng)常式使用的字符串對象,本質(zhì)上就是一個不可變對象,例如,String name = 'xiaocai',我們說的字符串是'xiaocai'這個字符串,而不是指的引用’xiaocai‘ 字符串的name變量,哪怕對'xiaocai'這個字符串進行了一系列的操作,例如拼接了其他的字符串,得到了一個新的字符串'good morning, xiaocai', 原來的'xiaocai'這個字符串也不會發(fā)生變化,這樣說明白了嗎?”。
“明白了,我記一下”。
“好,今天講的知識點有點多,自己要好好總結(jié)和消化下啊”,老王對小菜說。
“好的,我先記一下,下班后回去后,我再好好總結(jié)和思考下”,小王說到。
“好,那我們出去吧”。
“好的”。
二人一起走出了會議室,小菜今天又學(xué)到了不少知識。
九、本章總結(jié)
本章,主要以老王的視角為小菜,介紹了解決并發(fā)問題的常見方案。首先,從總體上介紹了并發(fā)問題的解決方案。接下來,以此介紹了加鎖方案、局部變量、CAS原子類、ThreadLocal和不可變對象。這些方案都能夠解決線程的安全問題,主人公小菜今天又學(xué)到了不少知識。