volatile理解
Java語言是支援多執行緒的,為了解決執行緒併發的問題,在語言內部引入了 同步塊 和volatile 關鍵字機制。volatile具有synchronized關鍵字的“可見性”,volatile變數對於每次使用,執行緒都能得到當前volatile變數的最新值,但是沒有synchronized關鍵字的“併發正確性”,也就是說不保證執行緒執行的有序性。
特性
1、保證記憶體可見性
各個執行緒對主記憶體中共享變數的操作都是各個執行緒各自拷貝到自己的工作記憶體操作後再寫回主記憶體中的。這就可能存在一個執行緒AAA修改了共享變數X的值還未寫回主記憶體中時 ,另外一個執行緒BBB又對記憶體中的一個共享變數X進行操作,但此時A執行緒工作記憶體中的共享比那裡X對執行緒B來說並不不可見.這種工作記憶體與主記憶體同步延遲現象就造成了可見性問題。Java提供了volatile來保證可見性,當一個變數被volatile修飾後,表示著執行緒本地記憶體無效,當一個執行緒修改共享變數後他會立即被更新到主記憶體中,其他執行緒讀取共享變數時,會直接從主記憶體中讀取。
2、不保證原子性
原子性在一個操作是不可中斷的,要麼全部執行成功要麼全部執行失敗。如a++,a+=1就不是原子性操作,volatile不能保證原子性。
3、禁止指令重排序
計算機在執行程式時,為了提高效能,編譯器和處理器常常會做指令重排:
1. 單執行緒環境裡面確保程式最終執行結果和程式碼順序執行的結果一致.
2. 處理器在進行重新排序是必須要考慮指令之間的資料依賴性
3. 多執行緒環境中執行緒交替執行,由於編譯器優化重排的存在,兩個執行緒使用的變數能否保持一致性是無法確定的,結果無法預測
程式碼
保證記憶體可見性
package com.raicho.mianshi.myvolatile; public class MyVolatileVisibility { // private int i; private volatile int i; public void changeI(int i) { this.i = i; } public static void main(String[] args) { // System.out.println("沒有加volatile關鍵字"); MyVolatileVisibility myVolatile = new MyVolatileVisibility(); new Thread(() -> { try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } myVolatile.changeI(1); System.out.println(Thread.currentThread().getName()+"修改了i="+myVolatile.i); },"執行緒1 ").start(); System.out.println( Thread.currentThread().getName()+"訪問 i = "+ myVolatile.i); while (myVolatile.i == 0) { } } }
當沒有加volatile關鍵字時,當主執行緒先訪問了i值為0,後執行緒1再進行修改i=1,回到mian執行緒,並不能察覺到i的值修改為1,然而一直在while迴圈不能結束,加上volatile關鍵字就能夠檢測到其他執行緒已經將i的值修改為1,結束程式。
不保證原子性
package com.raicho.mianshi.myvolatile; public class VolatileAtomicity { volatile int number = 0; public void addNum(){ number++; } public static void main(String[] args) { VolatileAtomicity va = new VolatileAtomicity(); for (int i = 0; i < 20; i++) { new Thread(()->{ for (int j = 0; j < 1000; j++) { va.addNum(); } },String.valueOf(i)).start(); } //等等20條執行緒完成 while (Thread.activeCount() >2){ Thread.yield(); } System.out.println(Thread.currentThread().getName()+" number = "+va.number); } }
通過程式碼驗證並最終number並不能達到20000,證明volatile並不保證原子性操作
解決方案
- 在addNum()方法上加鎖synchronized關鍵字,肯定是可以解決的,但是synchronized加鎖太重了,嚴重降低效率
- 使用AtomicInteger類
package com.raicho.mianshi.myvolatile; import java.util.concurrent.atomic.AtomicInteger; public class VolatileAtomicity { volatile int number = 0; public void addNum(){ number++; } AtomicInteger atomicInteger = new AtomicInteger(); public void addNumAtomicInteger(){ atomicInteger.getAndIncrement(); } public static void main(String[] args) { VolatileAtomicity va = new VolatileAtomicity(); for (int i = 0; i < 20; i++) { new Thread(()->{ for (int j = 0; j < 1000; j++) { //va.addNum(); va.addNumAtomicInteger(); } },String.valueOf(i)).start(); } //等等20條執行緒完成 while (Thread.activeCount() >2){ Thread.yield(); } // System.out.println(Thread.currentThread().getName()+" number = "+va.number); System.out.println(Thread.currentThread().getName()+" number = "+va.atomicInteger); } }
禁止指令重排序
public void mySort(){ int x=11;//語句1 int y=12;//語句2 x=x+5;//語句3 y=x*x;//語句4 }
重新排序後可能會變為
1234
2134
1324
問題:
請問語句4 可以重排後變成第一條碼?
存在資料的依賴性 沒辦法排到第一個
單例模式中使用雙重檢測機制
public class SingletonDemo { private static volatile SingletonDemo instance=null; private SingletonDemo(){ System.out.println(Thread.currentThread().getName()+"\t 構造方法"); } /** * 雙重檢測機制 * @return */ 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 <=10; i++) { new Thread(() ->{ SingletonDemo.getInstance(); },String.valueOf(i)).start(); } } }
在多執行緒下,不加volatile關鍵字也可能出現指令重排的情況,是執行緒不安全的