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