我就站在你面前,你卻視而不見!
在上一篇文章一男子給對象轉賬5000元,居然又退還了!中,我們學習了並發三大特性之一的原子性,並對原子性問題進行分析。
這篇文章我們就一起來了解下可見性:
可見性
首先看下可見性的概念:
可見性就是指某一個線程修改了共享變量的值時,其他線程能夠立即得知這個修改。
什麼?難道變量被修改了,線程不應該馬上讀取到的嗎?為什麼和我認知的不一樣呢?
好的,那麼接下來讓我們帶着問題,一起來搞懂可見性問題。
可見性問題
可見性問題的元兇就是 CPU 緩存,都怪 CPU 為程序性能優化做的努力,搞出這麼多幺蛾子。
關於 CPU 緩存可以閱讀:原來 CPU 為程序性能優化做了這麼多
首先在單核 CPU 上,是不存在可見性問題的,因為所有的線程都在一個 CPU 上執行,所有的線程都是操作同一 CPU 緩存,某一個線程修改了共享變量的值,另外的線程也可以馬上讀取到,因此是可見的。
如上圖所示,Thread-0
和 Thread-1
都是在一個 CPU 緩存上進行操作,所以 Thread-0
修改了變量 flag
的值後,Thread-1
再去訪問變量 flag
,得到的一定是最新的 flag
值。
然而在多核 CPU 上,由於每個 CPU 都有自己的緩存,當多個不同線程運行在不同的 CPU 上時,這些線程操作的 CPU 緩存也是不同的,因此某一個線程對共享變量進行修改時,另外的線程讀取到的不一定是最新值,也就不具有可見性了。
如上圖所示,Thread-0
是在 CPU-0
上的緩存進行操作,Thread-1
是在 CPU-1
上的緩存進行操作,所以 Thread-0
修改了變量 flag
的值後,Thread-1
再去訪問變量 flag
,得到的不一定是最新的 flag
值,因此 Thread-0
對共享變量 flag
的修改對 Thread-1
是不可見的。
下面用一個例子來看下可見性問題,創建一個 VisibilityTest
類,實現 Runnable
接口,在 run()
方法中判斷 flag
是否為 true
,若為 true
則進行打印操作,主方法中啟動一個線程 thread
,主線程等待 0.5 秒後,將 flag
的值設為 true
。
public class VisibilityDemo {
private static class VisibilityTest implements Runnable {
private boolean flag = false;
@Override
public void run() {
while (true) {
if (flag) {
System.out.println(Thread.currentThread().getName() + ":" + flag);
}
}
}
}
public static void main(String[] args) throws InterruptedException {
VisibilityTest visibilityTest = new VisibilityTest();
Thread thread = new Thread(visibilityTest);
thread.start();
// 等待線程啟動
Thread.sleep(500);
// 更新 flag 為 true
visibilityTest.flag = true;
System.out.println(Thread.currentThread().getName() + ":" + visibilityTest.flag);
}
}
發現輸出的結果為:main:true
,和我們想像的不太一樣,按道理 thread 應該會持續打印出 Thread-0:true
的,但是並非如此,這也就驗證了我們剛才講的可見性問題。
那麼如何解決可見性問題呢?
可以採用同步的方式去解決或者使用 volatile 關鍵字也可以保證可見性。
關於 volatile 相關原理可以閱讀:你真的了解 volatile 關鍵字嗎?
總結
本文學習了線程安全三大特性之中的可見性,另外 CPU 緩存在提高程序性能的同時也帶來了可見性問題,只有我們理解了可見性的原理,才更容易去診斷並發編程中的 BUG。
參考
《Java並發編程實戰》
《深入理解Java虛擬機:JVM高級特性與最佳實踐》
《實戰Java高並發程序設計》
《Java多線程編程核心技術》
Java並發編程實戰