volatile
<mark>volatile是Java虛擬機提供的輕量級的同步機制,保證可見性袖订,不保證原子性痒玩,禁止指令重排(保證可序性)</mark>
驗證可見性
public class VolatileDemo {
public static void main(String[] args) {
//volatile 可以保證可見性须误,及時通知其他線程至扰,主物理內(nèi)存的值已經(jīng)被修改
MyData myData = new MyData();
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + "\t come in");
//暫停
try {
//確保后面的main線程啟動,進入循環(huán)
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
myData.addT060();
System.out.println(Thread.currentThread().getName() + "\t update number value" + myData.number);
}, "AAA").start();
//第二個線程就是我們的main線程
while (myData.number==0){
//直到number不為0才退出循環(huán)
}
System.out.println(Thread.currentThread().getName()+"\t mission is over");
}
}
class MyData {
int number = 0;
public void addT060() {
this.number = 60;
}
}
首先創(chuàng)建MyData類茬暇,定義一個int型的number變量值為0首昔,定義一個addT060()方法,將number值變成60糙俗。在從VolatileDemo中的主方法創(chuàng)建線程A勒奇,線程A被執(zhí)行調用后先暫停3秒,以取保此時的主線程已經(jīng)開始執(zhí)行while循環(huán)巧骚。3秒后A線程調用addT060()方法赊颠,將number值修改成60,A線程執(zhí)行結束劈彪。但是main線程中的number取依舊還是0竣蹦,而導致無法退出while循環(huán)!
運行查看結果~
通過畫圖可以清晰的看到main線程為啥會nmber值為60后沧奴,還處于while循環(huán)而導致的死循環(huán)痘括。這都是當前變量沒有可見性所導致的結果。
下面驗證volatile保證可見性,只需要在number變量中添加volatile
關鍵字即可纲菌。
再次運行程序查看結果挠日。
線程A修改number的值后,就值寫入主內(nèi)存翰舌,main線程從主內(nèi)存中獲取最新的number值拷貝到工作內(nèi)存中嚣潜,此時nuber值不等于0main線程退出循環(huán),程序結束椅贱。
驗證原子性
原子性就是不可分割懂算,具有完整性,也即某個線程正在做某個具體業(yè)務時庇麦,中間不可以被加塞或者被分割计技。需要整體完整要么同時成功,要么同時失敗女器。
在上文中的MyData
中添加addPlusPlus
方法酸役。創(chuàng)建20個線程每個循環(huán)調用1000次addPlusPlus方法住诸。
public class VolatileDemo {
public static void main(String[] args) {
MyData myData = new MyData();
for (int i = 1; i <= 20; i++) {
new Thread(() -> {
for (int j = 1; j <= 1000; j++) {
myData.addPlusPlus();
}
}, String.valueOf(i)).start();
}
//需要等待前面20個線程全部執(zhí)行完驾胆,在用main線程取得最終的結果值是多少
while (Thread.activeCount() > 2) {
//main 線程 GC線程
Thread.yield();
}
System.out.println(Thread.currentThread().getName() + "\t int type final number value:" + myData.number);
}
}
class MyData {
volatile int number = 0;
public void addT060() {
this.number = 60;
}
//number加了volatile關鍵字
public void addPlusPlus() {
number++;
}
}
運行查看結果~
number值大多不等于20000(存在等于20000情況),出現(xiàn)了數(shù)值丟失寫值的情況贱呐。所以volatite并不保證原子性丧诺!
例如現(xiàn)如今有兩個線程A和B,此時的number值為0奄薇。首先線程A被CPU調度執(zhí)行驳阎,被number值加1變成了1,正準備將工作內(nèi)存中的值同步更新到主內(nèi)存時馁蒂。CPU調度執(zhí)行了線程B呵晚,線程A被掛起,此時主內(nèi)存的number值還是為0沫屡,線程B將number值加一變成了1饵隙,并且成功的將number值同步更新到了主內(nèi)存中,主內(nèi)存中的值也變成了1沮脖。然后線程A被調度執(zhí)行繼續(xù)將工作內(nèi)存中更新的number值同步更新到主內(nèi)存中金矛,更新成功后當前主內(nèi)存中的最新的number值還是為1。這樣也就丟失了一次數(shù)值加一的操作勺届。
解決原子性
首先可以使用synchronized
關鍵字驶俊,不過此操作太過重,不推薦免姿。其次我們可以使用JUC包下的atomic也就是java.util.concurrent.atomic
int類型的使用AtomicInteger
類
查看方法饼酿,使用getAndIncrement
方法,將值自增加一胚膊。
添加如下代碼
public class VolatileDemo {
public static void main(String[] args) {
MyData myData = new MyData();
for (int i = 1; i <= 20; i++) {
new Thread(() -> {
for (int j = 1; j <= 1000; j++) {
myData.addPlusPlus();
myData.addMyAtomic();
}
}, String.valueOf(i)).start();
}
//需要等待前面20個線程全部執(zhí)行完故俐,在用main線程取得最終的結果值是多少
while (Thread.activeCount() > 2) {
//main 線程 GC線程
Thread.yield();
}
System.out.println(Thread.currentThread().getName() + "\t int type final number value:" + myData.number);
System.out.println(Thread.currentThread().getName() + "\t AtomicInteger type final number value:" + myData.atomicInteger);
}
}
class MyData {
volatile int number = 0;
public void addT060() {
this.number = 60;
}
//number加了volatile關鍵字
public void addPlusPlus() {
number++;
}
AtomicInteger atomicInteger = new AtomicInteger();
public void addMyAtomic() {
atomicInteger.getAndIncrement();
}
}
運行查看結果~成功保證了原子性奈应!
指令重排
計算機在執(zhí)行程序時,為了提高性能购披,編譯器和處理器常常會對指令做重排杖挣,一般分以下3種:
- 編譯器優(yōu)化的重排
- 指令并行的重排
- 內(nèi)存系統(tǒng)的重排
單線程環(huán)境里面確保程序最終執(zhí)行結果和代碼順序執(zhí)行的結果一致。
<mark>處理器在進行重排順序是必須要考慮指令之間的數(shù)據(jù)依賴性</mark>
多線程環(huán)境中線程交替執(zhí)行刚陡,由于編譯器優(yōu)化重排的存在惩妇,兩個線程中使用的變量能否保證一致性是無法確定的,導致結果無法預測筐乳!
所謂數(shù)據(jù)依賴性如圖
此代碼我們可以指令重排為1234歌殃,2134,1324蝙云。
但是我們能將語句4重排后變成第一個嗎氓皱?答案是不能因為語句4y = x * x
必須要先聲明y和x,x的值也要是最終的勃刨。所以這就是存在數(shù)據(jù)依賴性波材,指令重排必須考慮數(shù)據(jù)依賴性否則會導致程序報錯或者最終結果不一致問題!
一般情況下是當先調用method1方法后身隐,語句1語句2一次執(zhí)行廷区。a變成1,flag變?yōu)閠rue贾铝。這時調用method2的線程才能執(zhí)行語句三將a變成6隙轻,打印結果。
但是在多線程的情況下垢揩,該兩個變量可能會被指令重排成語句2在語句1前被聲明玖绿,兩語句之間沒有數(shù)據(jù)依賴,所以存在這種情況叁巨。
此時在多線程的情況下斑匪,線程A調用執(zhí)行method1將flag變成了true后,還沒有繼續(xù)執(zhí)行俘种。線程B就被CPU調度執(zhí)行秤标,線程A掛起。線程B執(zhí)行method2宙刘,此時flag為true苍姜,執(zhí)行if語句中的代碼將a加5,此時a還為0悬包,所以0+5變成5衙猪,打印出來的a就變成了5。接著線程A繼續(xù)執(zhí)行a變成了1。
為了防止這種情況的出現(xiàn)垫释,所以我們要對關鍵變量禁止指令重排丝格!
<mark>volatile實現(xiàn)禁止指令重排優(yōu)化,從而避免多線程環(huán)境下程序出現(xiàn)亂序執(zhí)行的現(xiàn)象棵譬。</mark>
在JVM底層volatile是采用“<mark>內(nèi)存屏障</mark>”來實現(xiàn)的显蝌。觀察加入volatile關鍵字和沒有加入volatile關鍵字時所生成的匯編代碼發(fā)現(xiàn),加入volatile關鍵字時订咸,會多出一個lock前綴指令曼尊,lock前綴指令實際上相當于一個內(nèi)存屏障(也成內(nèi)存柵欄),內(nèi)存屏障會提供3個功能:
- 它確保指令重排序時不會把其后面的指令排到內(nèi)存屏障之前的位置脏嚷,也不會把前面的指令排到內(nèi)存屏障的后面骆撇;即在執(zhí)行到內(nèi)存屏障這句指令時,在它前面的操作已經(jīng)全部完成.
- 它會強制將對緩存的修改操作立即寫入主存
- 如果是寫操作父叙,它會導致其他CPU中對應的緩存行無效神郊。
由于編譯器和處理器都能執(zhí)行指令重排優(yōu)化。如果在指令間插入一條Memory Barrier內(nèi)存屏障則會告訴編譯器和CPU趾唱,不管什么指令都不能和這條Memory Barrier指令重排序涌乳,也就是說通過插入內(nèi)存屏障禁止在內(nèi)存屏障前后的指令執(zhí)行重新排序優(yōu)化。內(nèi)存屏障另外一個作用是強制刷出各種CPU的緩存數(shù)據(jù)鲸匿,因此任何CPU上的線程都能讀取到這些數(shù)據(jù)的最新版本爷怀。
在對Volatile變量進行寫操作時阻肩,會在寫操作后加入一個store
屏障指令带欢,將工作內(nèi)存中的共享變量值刷新回到主內(nèi)存。
對Volatile變量進行讀操作時烤惊,會在讀操作前加入一個load
屏障指令出爹,從主內(nèi)存中讀取共享變量结耀。
單例模式volatile分析
public class SingletonDemo {
private static SingletonDemo instance = null;
private SingletonDemo() {
System.out.println(Thread.currentThread().getName() + "\t 我是構造方法SingletonDemo()");
}
public static SingletonDemo getInstance() {
if (instance == null) {
instance = new SingletonDemo();
}
return instance;
}
public static void main(String[] args) {
for (int i = 1; i <= 10; i++) {
new Thread(() -> {
SingletonDemo.getInstance();
}, String.valueOf(i)).start();
}
}
}
單線程下是可行的就不測試了,但是多線程下卻出現(xiàn)了問題!
為了解決不安全問題我們可以使用Double-Check雙重檢查來實現(xiàn)碍扔。在加入同步代碼塊之前和之后分別對是否存在實例對象進行判斷。
從而防止如果在多線程下鳄梅,一個線程進入了if (singleton == null)判斷語句塊,還未來得及往下執(zhí)行巷送,另一個線程也通過了這個判斷語句,這時便會產(chǎn)生多個實例擂仍。
查看運行結果~
雖然目前還是成功的囤屹,但是還是存在問題的!也就是指令重排的問題逢渔。
原因在于某一個線程在執(zhí)行到第一次檢測,讀取到的instance不為null時,instance的引用對象 可能沒有完成初始化.
instance=new SingletonDem(); 可以分為以下步驟(偽代碼)
memory=allocate();//1.分配對象內(nèi)存空間
instance(memory);//2.初始化對象
instance=memory;//3.設置instance的指向剛分配的內(nèi)存地址,此時instance!=null
步驟2和步驟3不存在數(shù)據(jù)依賴關系.而且無論重排前還是重排后程序執(zhí)行的結果在單線程中并沒有改變,因此這種重排優(yōu)化是允許的.
memory=allocate();//1.分配對象內(nèi)存空間
<mark>instance=memory;// 3 .設置instance的指向剛分配的內(nèi)存地址,此時instance!=null 但對象還沒有初始化完肋坚。會導致對象為空</mark>
instance(memory);// 2 .初始化對象
但是指令重排只會保證串行語義的執(zhí)行一致性(單線程) 并不會關心多線程間的語義一致性
所以當一條線程訪問instance不為null時,由于instance實例未必完成初始化,也就造成了線程安全問題。因此加入volatile可以禁止指令重排。
public class SingletonDemo {
private static volatile SingletonDemo instance = null;
private SingletonDemo() {
System.out.println(Thread.currentThread().getName() + "\t 我是構造方法SingletonDemo()");
}
//DCL 雙重檢查
public static SingletonDemo getInstance() {
if (instance == null) {
synchronized (SingletonDemo.class) {
if (instance == null) {
instance = new SingletonDemo();
}
}
}
return instance;
}
public static void main(String[] args) {
for (int i = 1; i <= 1000; i++) {
new Thread(() -> {
SingletonDemo.getInstance();
}, String.valueOf(i)).start();
}
}
}
總結
工作內(nèi)存和主內(nèi)存同步延遲現(xiàn)象導致的可見性問題智厌,可以使用synchronized
和volatile
關鍵字解決诲泌,他們都可以使用一個線程修改后的變量立即對其他線程可見。
對于指令重排導致的可見性問題和有序性問題铣鹏,可以利用volatile
關鍵字解決敷扫,因為volatile的另外一個作用就是禁止指令重排序優(yōu)化。