高併發的可見性搞不明白,就不用再研發了

本篇重點介紹

可見性是java中一種並不直觀的特性,是指執行緒之間的可見性,即一個執行緒修改的狀態對另一個執行緒是否是可見的,也就是一個執行緒修改了記憶體中的結果另一個執行緒能否馬上就能看到。通常情況下,因為執行緒執行速度的快慢導致了執行緒間資料讀取的先後問題,我們無法確保執行讀操作的執行緒能適

地看到其

執行緒寫入的值,有時甚至是根本不可能的事情。在高併發中可見性問題是保障執行緒之間互動的一個重要知識點。

禁止快取的可見性變數

volatile是輕量級的同步機制

關鍵點

:保證可見性、不保證原子性、禁止指令重排

保證可見性

解析

:當多個執行緒訪問同一個變數時,一個執行緒修改了這個變數的值,其他執行緒能夠立即看到修改的值。

當不新增volatile關鍵字時示例:

package com。jian8。juc;import java。util。concurrent。TimeUnit;/** * 1驗證volatile的可見性 * 1。1 如果int num = 0,number變數沒有新增volatile關鍵字修飾 * 1。2 添加了volatile,可以解決可見性 */public class VolatileDemo { public static void main(String[] args) { visibilityByVolatile();//驗證volatile的可見性 } /** * volatile可以保證可見性,及時通知其他執行緒,主物理記憶體的值已經被修改 */ public static void visibilityByVolatile() { MyData myData = new MyData(); //第一個執行緒 new Thread(() -> { System。out。println(Thread。currentThread()。getName() + “\t come in”); try { //執行緒暫停3s TimeUnit。SECONDS。sleep(3); myData。addToSixty(); System。out。println(Thread。currentThread()。getName() + “\t update value:” + myData。num); } catch (Exception e) { // TODO Auto-generated catch block e。printStackTrace(); } }, “thread1”)。start(); //第二個執行緒是main執行緒 while (myData。num == 0) { //如果myData的num一直為零,main執行緒一直在這裡迴圈 } System。out。println(Thread。currentThread()。getName() + “\t mission is over, num value is ” + myData。num); }}class MyData { // int num = 0; volatile int num = 0; public void addToSixty() { this。num = 60; }}

輸出結果:

thread1 come inthread1 update value:60//執行緒進入死迴圈

當我們加上

volatile

關鍵字後,

volatile int num = 0;

輸出結果為:

thread1 come inthread1 update value:60main mission is over, num value is 60//程式沒有死迴圈,結束執行

不保證原子性

描述

:原子性是指資料整體在當前的業務中不可分割、具有完整性,資料在業務流轉過程中必須保證完整,要麼同時成功要麼同時失敗。

驗證示例(變數新增volatile關鍵字,方法不新增synchronized):

package com。jian8。juc;import java。util。concurrent。TimeUnit;import java。util。concurrent。atomic。AtomicInteger;/** * 1驗證volatile的可見性 * 1。1 如果int num = 0,number變數沒有新增volatile關鍵字修飾 * 1。2 添加了volatile,可以解決可見性 * * 2。驗證volatile不保證原子性 * 2。1 原子性指的是什麼 * 不可分割、完整性,即某個執行緒正在做某個具體業務時,中間不可以被加塞或者被分割,需要整體完整,要麼同時成功,要麼同時失敗 */public class VolatileDemo { public static void main(String[] args) {// visibilityByVolatile();//驗證volatile的可見性 atomicByVolatile();//驗證volatile不保證原子性 } /** * volatile可以保證可見性,及時通知其他執行緒,主物理記憶體的值已經被修改 */ //public static void visibilityByVolatile(){} /** * volatile不保證原子性 * 以及使用Atomic保證原子性 */ public static void atomicByVolatile(){ MyData myData = new MyData(); for(int i = 1; i <= 20; i++){ new Thread(() ->{ for(int j = 1; j <= 1000; j++){ myData。addSelf(); myData。atomicAddSelf(); } },“Thread ”+i)。start(); } //等待上面的執行緒都計算完成後,再用main執行緒取得最終結果值 try { TimeUnit。SECONDS。sleep(4); } catch (InterruptedException e) { e。printStackTrace(); } while (Thread。activeCount()>2){ Thread。yield(); } System。out。println(Thread。currentThread()。getName()+“\t finally num value is ”+myData。num); System。out。println(Thread。currentThread()。getName()+“\t finally atomicnum value is ”+myData。atomicInteger); }}class MyData { // int num = 0; volatile int num = 0; public void addToSixty() { this。num = 60; } public void addSelf(){ num++; } AtomicInteger atomicInteger = new AtomicInteger(); public void atomicAddSelf(){ atomicInteger。getAndIncrement(); }}

執行三次結果為:

//1。main finally num value is 19580 main finally atomicnum value is 20000//2。main finally num value is 19999main finally atomicnum value is 20000//3。main finally num value is 18375main finally atomicnum value is 20000//num並沒有達到20000

禁止指令重排

描述

:在計算機CPU中程式碼邏輯均是以一組組的指令形式交付給CPU。有序性是指在計算機執行程式時,為了提高效能,編譯器在程式碼達到某些條件時會對程式碼邏輯的指令排序。

單執行緒環境裡面每次執行程式碼只有一個執行緒,也就確保了程式最終執行結果和程式碼順序執行的結果一致。(

某些存在的

競態

環境也會觸發

) 在多執行緒環境中由於執行緒交替執行,編譯器最佳化重排的存在,兩個執行緒中使用的變數能否保證一致性時無法確定的,導致了結果無法預測。

高併發的可見性搞不明白,就不用再研發了

程式碼例項:

描述:在公共類中宣告成員變數:

int a,b,x,y=0

高併發的可見性搞不明白,就不用再研發了

說明

:常規情況下,執行緒按照正常的邏輯先後執行。在高併發下,

如果編譯器對這段程式程式碼執行重排最佳化後,可能出現如下情況:

高併發的可見性搞不明白,就不用再研發了

說明

:在多執行緒環境下,由於編譯器最佳化重排的存在,兩個執行緒中使用的變數能否保證一致性是無法確定的。

volatile可以實現禁止指令重排,從而避免了多執行緒環境下程式出現亂序執行的現象

記憶體屏障

(Memory Barrier)又稱記憶體柵欄,是一個CPU指令,他的作用有兩個:

保證特定操作的執行順序

保證某些變數的記憶體可見性(利用該特性實現volatile的記憶體可見性)

由於編譯器和處理器都能執行指令重排最佳化。如果在執行前Memory Barrier則會告訴編譯器和CPU,不管什麼指令都不能和這條Memory Barrier指令重排順序,也就是說透過插入記憶體屏障禁止在記憶體屏障前後的指令執行重排序最佳化。記憶體屏障另外一個作用是強制刷出各種CPU的快取資料,因此任何CPU上的執行緒都能讀取到這些資料的最新版本。(

本質上由於快取記憶體禁止了

高併發的可見性搞不明白,就不用再研發了

JMM(java記憶體模型)

JMM(Java Memory Model)本身是一種抽象的概念,並不真實存在,他描述的

一組規則或規範,透過這組規範

定義了程式中各個變數(包括例項欄位,靜態欄位和構成陣列物件的元素)的訪問方式

JMM關於同步的規定

① 執行緒解鎖前,必須把共享變數的值重新整理回主記憶體

② 執行緒加鎖前,必須讀取主記憶體的最新值到自己的工作記憶體

③ 加鎖解鎖時同一把鎖

解析

:由於JVM執行程式的實體是執行緒,而每個執行緒建立時JVM都會為其建立一個工作記憶體(有的

成為

棧空間),工作記憶體是每個執行緒的私有資料區域,而java記憶體模型中規定所有變數都儲存在JVM主記憶體

,主記憶體是共享記憶體區域,所有執行緒都可以訪問,

但執行緒對變數的操作(讀取賦值等)必須在私有記憶體區域中進行。每個執行緒在這個過程中都要將變數從主記憶體複製到自己的獨佔記憶體空間,在私記憶體中對變數副本進行操作,操作完成後再將變數副本寫回主記憶體。由於

不能直接操作主記憶體中的變數,各個執行緒中的工作記憶體中儲存著主記憶體的

變數副本,因此不同的執行緒件無法訪問對方的工作記憶體,執行緒間的通訊(傳值)也必須透過主記憶體來完成。Voliate可以很好地保證共享變數線上程之間的

可見性。

volatile的使用

當普通單例模式在多執行緒情況下:

public class SingletonDemo { private static SingletonDemo instance = null; private SingletonDemo() { System。out。println(Thread。currentThread()。getName() + “\t 構造方法SingletonDemo()”); } public static SingletonDemo getInstance() { if (instance == null) { instance = new SingletonDemo(); } return instance; } public static void main(String[] args) { //構造方法只會被執行一次// System。out。println(getInstance() == getInstance());// System。out。println(getInstance() == getInstance());// System。out。println(getInstance() == getInstance()); //併發多執行緒後,構造方法會在一些情況下執行多次 for (int i = 0; i < 10; i++) { new Thread(() -> { SingletonDemo。getInstance(); }, “Thread ” + i)。start(); } }}

說明:其構造方法在多執行緒情況下可能會被執行多次

利用【

volatile

】的解決方式:

單例模式DCL程式碼

DCL (Double Check Lock雙端檢鎖機制)在加鎖前和加鎖後都進行一次判斷

public static SingletonDemo getInstance() { if (instance == null) { synchronized (SingletonDemo。class) { if (instance == null) { instance = new SingletonDemo(); } } } return instance; }

大部分執行結果構造方法只會被執行一次

,但指令重排機制會讓程式很小的機率出現構造方法被執行多次。

DCL(雙端

檢鎖

)機制不一定能執行緒安全,由於執行緒執行速度快慢有別的原因或指令重排序,可能導致重入的風險。

描述:

在某一個執行緒執行到第一次檢測,讀取到instance不為null時,instance的引用物件可能沒有完成初始化。instance=new SingleDemo();可以被分為

一下

三步(虛擬碼):

memory = allocate();//1。分配物件記憶體空間instance(memory); //2。初始化物件instance = memory; //3。設定instance執行剛分配的記憶體地址,此時instance!=null

指令重排是不會對有明顯依賴關係的程式碼進行重排序的,我們的程式碼可能存在著邏輯上的先後關係,但JVM並無法智慧識別。比如圖中步驟2和步驟3在依賴上不存在關係,而且無論重排前還是重排後程序的執行結果在單執行緒中並沒有改變序列語義執行的一致性,在JVM看來這種重排最佳化是被允許的。如圖

如果3步驟提前於步驟2,而instance還沒有初始化完成,這個時候便出現

A執行緒訪問instance不為null時,由於instance例項尚未初始化完成,執行緒A本該獲取到存在的instance例項,但由於錯誤判斷獲取也就造成了執行緒安全問題。

單例模式volatile程式碼

說明

:在上述程式碼中的SingletongDemo例項上加上【volatile】:

private static volatile SingletonDemo instance = null;

有任何問題歡迎留言交流~

整理總結不易,如果覺得這篇文章有意思的話,歡迎轉發、收藏,給我一些鼓勵~

有想看的內容或者建議,敬請留言!

最近利用空餘時間整理了一些

精選Java架構學習影片和大廠專案底層知識點,需要的同學歡迎私信我發給你~一起學習進步!有任何問題也歡迎交流~

Java日記本,每日存檔超實用的技術乾貨學習筆記,每天陪你前進一點點~