您當前的位置:首頁 > 文化

java多執行緒,高併發知識詳解(1)——Volatile關鍵字(可見性)

作者:由 逛街的貓 發表于 文化時間:2022-05-20

Volatile的可見性:

透過對JMM的瞭解,各個執行緒對主記憶體中共享變數的操作都是各個執行緒各自複製到自己工作記憶體進行操作後,再寫回到主記憶體中的。

這就可能存在:你一個執行緒AAA修改了共享變數X的值,但還未寫回記憶體時,另外一個執行緒BBB又對主記憶體中同一個共享變數X進行操作,但此時AAA執行緒工作記憶體中共享變數X對執行緒BBB來說並不可見,這種工作記憶體與主記憶體同步延遲現象就造成了可見性的問題。

class

MyData

{

//mydata。java ——-> mydata。class ——-> JVM位元組碼

volatile

int

number

=

0

//初始值

public

void

addTo60

(){

this

number

=

60

//只要有執行緒呼叫這個方法,就可以把number設定為60

}

//請注意,此時number前面是加了volatile關鍵字修飾的。volatile不保證原子性

public

void

addplusspluss

()

{

number

++;

}

AtomicInteger

atomicInteger

=

new

AtomicInteger

()

public

void

addAtomic

(){

atomicInteger

getAndIncrement

();

//原子性加1,相當於i++;

}

}

/**

1。驗證volatile的可見性

1。1 假如 int number = 0,number變數之前根本沒有新增volatile關鍵字修飾,沒有可見性

1。2 添加了volatile,可以解決可見性問題。

2。 驗證volatile 不保證原子性

2。1 原子性指的是什麼意思?

不可分割,完整性,也即某個執行緒正在做某個具體業務時,中間不可以被加塞或者分割。需要整體完整。要麼同時成功,要麼同時失敗。

2。2 volatile不保證原子性

不用synchronize,是因為synchronize是重量級的

2。3 如何解決原子性?

使用JUC下 AtomicInteger 來解決原子性。

*/

//volatile 可以保證可見性,及時通知其它執行緒,主物理記憶體的值已經被修改。

public

class

VolatileDemo

{

public

station

void

main

String

[]

args

){

//main是一切方法的執行入口

MyData

mydata

=

new

MyData

();

//資源類

for

int

i

=

1

i

<

20

i

++){

new

Thread

(()->

{

for

int

j

=

1

j

<=

1000

j

++){

mydata

addplusplus

();

}

}

},

String

valueOf

i

))。

start

();

}

//需要等上面20個執行緒都全部計算完成後,在用main執行緒取的最終的結果值看是多少

while

Thread

activeCount

()

>

2

){

Thread

yield

)();

}

system

out

println

thread

currentThread

()。

getName

()+

“ finally nmber number”

+

mydata

number

);

}

}

*******有序性:

計算機在執行程式時,為了提高效能,編譯器和處理器的常常會對指令做重排,一般分以下三種:

原始碼——>編譯器最佳化的重排——>指令並行的重排——>記憶體系統的重排——>最終執行的指令

單執行緒環境裡面確保程式最終執行結果和程式碼順序執行的結果一致。

處理器在進行重排時必須要考慮指令之間的資料依賴性。

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

小結:volatile實現禁止指令重排最佳化,從而避免多執行緒下程式出現亂序執行的現象。

先了解一個概念:記憶體遮蔽(Memory Barrier)又稱記憶體柵欄,是一個CPU指令,它的作用有兩個:

1、保證特定操作的執行順序

2、保證某系變數的記憶體可見性(利用該特性實現Volatile的記憶體可見性)。

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

對Volatile變數進行寫操作時,會在寫操作後加入一條store屏障指令,將工作記憶體中的共享變數值重新整理回到主記憶體。

對Volatile變數進行讀操作時,會在讀操作前加入一條load屏障指令,從記憶體中讀取共享變數。

最經典的是Volatile DCL單例模式,一下程式碼就是DCL單例模式程式碼

public class SingletonDemo{

//單例第一步

privae static volatile SingletonDemo instance = null;

//單例第二步

private SingletonDemo(){

System。out。println(Thread。currentThread()。getName()+“我是構造方法SingletonDemo()”);

}

//單例第三步 加入synchronize是可以解決的,但是synchronize是重量級的,太重,不提倡使用synchronize

//使用DCL模式(double check lock 雙端檢鎖機制)

public static SingletonDemo getInstance(){

if(instance == null){

//同步程式碼塊

synchronized(SingletonDemo。class){

if(instance == null){

instance = new SingletonDemo();

}

}

}

//單例第四步

return instance;

}

public static void main(String[] args){

//多執行緒訪問單例模式。併發多執行緒後,情況發生了很大的變化

for(int i =1; i <=10 ;i++){

new Thread(()-> {

SingletonDemo。getInstance();

},String。valueOf(i))。start();

}

}

}

分析:

DCL(雙端檢鎖)機制不一定執行緒安全,原因是有指令重排的存在,加入Volatile可以禁止指令重排。

原因在於某一個執行緒執行到第一次檢測,讀取到instance不為null是,instance的引用物件 可能沒有完成初始化。instance = new SingleTonDemo();可以分為以下三步完成(虛擬碼)

首先memory(記憶體)= allocate();//1 分配物件記憶體空間

其次instance(memory);//2 初始化物件

最後 instance = memory // 3 設定instance指向剛分配的記憶體地址,此時instance != null

步驟2 步驟3 不存在資料依賴關係,而且無論重排前,還是重排後,程式的執行結果在單執行緒中並沒有改變,因此,這種重排最佳化是允許的。

還有一種情況:

首先 memory(記憶體) = allocate();//1 分配物件記憶體空間

其次instance = memory // 3 設定instance指向剛分配的記憶體地址,此時instance != null,但是,物件還沒有初始化完成。

最後 instance(memory);//2 初始化物件

指令重排只會保證序列語義的執行的一致性(單執行緒),但並不會關心多執行緒間的語義一致性。

所以當一條執行緒方位instance不為null時,由於instance例項未必已經初始化完成,也就造成了執行緒安全的問題。

(作者衷心的對大家說,既然做了IT這一行,就要不斷的去學習,就要不斷的去完善自己,其實我這些知識也是在書中學習的,書中自由黃金屋,書中自有顏如玉。)

題外話,徐鳳年裡面,儒聖軒轅敬城,就是看書能看出來個陸地神仙,何況我們做技術的,天花板永遠再漲。