Java 線程安全問(wèn)題的本質(zhì)
點(diǎn)擊上方藍(lán)色字體,選擇“標(biāo)星公眾號(hào)”
優(yōu)質(zhì)文章,第一時(shí)間送達(dá)
? 作者?|??Arnold-zhao
來(lái)源 |? urlify.cn/ryQFJn
66套java從入門(mén)到精通實(shí)戰(zhàn)課程分享
出現(xiàn)線程安全的問(wèn)題本質(zhì)是因?yàn)椋?/span>
主內(nèi)存和工作內(nèi)存數(shù)據(jù)不一致性以及編譯器重排序?qū)е隆?/span>
所以理解上述兩個(gè)問(wèn)題的核心,對(duì)認(rèn)知多線程的問(wèn)題則具有很高的意義;
簡(jiǎn)單理解CPU
CPU除了控制器、運(yùn)算器等器件還有一個(gè)重要的部件就是寄存器。其中寄存器的作用就是進(jìn)行數(shù)據(jù)的臨時(shí)存儲(chǔ)。
CPU的運(yùn)算速度是非??斓模瑸榱诵阅蹸PU在內(nèi)部開(kāi)辟一小塊臨時(shí)存儲(chǔ)區(qū)域,并在進(jìn)行運(yùn)算時(shí)先將數(shù)據(jù)從內(nèi)存復(fù)制到這一小塊臨時(shí)存儲(chǔ)區(qū)域中,運(yùn)算時(shí)就在這一小快臨時(shí)存儲(chǔ)區(qū)域內(nèi)進(jìn)行。我們稱這一小塊臨時(shí)存儲(chǔ)區(qū)域?yàn)榧拇嫫鳌?/span>
CPU讀取指令是往內(nèi)存里面去讀取的,讀一條指令放到CPU中,CPU去執(zhí)行,對(duì)內(nèi)存的讀取速度比較慢,所以從內(nèi)存讀取的速度去決定了這個(gè)CPU的執(zhí)行速度的。所以無(wú)論我們的CPU怎么去升級(jí),但是如果這方面速度沒(méi)有解決的話,其的性能也不會(huì)得到多大的提升。
為了彌補(bǔ)這個(gè)缺陷,所以添加了高速緩存的機(jī)制,如ARM A11的處理器,它的1級(jí)緩存中的容量是64KB,2級(jí)緩存中的容量是8M,
通過(guò)增加cpu高速緩存的機(jī)制,以此彌補(bǔ)服務(wù)器內(nèi)存讀寫(xiě)速度的效率問(wèn)題;
JVM虛擬機(jī)類比于操作系統(tǒng)
JVM虛擬計(jì)算機(jī)平臺(tái)就類似于一個(gè)操作系統(tǒng)的角色,所以在具體實(shí)現(xiàn)上JVM虛擬機(jī)也的確是借鑒了很多操作系統(tǒng)的特點(diǎn);
JAVA中線程的工作空間(working memory)就是CPU的寄存器和高速緩存的抽象描述,cpu在計(jì)算的時(shí)候,并不總是從內(nèi)存讀取數(shù)據(jù),它的數(shù)據(jù)讀取順序優(yōu)先級(jí) 是:寄存器-高速緩存-內(nèi)存;
而在JAVA的內(nèi)存模型中也是同等的,Java內(nèi)存模型中規(guī)定了所有的變量都存儲(chǔ)在主內(nèi)存中,每條線程還有自己的工作內(nèi)存(類似于CPU的高速緩存),線程的工作內(nèi)存中保存了該線程使用到的變量到主內(nèi)存副本拷貝,線程對(duì)變量的所有操作(讀取、賦值)都必須在工作內(nèi)存中進(jìn)行,而不能直接讀寫(xiě)主內(nèi)存中的變量,操作完成后再將變量寫(xiě)回主內(nèi)存。不同線程之間無(wú)法直接訪問(wèn)對(duì)方工作內(nèi)存中的變量,線程間變量值的傳遞均需要在主內(nèi)存來(lái)完成?;娟P(guān)系如下圖:

注意:這里的Java內(nèi)存模型,主內(nèi)存、工作內(nèi)存與Java內(nèi)存區(qū)域模型的Java堆、棧、方法區(qū)不是同一層次內(nèi)存劃分,這兩者基本上沒(méi)有關(guān)系。
重排序
在執(zhí)行程序時(shí),為了提高性能,編譯器和處理器常常會(huì)對(duì)指令進(jìn)行重排序。一般重排序可以分為如下三種:

