綜合上篇文章的代碼一的說明
如果我們使用voliate關鍵字修飾flag會出現什麼效果
public static volatile boolean flag = false;
public static void main(String[] args) throws InterruptedException {
new Thread(new Runnable() {
@Override
public void run() {
while (!flag){
}
System.out.println("線程1結束");
}
}).start();
Thread.sleep(5000);
new Thread(new Runnable() {
@Override
public void run() {
flag = true;
System.out.println("線程2結束");
}
}).start();
}
看下執行結果:
線程2結束
線程1結束
Process finished with exit code 0
明顯可以看到當添加關鍵字修飾時,當線程二改變flag的值以後,線程一明顯是感受到了主內存的變化並且從主內存重新獲取flag的最新值,那voliate我們僅僅加個關鍵字,jvm到底做了哪些東西?爲明確這點,先來看一個概念
在併發編程中的三個問題:原子性,可見性,有序性。
1.原子性
原子性:即一個操作或者多個操作 要麼全部執行並且執行的過程不會被任何因素打斷,要麼就都不執行。
(1)原子的意思代表着——“不可分”;
(2)在整個操作過程中不會被線程調度器中斷的操作,都可認爲是原子性。原子性是拒絕多線程交叉操作的,不論是多核還是單核,具有原子性的量,同一時刻只能有一個線程來對它進行操作。例如 a=1是原子性操作,但是a++和a +=1就不是原子性操作
2.可見性
可見性是指當多個線程訪問同一個變量時,一個線程修改了這個變量的值,其他線程能夠立即看得到修改的值。
變量經過volatile修飾後,對此變量進行寫操作時,彙編指令中會有一個LOCK前綴指令,加了這個指令後,會引發兩件事情:
- 發生修改後強制將當前處理器緩存行的數據寫回到系統內存。
- 這個寫回內存的操作會使得在其他處理器緩存了該內存地址無效,重新從內存中讀取。
3.有序性
有序性:即程序執行的順序按照代碼的先後順序執行。
在Java內存模型中,允許編譯器和處理器對指令進行重排序,但是重排序過程不會影響到單線程程序的執行,卻會影響到多線程併發執行的正確性。
通過上述的例子可以很明確的一點就是:voliate保證了可見性。
那voliate是不是保證了原子性呢?
結合第二個代碼來看下
public static volatile int flag = 0;
public static void main(String[] args) throws InterruptedException {
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 50; i++) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
flag++;
System.out.println("線程一"+flag);
}
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 50; i++) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
flag++;
System.out.println("線程二"+flag);
}
}
}).start();
}
我們給flag加上voliate關鍵字,如果voliate保證一致性,那程序的執行結果肯定是100。結果顯示flag最後的累加值並不等於100,所以
voliate關鍵字是無法保證一致性的
那voliate是不是保證了有序性呢?
static int a, b;
static int x, y;
public static void main(String[] args) {
int i = 0;
for (; ; ) {//死循環
a = 0;
b = 0;
x = 0;
y = 0;
i++;
Thread aThread1 = new Thread(() -> {
a = 1;
x = b;
});
Thread bThread1 = new Thread(() -> {
b = 1;
y = a;
});
aThread1.start();
bThread1.start();
try {
aThread1.join();
bThread1.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("第" + i + "次循環輸出:" + "x=" + x + ";y=" + y);
if (x == 0 && y == 0) {
break;
}
}
}
運行結果後發現有可能會出現x = 0 y = 0的情況,
x = 0 y = 0 執行順序: x = b; a = 1; y = a; b = 1;
x = 0 y = 1 執行順序: x = b; a = 1; b = 1; y = a;
x = 1 y = 0 執行順序: a = 1; x = b; y = a; b = 1;
x = 1 y = 1 執行順序: a = 1; b = 1; x = b; y = a;
那就說明java代碼發生了重排序,先執行x = b = 0,y = a = 0 後執行a = 1,b = 1
當給變量加上voliate關鍵字
static volatile int a, b;
static int x, y;
執行結果只有兩種.兩種結果的差異完全是在兩個線程誰先執行的問題上,而不是重排序的問題上。
x = 1 y = 0 執行順序: b = 1; y = a;
a = 1; x = b;
x = 0 y = 1 執行順序: a = 1;x = b;
b = 1; y = a;
總結下就是:
x = 1; //語句1
y = 2; //語句2
flag = true; //語句3
x = 3; //語句4
y = 4; //語句5
當flag被voliate修飾後,無論如何進行重排序,語句3永遠在第三次執行,1,2永遠在3前面執行。4,5永遠在3後面執行
在前面提到volatile關鍵字能禁止指令重排序,所以volatile能在一定程度上保證有序性。
volatile關鍵字禁止指令重排序有兩層意思:
1)當程序執行到volatile變量的讀操作或者寫操作時,在其前面的操作的更改肯定全部已經進行,且結果已經對後面的操作可見;在其後面的操作肯定還沒有進行;
2)在進行指令優化時,不能將在對volatile變量訪問的語句放在其後面執行,也不能把volatile變量後面的語句放到其前面執行。
所以voliate關鍵字保證了變量的可見性,以及一定程度上的有序性,但無法保證原子性
接下看看voliate關鍵字到底做了什麼?