3.线程安全之可见性、有序性、原子性是什么?
小陳:上一篇說(shuō)了JAVA內(nèi)存模型,但是后面說(shuō)了在多線程并發(fā)操作的時(shí)候有可見(jiàn)性問(wèn)題,我現(xiàn)在迫不及待想知道線程安全的可見(jiàn)性、原子性、有序性是啥了
老王:哈哈,可以。我先說(shuō)說(shuō)我自己對(duì)可見(jiàn)性、有序性、原子性的理解:
可見(jiàn)性
上一篇講了,多個(gè)線程同時(shí)對(duì)某一個(gè)共享變量進(jìn)行操作的時(shí)候,存在線程A的操作對(duì)線程B不可見(jiàn)的問(wèn)題。簡(jiǎn)單來(lái)說(shuō)就是線程A執(zhí)行了某些操作對(duì)數(shù)據(jù)進(jìn)行了變更;但是線程B并不知道,所以還是使用舊數(shù)據(jù)干它自己的活。
小陳:這么講,按照概念來(lái)理解我還是很模糊啊,能不能搞個(gè)例子來(lái)講解一下?
老王,沒(méi)問(wèn)題,我就結(jié)合上次你提的那個(gè)JAVA內(nèi)存模型可能導(dǎo)致數(shù)據(jù)不一致的這個(gè)例子給你講解一下
比如線程A和線程B都執(zhí)行x++操作(x的初始值是0),線程A執(zhí)行完了之后將主內(nèi)存的值更新為1,但是線程B由于已經(jīng)將 x = 0?讀取進(jìn)入自己的工作內(nèi)存了,不知道線程A將x更新為1了,所以還是使用x=0去進(jìn)行++操作。
像這種,就是典型的可見(jiàn)性問(wèn)題,就是線程A操作了數(shù)據(jù),但是線程B不可見(jiàn),感知不到。
小陳:嘿嘿,看來(lái)上次我的猜測(cè)沒(méi)錯(cuò)啊,無(wú)論是CPU緩存架構(gòu)下還是JAVA內(nèi)存模型都是有可見(jiàn)性的問(wèn)題。
老王:沒(méi)錯(cuò),你說(shuō)的這個(gè)問(wèn)題是存在的,但是還是有些手段可以避免的,后面我們?cè)賮?lái)討論。下面我們?cè)賮?lái)說(shuō)一下有序性的問(wèn)題
有序性
有序性是指由于JIT動(dòng)態(tài)編譯器、操作系統(tǒng)為了給提高程序的執(zhí)行效率,可能會(huì)對(duì)按順序書寫好的指令進(jìn)行重排,線程或者CPU執(zhí)行的時(shí)候不一定按照程序書寫的順序來(lái)執(zhí)行:
比如程序的書寫順序是 指令1 -> 指令2 -> 指令3;但是由于指令重排序,某個(gè)線程執(zhí)行這幾個(gè)指令的時(shí)候,比如說(shuō)線程A執(zhí)行的時(shí)候,可能先執(zhí)行指令3,然后再執(zhí)行指令2、指令1。導(dǎo)致別的線程,比如說(shuō)線程B看到線程A的指令執(zhí)行是亂序的。
我搞個(gè)代碼給你講解一下:
線程A在執(zhí)行數(shù)據(jù)庫(kù)、http客戶端的初始化工作,初始化完畢之后將initOk初始化表示置為true表示初始化完畢。
// 步驟1 dataSource = initDataSource(); // 步驟2 httpClient = initHttpClient(); // 步驟3 initOK = true;線程B在這里一直監(jiān)聽(tīng)線程A是否初始化資源完畢,看到initOK標(biāo)識(shí)為true表示初始化結(jié)束。開(kāi)始執(zhí)行業(yè)務(wù)操作,獲取數(shù)據(jù),根據(jù)數(shù)據(jù)發(fā)起網(wǎng)絡(luò)調(diào)用。
// 步驟4 while(!initOK) { } // 步驟5 Object data = dataSource.getData(); // 步驟6 httpClient.request(data);上面這段代碼,正常來(lái)說(shuō)線程A的執(zhí)行順序應(yīng)該是 步驟1 -> 步驟2 -> 步驟3。但是由于JIT動(dòng)態(tài)編譯器或者操作系統(tǒng)可能對(duì)指令進(jìn)行重排序,所以可能執(zhí)行順序是 步驟3 -> 步驟1 -> 步驟2。
這樣就會(huì)導(dǎo)致線程B先看到了initOk = true,這樣就會(huì)導(dǎo)致線程B直接跳出while循環(huán),跳出等待,執(zhí)行dataSource.getData方法,執(zhí)行httpClient.request()方法;但是線程A的步驟1、步驟2還沒(méi)執(zhí)行dataSource、httpClient是null,會(huì)拋出空指針異常。
小陳:等等,我來(lái)理解一下;線程A先執(zhí)行了initOK = true;導(dǎo)致線程B跳出了while循環(huán),然后調(diào)用dataSource.getData方法,由于線程A還沒(méi)執(zhí)行dataSource = initDataSource()方法,所以dataSource對(duì)象可能是null值,這樣線程B調(diào)用的時(shí)候可能拋出空指針異常,是這樣吧?
老王:沒(méi)錯(cuò),理解得非常好,小陳你果然聰明啊;你這個(gè)理解力,我對(duì)后面講解的文章越來(lái)越有信心了。
小陳:嘿嘿......
老王:上面這種有序性問(wèn)題,在多線程并發(fā)執(zhí)行的時(shí)候,由于指令的重排序存在,很可能是會(huì)發(fā)生的。
這就是有序性帶來(lái)的線程安全問(wèn)題,也就是線程B看到線程A的執(zhí)行時(shí)亂序的,也就是不是按照步驟1、2、3這樣順序的來(lái)執(zhí)行。
簡(jiǎn)單點(diǎn)來(lái)講就是線程A還沒(méi)初始化好,就將標(biāo)識(shí)initOk設(shè)置為true。導(dǎo)致線程B誤以為線程A搞定了,然后去獲取數(shù)據(jù),發(fā)起http請(qǐng)求,然后...,然后線程B就掛了...(線程B:線程A這坑爹的,還沒(méi)初始化好就告訴我搞定了,這不是坑我嘛...)
老王:說(shuō)了可見(jiàn)性、有序性的問(wèn)題,下面我們?cè)賮?lái)說(shuō)說(shuō)原子性問(wèn)題。
原子性
老王:原子性是說(shuō)某個(gè)操作是不可分割的、不可中斷的。
小陳:這個(gè)不可分割、不可中斷是啥意思?
老王:
比如之前說(shuō)的JAVA內(nèi)存模型定義的8中操作;read、load、use、assign、store、write、lock、unlock等八種指令都是原子的。
老王:比如說(shuō)read指令,不可分割:說(shuō)的是這條指令是讀取數(shù)據(jù)最小的指令了,不能再拆分成更多的指令
小陳:不可分割是不是說(shuō)它就是最小的執(zhí)行單元了,不能被拆分的意思?
老王:沒(méi)錯(cuò),就是這個(gè)意思.....
小陳:哦哦,這個(gè)不可分割我懂了,那不可中斷又是啥玩意?
老王:簡(jiǎn)單來(lái)講就是不能執(zhí)行到一半就不干了,比如這個(gè)read指令,你不能讀取一個(gè)變量的數(shù)據(jù),只讀取到一半的時(shí)候就撂挑子不干了;要執(zhí)行就一起全部執(zhí)行,不能干了一半就不干了,同時(shí)也不能被其它外部的因素打斷了。
小陳:那意思是說(shuō)cpu執(zhí)行read指令,執(zhí)行到一半的時(shí)候,就把這個(gè)線程掛起來(lái),這個(gè)是不被允許的咯。
老王:哈哈,就是這樣的。要干就全部都干了,不能中途搞了一半你跟我說(shuō)退出了....
小陳:老王你真牛逼,這么晦澀的東西都被你三言兩句簡(jiǎn)單的話就給說(shuō)清楚了。
老王:那是,畢竟我可是單身十多年...;不,是工作十多年的老兵了,“技巧”早就磨練的杠杠的......
小陳:......
小陳:道理我是聽(tīng)明白了,實(shí)際編碼里面那些操作是原子的,那些不是原子的呢?
老王:給你講講下面的例子就知道了:
比如下面的操作:
(1)y = 1;
(2)x++;
(3)z = y;
(1)其中y = 1操作是原子的,因?yàn)橹皇菆?zhí)行了load操作,將1直接load給y,只有一條指令的執(zhí)行。??
(2) x++操作就不是原子性的,之前畫圖講解過(guò),i++操作經(jīng)過(guò),read、load、use、assign、store、write等六個(gè)操作;雖然每個(gè)指令都是原子的,但是合并起來(lái)并不是原子的。
比如說(shuō)線程A執(zhí)行read和load操作將工作內(nèi)存的變量x的值載入自己工作內(nèi)存的變量副本中。但是還沒(méi)來(lái)得及執(zhí)行后續(xù)的use、assign、store、write指令,這個(gè)時(shí)候線程A就被掛起了。
線程A被掛起期間,線程B就也執(zhí)行了read、load指令將變量x放入線程B的工作內(nèi)存里了。這就相當(dāng)于線程A的這6條指令沒(méi)有連續(xù)執(zhí)行完,被中斷了,中途CPU又去執(zhí)行別的指令了,并不是不可分割、不可中斷的。
(3)z = y 也不是原子的,它先要執(zhí)行read指令讀取y的值,然后執(zhí)行l(wèi)oad執(zhí)行賦值給z。并不是單一的原子指令
小陳:哇塞,老王你太牛逼了,你這么說(shuō)我全懂了。
小陳:既然多線程并發(fā)操作的時(shí)候會(huì)有這些問(wèn)題,那操作系統(tǒng)或者說(shuō)JAVA底層是怎么解決這些問(wèn)題達(dá)到并發(fā)安全的效果的呢?
老王:操作系統(tǒng)設(shè)計(jì)者肯定是會(huì)想到這些問(wèn)題的,這就是我們下面要慢慢講解的話題了,操作系統(tǒng)或者JAVA底層是怎么解決這些并發(fā)安全的問(wèn)題的。
老王:小陳,給你個(gè)任務(wù),你去看看MESI一致性協(xié)議的內(nèi)容,下面我們講解一下MESI一致性協(xié)議,以及MESI一致性協(xié)議是如何解決可見(jiàn)性問(wèn)題的。
關(guān)注小陳,公眾號(hào)上更多更全的文章
JAVA并發(fā)文章目錄(公眾號(hào))
JAVA并發(fā)專題 《筑基篇》
1.什么是CPU多級(jí)緩存模型?
2.什么是JAVA內(nèi)存模型?
3.線程安全之可見(jiàn)性、有序性、原子性是什么?
4.什么是MESI緩存一致性協(xié)議?怎么解決并發(fā)的可見(jiàn)性問(wèn)題?
JAVA并發(fā)專題《練氣篇》
5.volatile怎么保證可見(jiàn)性?
6.什么是內(nèi)存屏障?具有什么作用?
7.volatile怎么通過(guò)內(nèi)存屏障保證可見(jiàn)性和有序性?
8.volatile為啥不能保證原子性?
9.synchronized是個(gè)啥東西?應(yīng)該怎么使用?
10.synchronized底層之monitor、對(duì)象頭、Mark Word?
11.synchronized底層是怎么通過(guò)monitor進(jìn)行加鎖的?
12.synchronized的鎖重入、鎖消除、鎖升級(jí)原理?無(wú)鎖、偏向鎖、輕量級(jí)鎖、自旋、重量級(jí)鎖
13.synchronized怎么保證可見(jiàn)性、有序性、原子性?
JAVA并發(fā)專題《結(jié)丹篇》
14. JDK底層Unsafe類是個(gè)啥東西?
15.unsafe類的CAS是怎么保證原子性的?
16.Atomic原子類體系講解
17.AtomicInteger、AtomicBoolean的底層原理
18.AtomicReference、AtomicStampReference底層原理
19.Atomic中的LongAdder底層原理之分段鎖機(jī)制
20.Atmoic系列Strimped64分段鎖底層實(shí)現(xiàn)源碼剖析
JAVA并發(fā)專題《金丹篇》
21.AQS是個(gè)啥?為啥說(shuō)它是JAVA并發(fā)工具基礎(chǔ)框架?
22.基于AQS的互斥鎖底層源碼深度剖析
23.基于AQS的共享鎖底層源碼深度剖析
24.ReentrantLock是怎么基于AQS實(shí)現(xiàn)獨(dú)占鎖的?
25.ReentrantLock的Condition機(jī)制底層源碼剖析
26.CountDownLatch 門栓底層源碼和實(shí)現(xiàn)機(jī)制深度剖析
27.CyclicBarrier 柵欄底層源碼和實(shí)現(xiàn)機(jī)制深度剖析
28.Semaphore 信號(hào)量底層源碼和實(shí)現(xiàn)機(jī)深度剖析
29.ReentrantReadWriteLock 讀寫鎖怎么表示?
30. ReentrantReadWriteLock 讀寫鎖底層源碼和機(jī)制深度剖析
JAVA并發(fā)專題《元神篇》并發(fā)數(shù)據(jù)結(jié)構(gòu)篇
31.CopyOnAarrayList 底層分析,怎么通過(guò)寫時(shí)復(fù)制副本,提升并發(fā)性能?
32.ConcurrentLinkedQueue 底層分析,CAS 無(wú)鎖化操作提升并發(fā)性能?
33.ConcurrentHashMap詳解,底層怎么通過(guò)分段鎖提升并發(fā)性能?
34.LinkedBlockedQueue 阻塞隊(duì)列怎么通過(guò)ReentrantLock和Condition實(shí)現(xiàn)?
35.ArrayBlockedQueued 阻塞隊(duì)列實(shí)現(xiàn)思路竟然和LinkedBlockedQueue一樣?
36.DelayQueue 底層源碼剖析,延時(shí)隊(duì)列怎么實(shí)現(xiàn)?
37.SynchronousQueue底層原理解析
JAVA并發(fā)專題《飛升篇》線程池底層深度剖析
38. 什么是線程池?看看JDK提供了哪些默認(rèn)的線程池?底層竟然都是基于ThreadPoolExecutor的?
39.ThreadPoolExecutor 構(gòu)造函數(shù)有哪些參數(shù)?這些參數(shù)分別表示什么意思?
40.內(nèi)部有哪些變量,怎么表示線程池狀態(tài)和線程數(shù),看看道格.李大神是怎么設(shè)計(jì)的?
41. ThreadPoolExecutor execute執(zhí)行流程?怎么進(jìn)行任務(wù)提交的?addWorker方法干了啥?什么是workder?
42. ThreadPoolExecutor execute執(zhí)行流程?何時(shí)將任務(wù)提交到阻塞隊(duì)列? 阻塞隊(duì)列滿會(huì)發(fā)生什么?
43. ThreadPoolExecutor 中的Worker是如何執(zhí)行提交到線程池的任務(wù)的?多余Worker怎么在超出空閑時(shí)間后被干掉的?
44. ThreadPoolExecutor shutdown、shutdownNow內(nèi)部核心流程
45. 再回頭看看為啥不推薦Executors提供幾種線程池?
46. ThreadPoolExecutor線程池篇總結(jié)
總結(jié)
以上是生活随笔為你收集整理的3.线程安全之可见性、有序性、原子性是什么?的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: Android基础知识精简版(转)
- 下一篇: Scheme语言深入