併發的相關的幾個疑問

緩存一致性問題

參考下圖,同一塊主內存區域的值在處理器中多個處理單元處理了,都同時寫入的時候,就需要決定是以誰的值爲準,這個問題就是緩存一致性問題
解決辦法就是定義一套協議來處理,JVM中也有類似的協議來處理主內存與工作內存之間緩存一致性問題。

處理器、高速緩存、主內存間的交互關係

線程、主內存、工作內存三者的交互關係

JVM緩存一致性問題解決協議?

Java內存模型中定義了以下8中操作來完成主內存與工作內存之間交互的實現細節:

1、luck(鎖定):作用於主內存的變量,它把一個變量標示爲一條線程獨佔的狀態。

2、unlock(解鎖):作用於主內存的變量,它把一個處於鎖定狀態的變量釋放出來,釋放後的變量纔可以被其他線程鎖定。

3、read(讀取):作用於主內存的變量,它把一個變量的值從主內存傳輸到工作內存中,以便隨後的load動作使用。

4、load(載入):作用於工作內存的變量,它把read操作從主內存中得到的變量值放入工作內存的變量副本中。

5、use(使用):作用於工作內存的變量,它把工作內存中的一個變量的值傳遞給執行引擎,每當虛擬機遇到一個需要使用到變量的值得字節碼指令時將會執行這個操作。

6、assign(賦值):作用於工作內存的變量,它把一個從執行引擎接收到的值賦給工作內存的變量,每當虛擬機遇到一個給變量賦值的字節碼指令時執行這個操作。

7、store(存儲):作用於工作內存的變量,它把工作內存中的一個變量的值傳遞到主內存中,以便隨後的write操作使用。

8、write(寫入):作用於主內存的變量,它把store操作從工作內存中得到的變量值放入主內存的變量中。

Java內存模型還規定了執行上述8種基本操作時必須滿足如下規則:

1、不允許read和load、store和write操作之一單獨出現,以上兩個操作必須按順序執行,但沒有保證必須連續執行,也就是說,read與load之間、store與write之間是可插入其他指令的。

2、不允許一個線程丟棄它的最近的assign操作,即變量在工作內存中改變了之後必須把該變化同步回主內存。

3、不允許一個線程無原因地(沒有發生過任何assign操作)把數據從線程的工作內存同步回主內存中。

4、一個新的變量只能從主內存中“誕生”,不允許在工作內存中直接使用一個未被初始化(load或assign)的變量,換句話說就是對一個變量實施use和store操作之前,必須先執行過了assign和load操作。

5、一個變量在同一個時刻只允許一條線程對其執行lock操作,但lock操作可以被同一個條線程重複執行多次,多次執行lock後,只有執行相同次數的unlock操作,變量纔會被解鎖。

6、如果對一個變量執行lock操作,將會清空工作內存中此變量的值,在執行引擎使用這個變量前,需要重新執行load或assign操作初始化變量的值。

7、如果一個變量實現沒有被lock操作鎖定,則不允許對它執行unlock操作,也不允許去unlock一個被其他線程鎖定的變量。

8、對一個變量執行unlock操作之前,必須先把此變量同步回主內存(執行store和write操作)。


先行發生原則

  • 程序次序規則(Program Order Rule):在一個線程中,按照程序代碼順序,書寫在前面的代碼操作先行發生於後面的操作

  • 管程鎖定規則(Monitor Lock Rule):一個 unlock 操作先行發生於後面對同一個鎖的 lock 操作。“後面”指的是時間的先後順序。

  • volatile 變量規則(Volatile Variable Rule):對一個 volatile 變量的寫操作先行發生於後面對這個變量的讀操作。“後面”指的是時間的先後順序。

  • 線程啓動規則(Thread Start Rule):Thread 對象的 start() 方法先行發生於此線程的每一個動作。

  • 線程終止規則(Thread Termination Rule):線程中所有操作先行發生於對此線程的終止檢測。

  • 線程中斷規則(Thread Interruption Rule):對線程 interrupt() 方法的調用先行發生於代碼檢測到中斷事件的發生。

  • 對象終結規則(Finalizer Rule):一個對象的初始化完成(構造函數結束)先行發生於它的 finalize() 方法的開始。

  • 傳遞性(Transitivity):如果操作A先行發生於操作B,操作B先行發生於操作C,那麼操作A先行發生於操作C。

