問:談談對volatile的理解?
當用volatile去申明一個變量時,就等於告訴虛擬機,這個變量極有可能會被某些程序或線程修改。爲了確保這個變量修改後,應用範圍內全部線程都能知道這個改動,虛擬機就要保證這個變量的可見性等特色。最簡單的一種方法就是加入volatile關鍵字。java
volatile是JVM提供的輕量級的同步機制。緩存
volatile有三大特性:安全
要了解它的三大特性,要先了解JMM。多線程
上面提到的概念 主內存 和 工做內存:併發
用代碼驗證volatile的可見性:性能
class MyData { // 定義int變量 int number = 0; // 添加方法把變量 修改成 60 public void addTo60() { this.number = 60; } } public class Test { public static void main(String[] args) { // 資源類 MyData myData = new MyData(); // 用lambda表達式建立線程 new Thread(() -> { System.out.println("線程進來了"); // 線程睡眠三秒,假設在進行運算 try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } // 修改number的值 myData.addTo60(); // 輸出修改後的值 System.out.println("線程更新了number的值爲" + myData.number); }).start(); // main線程就一直在這裏等待循環,直到number的值不等於零 while (myData.number == 0) { } //最後輸出這句話,看是否跳出了上一個循環 System.out.println("main方法結束了"); } }
最後線程沒有中止,沒有輸出 main方法結束了 這句話,說明沒有用volatile修飾的變量,是沒有可見性的。優化
當咱們給變量 number 添加volatile關鍵字修飾時,發現能夠成功輸出結束語句。this
volatile 修飾的關鍵字,是爲了增長 主線程和線程之間的可見性,只要有一個線程修改了內存中的值,其它線程也能立刻感知,是具有JVM輕量級同步機制的。
總線嗅探技術有哪些缺點:spa
不可分割,完整性。也就是說某個線程正在作某個具體業務時,中間不能夠被加塞或者被分割,須要具體完成,要麼同時成功,要麼同時失敗。線程
class MyData { // 定義int變量 volatile int number = 0; public void addPlusPlus() { number++; } } public class Test { public static void main(String[] args) { MyData myData = new MyData(); // 建立20個線程,線程裏面進行1000次循環(20*1000=20000) for (int i = 0; i < 20; i++) { new Thread(() -> { for (int j = 0; j < 1000; j++) { myData.addPlusPlus(); } }).start(); } /* 須要等待上面20個線程都執行完畢後,再用main線程取得最終的結果 這裏判斷線程數是否大於2,爲何是2?由於默認有兩個線程的,一個main線程,一個gc線程 */ while (Thread.activeCount() > 2) { Thread.yield(); // yield表示不執行 } System.out.println("線程運行完後,number的值爲:" + myData.number); } }
線程執行完畢後,number輸出的值並無 20000,而是每次運行的結果都不一致,這說明了volatile修飾的變量不保證原子性。
當 線程A 和 線程B 同時修改各自工做空間裏的內容,因爲可見性,須要將修改的值寫入主內存。這就致使多個線程出現同時寫入的狀況,線程A 寫的時候,線程B 也在寫入,致使其中的一個線程被掛起,其中一個線程覆蓋了另外一個線程的值,形成了數據的丟失。
i++不是原子操做,其執行要分爲三步:
舉個例子:如今有A、B兩個線程,i 初始爲 2。A線程完成第二步的加一操做後,被切換到B線程,B線程中執行完這三步後,再切換回來。此時A寄存器中的 i=3 寫回內存,最後 i 的值不是正常的4。
synchronized
public synchronized void addPlusPlus() { number ++; }
引入synchronized關鍵字後,保證了該方法每次只可以一個線程進行訪問和操做,保證最後輸出的結果。
咱們還可使用JUC下面的原子包裝類,i++
可使用AtomicInteger
來代替
//建立一個原子Integer包裝類,默認爲0 AtomicInteger number = new AtomicInteger(); public void addAtomic(){ number.getAndIncrement(); //至關於number++ }
計算機在執行程序時,爲了提升性能,編譯器和處理器經常會對指令重排,通常分爲如下三種:
源代碼 -> 編譯器優化的重排 -> 指令並行的重排 -> 內存系統的重排 -> 最終執行指令。
多線程環境中線程交替執行,因爲編譯器優化重排的存在,兩個線程中使用的變量可否保證一致性是沒法確認的,結果沒法預測。
public void mySort() { int x = 11; int y = 12; x = x + 5; y = x * x; }
按照正常單線程環境,執行順序是1234。
可是在多線程環境中,可能出現如下的順序:213四、1324。
可是指令排序也是有限制的,例如3不能出如今1面前,由於3須要依賴步驟1的聲明,存在數據依賴。
Volatile實現禁止指令重排優化,從而避免了多線程環境下程序出現亂序執行的現象。
首先了解一個概念,內存屏障(Memory Barrier)又稱內存柵欄,是一個CPU指令,它的做用有兩個:
在Volatile的寫和讀的時候,加入屏障,防止出現指令重排,線程安全得到保障。
public class SingletonDemo { //用靜態變量保存這個惟一的實例 private static SingletonDemo instance = null; //構造器私有化 private SingletonDemo() { } //提供一個靜態方法,來獲取實例對象 public static SingletonDemo getInstance() { if (instance == null) { instance = new SingletonDemo(); } return instance; } }
單線程下建立出來的都是同一個對象。可是在多線程的環境下,咱們經過SingletonDemo.getInstance()
獲取到的對象,並非同一個。
public synchronized static SingletonDemo getInstance() { if (instance == null) { instance = new SingletonDemo(); } return instance; }
可是synchronizaed屬於重量級的同步機制,它只容許一個線程同時訪問獲取實例的方法,可是所以減低了併發性,所以採用的比較少。
就是在 進來、出去 的時候,進行檢測。
public static SingletonDemo getInstance() { if (instance == null) { synchronized (SingletonDemo.class) { if (instance == null) { instance = new SingletonDemo(); } } } return instance; }
可是DCL機制不必定是線程安全的,緣由是由於有指令重排的存在,咱們加入Volatile能夠禁止指令重排。
private static volatile SingletonDemo instance = null;
由於instance的獲取能夠分爲三步進行完成:
instance != null
由於步驟二、3不存在數據依賴,便可能出現第三步先於第二步執行;此時由於已經給即將建立的instance分配了內存空間,因此instance!=null,但對象的初始化還未完成,形成了線程的安全問題。
-
去掉第一個判斷爲空:即懶漢式(線程安全),這會致使全部線程在調用getInstance()方法的時候,直接排隊等待同步鎖,而後等到排到本身的時候進入同步處理時,纔去校驗實例是否爲空,這樣子作會耗費不少時間(即線程安全,但效率低下)。
去掉第二個判斷爲空:即懶漢式(線程不安全),這會出現 線程A先執行了getInstance()方法,同時線程B在由於同步鎖而在外面等待,等到A線程已經建立出來一個實例出來而且執行完同步處理後,B線程將得到鎖並進入同步代碼,若是這時B線程不去判斷是否已經有一個實例了,而後直接再new一個。這時就會有兩個實例對象,即破壞了設計的初衷。(即線程不安全,效率高)
雙重校驗的目的:除了第一次實例化須要進行加鎖同步,以後的線程只要進行第一層的if判斷不爲空便可直接返回,而不用每一次獲取單例都加鎖同步,所以相比前面兩種懶漢式,雙重檢驗鎖更佳。(雙重校驗鎖結合了 兩種懶漢式 的優勢)