volatile 是 Java 中的關鍵字,是一個變量修飾符,被用來修飾會被不一樣線程訪問和修改的變量。java
可見性是指多個線程訪問同一個變量時,其中一個線程修改了該變量的值,其它線程可以當即看到修改的值。緩存
在 Java 內存模型中,全部的變量都存儲在主存中,同時每一個線程都擁有本身的工做線程,用於提升訪問速度。線程會從主存中拷貝變量值到本身的工做內存中,而後在本身的工做線程中操做變量,而不是直接操做主存中的變量,因爲每一個線程在本身的內存中都有一個變量的拷貝,就會形成變量值不一致的問題。安全
以下面的代碼所示:多線程
測試類:併發
class VolatileTestObj {
private String value = null;
private boolean hasNewValue = false;
public void put(String value) {
while (hasNewValue) {
// 等待,防止重複賦值
}
this.value = value;
hasNewValue = true;
}
public String get() {
while (!hasNewValue) {
// 等待,防止獲取到舊值
}
String value = this.value;
hasNewValue = false;
return value;
}
}複製代碼
測試代碼:app
public class VolatileTest {
public static void main(String... args) {
VolatileTestObj obj = new VolatileTestObj();
new Thread(() -> {
while (true) {
obj.put("time:" + System.currentTimeMillis());
}
}).start();
new Thread(() -> {
while (true) {
System.out.println(obj.get());
}
}).start();
}
}複製代碼
以上測試代碼中,一個線程進行賦值操做,另外一個線程取值,運行該測試代碼能夠發現,很容易阻塞在循環等待中。jvm
這是由於寫線程寫入一個新值,同時將 hasNewValue 置爲 true,可是隻更新了寫線程本身工做線程的緩存值,沒有更新主存中的值。而讀線程在獲取新值是,其工做線程中的 hasNewValue 爲 false,會陷入到循環等待中,即便寫線程寫了新值,讀線程也沒法獲取。由於讀線程沒有獲取都新值,寫線程的 hasNewValue 沒有被置回 false,因此寫線程也會陷入到循環等待中。所以產生了死鎖。測試
使用 volatile 關鍵字能夠解決這個問題,使用 volatile 修飾的變量確保了線程不會將該變量拷貝到本身的工做線程中,全部線程對該變量的操做都是在主存中進行的,因此 volatile 修飾的變量對全部線程可見。優化
使用 volatile 修飾 hasNewValue,這樣在寫線程和讀線程中都是在主存中操做 hasNewValue 的值,就不會產生死鎖。this
volatile 只保證單次讀/寫操做的原子性,對於多步操做,volatile 不能保證原子性,以下代碼所示:
測試類:
class VolatileCounter {
private volatile int count = 0;
public void inc() {
count++;
}
public void dec() {
count--;
}
public int get() {
return count;
}
}複製代碼
測試代碼:
public class VolatileTest {
public static void main(String... args) {
while (true) {
VolatileCounter counter = new VolatileCounter();
Thread thread1 = new Thread(() -> {
for (int i = 0; i < 50; i++) {
counter.inc();
}
});
Thread thread2 = new Thread(() -> {
for (int i = 0; i < 50; i++) {
counter.dec();
}
});
thread1.start();
thread2.start();
try {
thread1.join();
thread2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("counter = " + counter.get());
}
}
}複製代碼
運行結果:
...
counter = 0
counter = 0
counter = 0
counter = 0
counter = -21
counter = 0
counter = 0
counter = 0
counter = 0
...複製代碼
從運行結果能夠看出,絕大部分狀況下輸出結果爲 counter = 0,但也有部分其它結果。由此可知,對於 count++; 和 count--; 這兩個操做並不具備原子性。
這是由於 count++ 是一個複合操做,包括三個部分:
讀取 count 的值;
對 count 加 1;
將 count 的值寫回內存;
volatile 對於這三步操做是沒法保證原子性的,因此會出現上述運行結果。
因此,vloatile 並不能解決全部同步的問題
在 Java 內存模型中,容許編譯器和處理器對指令進行重排序,重排序過程不會影響到單線程程序的執行,可是會影響到多線程併發執行的正確性。
volatile 關鍵字能夠禁止指令從新排序,能夠保證必定的有序性。
volatile 修飾的變量的有序性有兩層含義:
全部在 volatile 修飾的變量寫操做以前的寫操做,將會對隨後該 volatile 修飾的變量讀操做以後的語句可見。
禁止 JVM 重排序:volatile 修飾的變量的讀寫指令不能和其先後的任何指令重排序,其先後的指令可能會被重排序。
happen-before 關係是用來判斷是否存在數據競爭、線程是否安全的主要依據,也是指令重排序的依據,保證了多線程下的可見性。
volatile 修飾的變量在讀寫時會創建 happen-before 關係。
以下面的測試類:
class VolatileOrder {
int i = 0;
volatile boolean flag = false;
public void write() {
i = 1; // 步驟 1
flag = true; // 步驟 2
}
public String get() {
if (flag) { // 步驟 3
System.out.println("i = " + i); // 步驟 4
}
}
}複製代碼
上面的代碼依據 happen-before 原則(關於 happen-before 原則可自行搜索)會創建以下的關係:
根據 happen-before 單線程順序原則會有:步驟 1 happen-before 步驟 二、步驟 3 happen-before 步驟 4;
根據 happen-before 的 volatile 原則會有:步驟 2 happen-before 步驟 3;
根據 happen-before 的傳遞性原則會有:步驟 1 happen-before 步驟 4;
因此 步驟 1 對於 步驟 4 是可見的,即變量 i 在多個線程中具備可見性。
這也解釋了 volatile 有序性的第一層含義:全部在 volatile 修飾的變量寫操做以前的寫操做,將會對隨後該 volatile 修飾的變量讀操做以後的語句可見。
利用這個特性能夠優化變量在線程間的可見性,不須要對每一個變量都用 volatile 修飾,只須要用 volatile 修飾一部分變量便可保證其它變量在多線程間也具備可見性。
對於上述代碼,若是變量 flag 沒有使用 volatile 修飾,那麼步驟 1 和步驟 2 就有可能被 JVM 重排序,就沒法獲得上述的 happen-before 關係,因此 volatile 修飾的變量禁止 JVM 重排序。
以下代碼所示:
class VolatileOrder {
int a, b, c;
volatile int d;
void write() {
a = 1;
b = 2;
c = 3;
d = 4;
}
void read() {
int D = d;
int A = a;
int B = b;
int C = c;
}
}複製代碼
在 write() 方法中:
a = 1;
b = 2;
c = 3;複製代碼
JVM 可能會重排序這三個指令,可是這三個指令必定是排在 d = 4; 這個指令以前。
一樣的,在 read() 方法中:
int A = a;
int B = b;
int C = c;複製代碼
JVM 可能會重排序這三個指令,可是這三個指令必定是排在 int D = d; 這個指令以後。