舉例如下:
public?class?Singleton?{
????public?static??Singleton?singleton;
????/**
?????*?構(gòu)造函數(shù)私有,禁止外部實(shí)例化
?????*/
????private?Singleton()?{};
????public?static?Singleton?getInstance()?{
????????if?(singleton?==?null)?{
????????????singleton?=?new?Singleton();
????????}
????????return?singleton;
????}
}
如上,一個(gè)簡(jiǎn)單的單例模式,按照對(duì)象的構(gòu)造過(guò)程,實(shí)例化一個(gè)對(duì)象1、可以分為三個(gè)步驟(指令):
1、 分配內(nèi)存空間。
2、 初始化對(duì)象。
3、 將內(nèi)存空間的地址賦值給對(duì)應(yīng)的引用。
但是由于操作系統(tǒng)可以對(duì)指令進(jìn)行重排序,所以上面的過(guò)程也可能變?yōu)槿缦碌倪^(guò)程:
1、 分配內(nèi)存空間。
2、 將內(nèi)存空間的地址賦值給對(duì)應(yīng)的引用。
3、 初始化對(duì)象 。
所以,如果出現(xiàn)并發(fā)訪問(wèn)getInstance()方法時(shí),則可能會(huì)出現(xiàn),線程二判斷singleton是否為空,此時(shí)由于當(dāng)前該singleton已經(jīng)分配了內(nèi)存地址,但其實(shí)并沒(méi)有初始化對(duì)象,則會(huì)導(dǎo)致return 一個(gè)未初始化的對(duì)象引用暴露出來(lái),以此可能會(huì)出現(xiàn)一些不可預(yù)料的代碼異常;
當(dāng)然,指令重排序的問(wèn)題并非每次都會(huì)進(jìn)行,在某些特殊的場(chǎng)景下,編譯器和處理器是不會(huì)進(jìn)行重排序的,但上述的舉例場(chǎng)景則是大概率會(huì)出現(xiàn)指令重排序問(wèn)題(關(guān)于指令重排序的概念后續(xù)給出詳細(xì)的地址)
原創(chuàng)聲明:作者:Arnold.zhao 博客園地址:https://www.cnblogs.com/zh94
匯總
所以,如上可知,多線程在執(zhí)行過(guò)程中,數(shù)據(jù)的不可見(jiàn)性,原子性,以及重排序所引起的指令有序性 三個(gè)問(wèn)題基本是多線程并發(fā)問(wèn)題的三個(gè)重要特性,也就是我們常說(shuō)的:
并發(fā)的三大特性:原子性,有序性,可見(jiàn)性;
原子性:代碼操作是否是原子操作(如:i++ 看似一個(gè)代碼片段,實(shí)際的執(zhí)行中將會(huì)分為三步執(zhí)行,則必然是非原子化的操作,在多線程的場(chǎng)景中則會(huì)出現(xiàn)異常)
有序性:CPU執(zhí)行代碼指令時(shí)的有序性;
可見(jiàn)性:由于工作線程的內(nèi)存與主內(nèi)存的數(shù)據(jù)不同步,而導(dǎo)致的數(shù)據(jù)可見(jiàn)性問(wèn)題;
一些解釋
但是,問(wèn)題就真的有那么復(fù)雜嗎?如果按照上面所說(shuō)的問(wèn)題,i++是非原子操作,就會(huì)出現(xiàn)并發(fā)異常的問(wèn)題,new Object() 就會(huì)出現(xiàn)重排序的并發(fā)問(wèn)題,那么Java開(kāi)發(fā)還能做嗎。。我隨便寫(xiě)個(gè)方法代碼,豈不是就會(huì)出現(xiàn)并發(fā)問(wèn)題?但是為什么我開(kāi)發(fā)了這么久的代碼,也沒(méi)有出現(xiàn)過(guò)方法并發(fā)導(dǎo)致的異常問(wèn)題啊?
燒的麻袋;
這里就要說(shuō)明另外一個(gè)問(wèn)題,JVM的線程棧,JVM線程棧中是線程獨(dú)有的內(nèi)存空間(如:程序計(jì)數(shù)器以線程棧幀)而線程棧幀中的局部變量表則用來(lái)存儲(chǔ)當(dāng)前所執(zhí)行方法的基本數(shù)據(jù)類型(包含 reference, returnAddress等),所以當(dāng)方法在被線程執(zhí)行的過(guò)程中,相關(guān)的對(duì)象引用信息,以及基本類型的數(shù)據(jù)都是線程獨(dú)有的,并不會(huì)出現(xiàn)多個(gè)線程訪問(wèn)時(shí)的并發(fā)問(wèn)題,也就是簡(jiǎn)單來(lái)說(shuō):一個(gè)方法內(nèi)的變量定義以及方法內(nèi)的業(yè)務(wù)代碼,是不會(huì)出現(xiàn)并發(fā)問(wèn)題的。多個(gè)線程并不會(huì)共享一個(gè)方法內(nèi)的變量數(shù)據(jù),而是每個(gè)方法內(nèi)的定義都屬于當(dāng)前該執(zhí)行線程的獨(dú)有棧空間中。(所以通過(guò)Java線程棧的這一獨(dú)特特性自然當(dāng)中則為我們省了很多事項(xiàng);)
但是由于我們的線程的數(shù)據(jù)操作不可能每次都去訪問(wèn)主存中的數(shù)據(jù),對(duì)于線程所使用到的變量需要copy至線程內(nèi)存中以增加我們的執(zhí)行速度,所以就引出了我們上述所提到的并發(fā)問(wèn)題的本質(zhì)問(wèn)題,線程工作空間和主內(nèi)存的數(shù)據(jù)不同步而導(dǎo)致的數(shù)據(jù)共享時(shí)的可見(jiàn)性問(wèn)題;
如:此時(shí)定義一個(gè)簡(jiǎn)單的類
class?Person{
????int?a?=?1;
????int?b?=?2;
????public?void?change()?{
????????a?=?3;
????????b?=?a;
????}
????public?void?print()?{
????????String?result?=?"b="?+?b?+?";a="?+?a;
????????System.out.println(result);
????}
????
????public?static?void?main(String[]?args)?{
????????while?(true)?{
????????????final?Person?test?=?new?Person();
????????????new?Thread(()?->?{
????????????????Thread.sleep(10);
????????????????test.change();
????????????}).start();
????????????new?Thread(()?->?{
????????????????Thread.sleep(10);
????????????????test.print();
????????????}).start();
????????}
????}
}
如上,假設(shè)此時(shí)多個(gè)線程同時(shí)訪問(wèn)change()以及print() 方法,則可能會(huì)出現(xiàn)print所輸出的結(jié)果是:b=2;a=1或者b=3;a=3;這兩種都是正常現(xiàn)象,但還有可能是會(huì)輸出結(jié)果是:b=2;a=3以及b=3;a=1;
Person類所定義的變量a和b,按照J(rèn)VM內(nèi)存區(qū)域劃分,在對(duì)象實(shí)例化后則都是存儲(chǔ)到數(shù)據(jù)堆中;
按照我們上述關(guān)于線程工作內(nèi)存的解釋來(lái)看,此時(shí)線程在執(zhí)行change()方法和print()方法時(shí),由于兩個(gè)方法都有關(guān)于外部變量的引用,所以需要copy主內(nèi)存中的這兩個(gè)變量副本到對(duì)應(yīng)的線程工作內(nèi)存中進(jìn)行操作,執(zhí)行完以后再同步至主內(nèi)存中。
此時(shí)在A線程執(zhí)行完change()方法后,a=3,b=3;但此時(shí)a=3在執(zhí)行完成后還沒(méi)有同步到主內(nèi)存,但b=3此時(shí)已經(jīng)提供至主內(nèi)存了,那么此時(shí)B線程執(zhí)行print()數(shù)據(jù)輸出后,則得到的是結(jié)果是:b=3;a=1;同理也可以得到b=2;a=3的可能性結(jié)果;所以此處則由于線程共享變量的可見(jiàn)性問(wèn)題,而導(dǎo)致了上述的問(wèn)題;
正是由于存在上述所提到的線程并發(fā)所可能引起的種種問(wèn)題,所以JDK則也有了后續(xù)的一系列多線程玩法:ThreadLocal,CountDownLatch,ReentrantLock,Unsafe,synchronized,volatile,Executor,Future 這些供開(kāi)發(fā)者在開(kāi)發(fā)程序時(shí)用來(lái)對(duì)多線程保駕護(hù)航的助手類,以及JDK已經(jīng)自身開(kāi)發(fā)好的支持線程安全的一些工具類,StringBuffer,CopyOnWriteArrayList, ConcurrentHashMap,AtomicInteger等,供開(kāi)發(fā)者開(kāi)箱即用;后續(xù)針對(duì)這些JDK自身所提供的一些類的玩法會(huì)做進(jìn)一步說(shuō)明,順便系統(tǒng)整理下腦中的信息,形成有效的知識(shí)結(jié)構(gòu);End;
粉絲福利:Java從入門(mén)到入土學(xué)習(xí)路線圖
???

?長(zhǎng)按上方微信二維碼?2 秒
感謝點(diǎn)贊支持下哈?
