/**
* 演示可見性帶來的問題
*/
public class VisibilityProblem {
int a = 10;
int b = 20;
private void change() {
a = 30;
b = a;
}
private void print() {
System.out.println("b=" + b + ";a=" + a);
}
public static void main(String[] args) {
while (true) {
VisibilityProblem problem = new VisibilityProblem();
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
problem.change();
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
problem.print();
}
}).start();
}
}
}
下面我們運行這段代碼并分析一下可能出現(xiàn)的情況萨咳。
假設(shè)第 1 個線程,也就是執(zhí)行 change 的線程先運行疫稿,并且運行完畢了培他,然后第 2 個線程開始運行,那么第 2 個線程自然會打印出 b = 30;a = 30 的結(jié)果遗座。
線程1先 start舀凛,并不代表它真的先執(zhí)行,于是第 2 個線程先打印途蒋,然后第 1 個線程再去進行 change猛遍,那么此時打印出來的就是 a 和 b 的初始值,打印結(jié)果為 b = 20;a = 10。
它們幾乎同時運行懊烤,所以會出現(xiàn)交叉的情況梯醒。比如說當(dāng)?shù)?1 個線程的 change 執(zhí)行到一半,已經(jīng)把 a 的值改為 30 了腌紧,而 b 的值還未來得及修改茸习,此時第 2 個線程就開始打印了,所以此時打印出來的 b 還是原始值 20壁肋,而 a 已經(jīng)變?yōu)榱?30号胚, 即打印結(jié)果為 b = 20;a = 30。
但是有一種情況不是特別容易理解浸遗,那就是打印結(jié)果為 b = 30;a = 10猫胁,我們來想一下,為什么會發(fā)生這種情況跛锌?
如果出現(xiàn)了打印結(jié)果為 b = 30;a = 10 這種情況杜漠,就意味著發(fā)生了可見性問題:a 的值已經(jīng)被第 1 個線程修改了,但是其他線程卻看不到察净,由于 a 的最新值卻沒能及時同步過來,所以才會打印出 a 的舊值盼樟。發(fā)生上述情況的幾率不高氢卡。
那么我們應(yīng)該如何避免可見性問題呢?
我們?nèi)绻o a 和 b 加了 volatile 關(guān)鍵字后晨缴,無論運行多長時間译秦,也不會出現(xiàn) b = 30;a = 10 的情況,這是因為 volatile 保證了只要 a 和 b 的值發(fā)生了變化击碗,那么讀取的線程一定能感知到筑悴。
除了 volatile 關(guān)鍵字可以讓變量保證可見性外,synchronized稍途、Lock阁吝、并發(fā)集合等一系列工具都可以在一定程度上保證可見性。
synchronized 不僅保證了原子性械拍,還保證了可見性突勇。
synchronized 不僅保證了臨界區(qū)內(nèi)最多同時只有一個線程執(zhí)行操作,同時還保證了在前一個線程釋放鎖之后乔询,之前所做的所有修改碍舍,都能被獲得同一個鎖的下一個線程所看到擂找,也就是能讀取到最新的值。因為如果其他線程看不到之前所做的修改定躏,依然也會發(fā)生線程安全問題。