當聲明共享變量爲volatile後,對這個變量的讀/寫將會很特別。那麼它到底起着怎樣的作用呢?
一、可見性
可見性指的是線程之間的可見性,一個線程修改的狀態對另一個線程是可見的。也就是一個線程對一個volatile變量的讀,總是能看到(任意線程)對這個volatile變量最後的寫入。
其實之所以要保證可見性,主要與Java的內存模型有關。
內存模型
每個線程在執行的時候,會從主內存中拷貝一份到自己的本地內存,線程操作的時候是操作自己本地內存的變量,這樣每個線程都操作自己本地內存的變量,就可能導致這個共享變量的數據不一致,這就體現了volatile的作用了。volatile可以使得一個線程操作共享變量的時候,能讀到這個共享變量最新的變化,在這個變化上操作。
示意圖
線程對共享變量的所有操作都必須在⾃⼰的本地內存中進⾏,不能直接從主內存中讀取。JMM通過控制主內存與每個線程的本地內存之間的交互,來提供內存可⻅性保證。
舉個栗子
下邊這個例子只是爲了理解volatile的作用
對任意單個volatile變量的讀/寫具有原子性,但類似於volatile++這種複合操作不具有原子性
線程A和線程B操作同一個變量a,假設線程A先執行,線程B後執行
在這裏,因爲有了volatile,所以線程A執行後的結果a=1對B是可見的,B執行的時候是在a=1的基礎上進行的,然後B執行完是2;
如果沒有volatile,線程A執行完a=1,但線程B可能不知道,B執行的時候以爲a=0,B執行完就是1了
public class VolatileDemo {
private static volatile int a = 0;
static class ThreadA implements Runnable {
@Override
public void run() {
a++;
System.out.println(a);
}
}
static class ThreadB implements Runnable {
@Override
public void run() {
a++;
System.out.println(a);
}
}
}
執行過程
最開始共享變量flag=0。也就是主內存中flag=0,線程A執行的時候,是先從主內存中拷貝一份到本地內存A,flag=0,然後線程A操作flag=1;A操作的flag=1更新到主內存中,接着B開始執行,主內存中flag=1,更新到本地內存B中,flag=1,接着B操作flag=2。
二、禁止重排序
在Java內存模型中,對一些語句進行重排序,可提升性能,但是有的地方一旦重排序了,得到的結果就不是我們想要的結果了。也就是我們寫代碼的時候看到的是語句1在語句2前,但在JMM中可能重排序後就是語句2在語句1前邊了,但我們可能就想讓語句1在語句2前執行,這就用到了volatile了,它是怎麼實現不重排序呢,JVM通過內存屏障來實現限制處理器的重排序
重排序
計算機在執⾏程序時,爲了提⾼性能,編譯器和處理器常常會對指令做重排。
原理是指令1還沒有執⾏完,就可以開始執⾏指令2,⽽不⽤等到指令1執⾏結束之後再執⾏指令2,這樣就⼤⼤提⾼了效率。
指令重排可以保證串⾏語義⼀致,但是沒有義務保證多線程間的語義也⼀致。所以在多線程下,指令重排序可能會導致⼀些問題。
內存屏障
分類
讀屏障(Load Barrier)和寫屏障(Store Barrier)
作⽤
(1) 阻⽌屏障兩側的指令重排序;
(2)強制把寫緩衝區/⾼速緩存中的髒數據等寫回主內存,或者讓緩存中相應的數據失效
基於保守策略的JMM內存屏障插⼊策略
在每個volatile寫操作前插⼊⼀個StoreStore屏障;
在每個volatile寫操作後插⼊⼀個StoreLoad屏障;
在每個volatile讀操作後插⼊⼀個LoadLoad屏障;
在每個volatile讀操作後再插⼊⼀個LoadStore屏障。
實際過程中的JMM內存屏障插⼊
class VolatileBarrier{
int a;
volatile int b=1;
volatile int c=2;
void readWrite(){
// 第一個volatile讀
int i=b;
// 第二個volatile讀
int j=c;
// 普通寫
a=i+j;
// 第一個volatile寫
b=i+1;
// 第二個volatile寫
c=j+1;
}
}
在保證內存可⻅性這⼀點上,volatile有着與鎖相同的內存語義,所以可以作爲⼀個“輕量級”的鎖來使⽤。但由於volatile僅僅保證對單個volatile變量的讀/寫具有原⼦性,⽽鎖可以保證整個臨界區代碼的執⾏具有原⼦性。所以在功能上,鎖⽐volatile更強⼤;在性能上,volatile更有優勢。