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