時間的先後與先行發生原則並沒有太多關係

volatile變量的特點

1、保證可見性,也就是說對volatile變量進行寫入操作的時候,其它線程能立即看到變化,而普通變量是做不到的。實現的原理就是volatile變量每次使用前都會去刷新,而每次寫入都會實時同步到主內存中。

2、volatile變量並不是併發安全的,雖然我們能立即看到volatile變量的最新值,但volatile變量參與的運算,並不是一個原子操作。

volatile變量適用的場景就是一個控制線程操作,其它work線程依賴它來進行決策,這種情況下不需要用鎖就能實現併發安全的效果

原子類的實現基礎

硬件支持的組合原子指令(CAS)是實現基礎,如:

測試並設置; 

獲取並增加; 

交換; 

比較並交換; 

加載鏈條/條件存儲;

線程安全的分類

1、不可變;例如final對象或者屬性,如果在初使化過程中沒有this引用的逃逸,初始完成後,就一定是線程安全的;
2、絕對線程安全;不需要任何外加輔助,類本身就能實現完全的線程安全訪問,則認爲這種類是絕對線程安全的;
3、相對線程安全;單個方法的調用是安全的,但是如果涉及到多個方法的調用,則不一定能保證線程安全的;jdk裏邊提供的很多系統類,如vector、HashTable、StringBuilder等,都是簡單的在單個方法上
加syncronized加實現安全,所以都只能保證單個方法的安全,是典型的相對線程安全類。
4、線程兼容;通過輔助工具,如加鎖,可以實現線程安全的類,就是線程兼容;
5、線程對立;無論如何都無法實現線程安全。

線程安全的實現方法

1、互斥同步;典型就是使用syncronized以及java.util.concurrent包裏邊的ReentrantLock;因爲需要線程阻塞,也叫做阻塞式同步;
2、非阻塞同步;不需要線程阻塞,相比阻塞鎖是悲觀鎖,這個是樂觀鎖,比如說原子類就都是非阻塞同步,實現的基礎是硬件支持的組合原子指令;
3、無同步方案;如可重入代碼(例如不引用共享數據的局部方法)、線程本地存儲(thread local storage)

鎖優化的常用手段有哪些?

鎖作爲最常用保證線程安全的工具,用的不好會導致死鎖,或者本來可以併發的變成了串行,降低了執行效率。

死鎖問題靠的是編碼邏輯實現的謹慎,而鎖的優化,則能大大的降低鎖對程序效率的影響。

1、自旋鎖與自適應自旋;

   不讓出對CPU的佔用時間,而是循環等待獲得對應的鎖。如果鎖佔用時間很短,自旋等待的效果就會非常好;反之,如果鎖被佔用的時間特別長,那麼自旋的線程只會白白的浪費處理器資源,而不會做任何有用的工作,反而會帶來性能上的浪費;我的理解是如果自旋的時間比線程上下文切換的代價還要大,就不如直接阻塞而不是自旋了。

2、鎖消除;

虛擬機檢測到一段代碼上不存在共享數據競爭的問題,就會把鎖消除;

3、鎖粗化;

一般我們是儘量把鎖的範圍減小。但是有時候反其道而行之也能提高效率,在一段代碼中反覆對同一個鎖加鎖解鎖,就可以直接對整塊代碼加鎖,以減少操作鎖的開銷;

4、輕量級鎖;


5、偏向鎖;


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章