前言:上一篇文章主要介紹了volatile的概念以及使用場景,但是沒有具體展示代碼示例,所謂不見真相就是不信嘛,或者看了概念也不是很理解,看下代碼可能就讓你豁然開朗了。
例子1、一次性標誌,先看下沒有使用volatile會出現什麼問題
發現即使關閉任務了,但是線程1還是執行的。這是由於不採取任務措施的情況下,共享變量的修改對其他線程未必是可見的造成的,那麼加上volatile呢?
可以看得出,任務結束了,使用一個布爾變量來標記狀態,用於指示發生了一件重要的一次性事件。只有一次性的狀態轉換。上面的代碼中,狀態標誌位只是從false轉換為true,並沒有繼續進行從true到false的轉換等。這種轉換的一次性杜絕了有序性問題的產生。
例子2、單例模式下的雙重檢查
這個真是太難模擬出來了,執行了好多次,都沒有復現,大家記住加volatile吧。
既然沒有復現出來,那就口頭說下出現問題的原因吧。主要在於instance = new TestThread()這句,這並非是一個原子操作,事實上在 JVM 中這句話大概做了下面 3 件事情:
1.給 instance 分配內存;
2.調用 TestThread的構造函數來初始化成員變量;
3.將instance對象指向分配的內存空間(執行完這步 instance 就為非 null 了)。
但是在 JVM 的即時編譯器中存在指令重排序的優化。也就是說上面的第二步和第三步的順序是不能保證的,最終的執行順序可能是 1-2-3 也可能是 1-3-2。如果是後者,則在 3 執行完畢、2 未執行之前,被線程二搶佔了,這時 instance 已經是非 null 了(但卻沒有初始化),所以線程二會直接返回instance,然後使用,然後順理成章地報錯。
例子3、獨立觀察模式(比如查看最新的值,比如溫度、最後登錄用戶點(不保證原子性))
例子4、開銷較低的「讀-寫鎖」策略
如果讀操作遠遠超過寫操作,您可以結合使用內部鎖和 volatile 變量來減少公共代碼路徑的開銷。
如下顯示的線程安全的計數器,使用 synchronized 確保增量操作是原子的,並使用 volatile 保證當前結果的可見性。如果更新不頻繁的話,該方法可實現更好的性能,因為讀路徑的開銷僅僅涉及 volatile 讀操作,這通常要優於一個無競爭的鎖獲取的開銷。
例子5、volatile bean 模式
volatile bean 模式的基本原理是:很多框架為易變數據的持有者(例如 HttpSession)提供了容器,但是放入這些容器中的對象必須是線程安全的。
在 volatile bean 模式中,JavaBean 的所有數據成員都是 volatile 類型的,並且 getter 和 setter 方法必須非常普通——即不包含約束。(這種模式小編在實際中沒用過,不多說,用過的人可以給我講講哈)。
總結:與鎖相比,volatile 變量是一種非常簡單但同時又非常脆弱的同步機制,它在某些情況下將提供優於鎖的性能和伸縮性。如果嚴格遵循 volatile 的使用條件 —— 即變量真正獨立於其他變量和自己以前的值 —— 在某些情況下可以使用 volatile 代替 synchronized 來簡化代碼。然而,使用 volatile 的代碼往往比使用鎖的代碼更加容易出錯。遵循這些模式(注意使用時不要超過各自的限制)可以幫助您安全地實現大多數用例,使用 volatile 變量獲得更佳性能。