volatile是什么?
volatile是java中的關鍵字,也是java虛擬機提供的輕量級的同步機制(乞丐版的synchronize)。
volatile的三大特性
1.可見性
2.不保證原子性
3.禁止指令重排序
為什么說volatile是輕量級的同步機制?
因為大多數多線程開發都需要遵守JMM的三大特性:
1.可見性
2.原子性
3.有序性
而volatile只保證可見性和禁止指令重排序(有序性)所以說是輕量級的同步機制。
可見性
由於JVM運行程序的實體是線程,而每個線程創建是JVM都會為其創建一個工作內存(有些地方稱為棧空間),工作內存是每個線程私有數據區域,而java內存模型中規定所有變量都存在主內存,主內存是共享區域,所有線程都可以訪問,但線程對變量的操作(讀取賦值等)必須在工作內存中進行,首先要將變量從主內存拷貝到自己的工作內存空間,如何對變量進行操作,操作完成后再將變量寫回主內存,不能直接操作主內存中的變量,各個線程中的工作內存中存儲着主內存中的變量副本拷貝,因此不同線程間無法訪問對方的工作內存,線程間的通信(傳值)必須通過主內存完成。
例:
現在這里有三個線程:線程1,線程2,線程3想要去改主內存的age它不是直接去主內存修改,而是先把主內存的age拷貝到直接的工作內存,然后在自己的工作內存進行修改,再寫會主內存,比如這里線程1把25改成37再寫回去,但是線程2,和線程3並不知道主內存的值從25改成37,所以我們必須要有一種機制,只要有一個線程修改了自己工作內存的值並寫回給主內存以后,要及時通知其他線程,這樣及時通知這種情況JMM內存模型中第一個重要特性:可見性。
代碼驗證:
沒有用volatile修飾
class Demo {
int num = 0;
public void add()
{
this.num = 60;
}
}
/**
* 驗證valatile的可見性
*/
public class Test {
public static void main(String[] args) {
Demo d=new Demo();// 線程操作資源類
new Thread(() -> {
System.out.println(Thread.currentThread().getName()+"\t come in:"+d.num);
try {
TimeUnit.SECONDS.sleep(3);
}catch (Exception e) {
e.printStackTrace();
}
d.add();
System.out.println(Thread.currentThread().getName()+"\t update num value:"+d.num);
},"線程1").start();
while(d.num == 0)
{
// main線程一直等待循環,直到num值不再等於0.
}
System.out.println(Thread.currentThread().getName()+"\t over:"+d.num);
}
}
運行結果:
我們可以看到線程1把num的值已經改成60了,也就是說線程1已經把60寫回主內存了,但是程序沒有停止,那就是說main線程不知道值已經改為60了,但沒有人通知main線程,main線程還在那做一個安靜的美男子。
加上volatile:
class Demo {
volatile int num = 0;
public void add()
{
this.num = 60;
}
}
/**
* 驗證valatile的可見性
*/
public class Test {
public static void main(String[] args) {
Demo d=new Demo();// 線程操作資源類
new Thread(() -> {
System.out.println(Thread.currentThread().getName()+"\t come in:"+d.num);
try {
TimeUnit.SECONDS.sleep(3);
}catch (Exception e) {
e.printStackTrace();
}
d.add();
System.out.println(Thread.currentThread().getName()+"\t update num value:"+d.num);
},"線程1").start();
while(d.num == 0)
{
// main線程一直等待循環,直到num值不再等於0.
}
System.out.println(Thread.currentThread().getName()+"\t over:"+d.num);
}
}
運行結果:
只要有一個線程修改了主內存的值,馬上寫回去的時候,只要加了volatile關鍵字的變量,其他線程迅速受到通知,拿到主內存最新的值。
不保證原子性
原子性值的是什么?
不可分割,完整性,也就是說某個線程正在做某個具體業務時,中間不可以被加塞或被分割,需要整體完整,要么同時成功,要么同時失敗。
代碼驗證:
class Demo {
volatile int num = 0;
// 此時num前面是加了volatile關鍵字修飾的,volatile不保證原子性
public void add()
{
num++;
}
}
/**
* 驗證valatile不保證原子性
*/
public class Test {
public static void main(String[] args) {
Demo d=new Demo();// 線程操作資源類
for(int i = 0;i <20; i++){
new Thread(() -> {
for (int j = 0; j <1000 ; j++) {
d.add();
}
},String.valueOf(i)).start();
}
while(Thread.activeCount() > 2)
{
Thread.yield();
}
System.out.println(Thread.currentThread().getName()+"\t num value:"+d.num);
}
}
運行結果:
這里有20個線程,每個線程執行1000次,按理來說最終運行結果應該是20000才對,但是每次的運行結果都不一樣。
volatile不保證原子性的解釋
假設這里有三個線程操作主內存數據,但不能直接操作主內存,要把它拷貝回自己的工作空間,所謂的變量的副本拷貝,這里每個線程工作內存都為0,這時候都在自己工作內存進行累加線程1工作內存值為1,線程2工作內存也為1,線程3工作內存也是1,只要線程的工作內存操作完成后會寫會主內存。假設說線程1和線程2同時讀到了這時候它們的工作內存值都為0,正常情況下線程1先寫回去,主內存值為1了,線程2再拿到這個值進行累加再寫回來,這個時候主內存值應該為2了,但是由於多線程競爭和調度的關系,某一個時間段線程1和線程2的值都為0,各自在自己的工作內存進行累加,准備把這個1寫回去,將會出現在某一時間段,線程1將要把1寫回去的時候突然被掛起了,線程2再刷的一下把它工作內存的1寫回去,然后線程2再通知其他線程,其他線程還沒有反應過來的情況下,被掛起的線程1馬上也寫回去,本來線程1和線程2應該加兩次,但是會出現線程1的值1把線程2寫回去的值1覆蓋了,出現了丟失數據的一次。這就是數值為什么達不到20000的原因,出現丟失寫值的情況。
禁止指令重排
計算機在執行程序時,為了提高性能,編譯器和處理器常常會對指令做重排,一般分以下3中:
源代碼——》編譯器優化的重排——》指令並行的重排——》內存系統的重排——》最終執行的指令
在單線程環境里確保程序最終執行結果和代碼順序執行的結果一致。
處理器在進行重排序時要考慮指令之間的數據依賴性。
多線程環境中線程交替執行,由於編譯器優化重排的存在,兩個線程中使用的變量能否保證一致性是無法確定的,結果無法預測。
代碼:
int a = 0;
boolean flag = false;
public void method01()
{
a = 1;
flag = true;
}
// 多線程環境中線程交替執行,由於編譯器優化重排的存在,兩個線程中使用的變量能否保證一致性是無法確定的,結果無法預測。
public void method02()
{
if(flag)
{
a = a + 5;
System.out.println("return value:"+a);
}
}
volatile禁止指令重排總結
volatile實現禁止指令重排優化,從而避免多線程環境下程序出現亂序執行的現象。
先了解一個概念,內存屏障(Memory Barrier)又稱內存柵欄,是一個CPU指令,它的作用有兩個:
1.保證特定操作的執行順序;
2.保證某些變量的內存可見性(利用改特性實現volatile的內存可見性)。
由於編譯器和處理器都能執行指令重排優化。如果在指令間插入一條Memory Barrier則會告訴編譯器和CPU,不管什么指令都不能和這條Memory Barrier指令重排序,也就是說通過插入內存屏障禁止在內存屏障前后的指令執行重排序優化。內存屏障另外一個作用是強制刷出各種CPU的緩存數據,因此任何CPU上的線程都能讀取到這些數據的最新版本。