簡介
volatile 是一種同步機制,比 synchronized 或 Lock 相關類更輕量,因此使用 volatile 并不會發生背景關系切換等開銷很大的行為,
如果一個變數被修飾成 volatile,那么 JVM 就知道了這個變數可能會被并發修改,
因為其開銷小,所以對應的功能也小,volatile 不能像 synchronized 一樣提供原子保護,
實作原理
Java語言規范第3版中對volatile的定義如下:Java編程語言允許線 程訪問共享變數,為了確保共享變數能被準確和一致地更新,執行緒應 該確保通過排他鎖單獨獲得這個變數,Java語言提供了volatile,在某些 情況下比鎖要更加方便,如果一個欄位被宣告成volatile,Java執行緒記憶體模型確保所有執行緒看到這個變數的值是一致的,
volatile 是如何保證可見性的?
首先通過工具獲取 JIT 編譯器生成的匯編指令來查看對volatile進行寫操作時,CPU會 做什么事情,
java 代碼:
instance = new Singleton(); // instance是volatile變數
轉變成匯編代碼,如下:
0x01a3de1d: movb $0×0,0×1104800(%esi);0x01a3de24: lock addl $0×0,(%esp);
有volatile變數修飾的共享變數進行寫操作的時候會多出第二行匯 編代碼,通過查IA-32架構軟體開發者手冊可知,Lock 前綴的指令在多核處理器下會引發了兩件事情 ,
- 將當前處理器快取行的資料寫回到系統記憶體,
- 這個寫回記憶體的操作會使在其他CPU里快取了該記憶體地址的數 據無效,
通過之前學習的 JMM 記憶體模 可以得知,為了提高處理速度,處理器不直接和記憶體進行通信,而是先將系 統記憶體的資料讀到內部快取(L1,L2或其他)后再進行操作,但操作完不知道何時會寫到記憶體,如果對宣告了volatile的變數進行寫操作, JVM就會向處理器發送一條 Lock 前綴的指令,將這個變數所在快取行 的資料寫回到系統記憶體,但是,就算寫回到記憶體,如果其他處理器緩 存的值還是舊的,再執行計算操作就會有問題,所以,在多處理器 下,為了保證各個處理器的快取是一致的,就會實作快取一致性協 議,每個處理器通過嗅探在總線上傳播的資料來檢查自己快取的值是 不是過期了,當處理器發現自己快取行對應的記憶體地址被修改,就會 將當前處理器的快取行設定成無效狀態,當處理器對這個資料進行修 改操作的時候,會重新從系統記憶體中把資料讀到處理器快取里,
volatile 的兩條實作原則
1)Lock前綴指令會引起處理器快取回寫到記憶體;
2)一個處理器的快取回寫到記憶體會導致其他處理器的快取無效 ;
主要特性
可見性
代碼實踐
public class MyData {
// 這里去掉 volatile 后 main 執行緒就發阻塞
volatile int num = 0;
public void addNum() {
num = 60;
}
}
class VolatileDome{
public static void main(String[] args) {
MyData myData = https://www.cnblogs.com/lhnstart/p/new MyData();
new Thread(() -> {
System.out.println(Thread.currentThread().getName());
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
myData.addNum();
System.out.println(Thread.currentThread().getName() + myData.num);
},"AAA").start();
while (myData.num == 0) {
}
System.out.println(Thread.currentThread().getName() + "mission is over");
}
}
不保證原子性
public class MyData {
volatile int num = 0;
public void addNumPlus() {
num++;
}
}
class VolatileDome{
public static void main(String[] args) {
MyData myData = https://www.cnblogs.com/lhnstart/p/new MyData();
for (int i = 0; i < 20; i++) {
new Thread(() -> {
for (int j = 0; j < 1000; j++) {
myData.addNumPlus();
}
}, String.valueOf(i)).start();
}
while (Thread.activeCount() > 2) {
Thread.yield();
}
// 最終列印的結果很大可能不是 20W,
System.out.println(myData.num);
}
}
addNumPlus 位元組碼解讀
0 aload_0
1 dup
2 getfield #2 <com/lhn/demo1/jmm/MyData.num : I>
5 iconst_1
6 iadd
7 putfield #2 <com/lhn/demo1/jmm/MyData.num : I>
10 return
通過位元組碼可以看出 n++ 被拆成了三個指令:
- 執行 getfieid 拿到原始 n;
- 執行 iadd 進行加 1 操作;
- 執行 putfieid 把累加后的值寫回;
因為 volatile 不能保證原子性,所以在執行 num++ 的時候可能會被其它執行緒給中斷操作,導致寫入的值覆寫掉前一個執行緒寫的值,出現丟失寫值的情況,
解決原子性問題
使用 synchronized(重量級操作,不推薦)
public synchronized void addNumPlus() {
num++;
}
使用 atomic (推薦使用)
AtomicInteger atomicInteger = new AtomicInteger();
public void addAtomic() {
atomicInteger.getAndIncrement();
}
禁止指令重排序
計算機在執行程式時,為了提高性能,編譯器和處理器常常會對指令做重排,一般分一下三種:
單執行緒環境下確保程式最終執行結果和代碼順序執行的結果一樣,處理器在進行重排序時必須要考慮指令之間的資料依賴性;
多執行緒環境中執行緒交替執行,由于編譯器優化重排的存在,兩個執行緒中使用的變數能否保證一致性是無法確定的,結果無法預測,
實作原理
volatile 實作禁止指令重排序優化,從而避免多執行緒環境下程式出現亂序執行的現象,
這個首先要了解一個概念,就是記憶體屏障(Memory Barrier) 又稱記憶體柵欄,是一個 CPU 指令,他的作用有兩個:
- 保證特定操作的執行順序,
- 保證某些變數的記憶體可見性(利用該特性實作 volatile 的記憶體可見性)
由于編譯器和處理器都能執行指令重排序優化,如果在指令間插入一條 Memory Barrier 則會告訴編譯器和 CPU,不管什么指令都不能和這條 Memory Barrier 指令重排序,也就是說通過插入記憶體屏障禁止在記憶體屏障前后的指令執行重新排序,
對 volatile 變數進行寫操作時,會在寫操作后加入一條 store 屏障指令,將作業記憶體中的共享變數值重繪回到主記憶體,
對 volatile 變數進行讀操作時,會在寫操作后加入一條 load 屏障指令,從主記憶體中讀取共享變數,
volatile 的應用場景
參考地址:https://www.cnblogs.com/krcys/p/9385360.html
狀態變數
由于boolean的賦值是原子性的,所以volatile布爾變數作為多執行緒停止標志還簡單有效的.
Copyclass Machine{
volatile boolean stopped = false;
void stop(){stopped = true;}
}
物件完整發布
這里要提到單例物件的雙重檢查鎖,物件完整發布也依賴于happens before原則,有興趣可以自己去查閱,這個原則是比較啰嗦,可以簡單理解為我滿足happens before,那么我之前的代碼按順序執行.
Copypublic class Singleton {
//單例物件
private static Singleton instance = null;
//私有化構造器,避免外部通過構造器構造物件
private Singleton(){}
//這是靜態工廠方法,用來產生物件
public static Singleton getInstance(){
if(instance ==null){
//同步鎖防止多次new物件
synchronized (Singleton.class){
//鎖內非空判斷也是為了防止創建多個物件
if(instance == null){
instance = new Singleton();
}
}
}
return instance;
}
}
這是一個會產生bug的雙重檢查鎖代碼,instance = new Singleton()并不是一步完成的,他被分為這幾步:
Copy1.分配物件空間;
2.初始化物件;
3.設定instance指向剛剛分配的地址,
下面圖中,執行緒A紅色先獲得鎖,B黃色后進入.
這種情況會出現bug,但是由于volatile滿足happens before原則,所以會等物件實體化之后再對地址賦值,我們需要將private static Singleton instance = null;改成private static volatile Singleton instance = null;即可.
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/442770.html
標籤:Java
上一篇:Java案例——學生管理系統
下一篇:Spring Ioc和DI注解
