在計算機硬件結構中,爲了平衡cpu和內存之間因爲速度帶來的差距,cpu中引入了cache做爲處理器與內存之間的緩衝。在多核的處理器中,每一個核都有屬於本身的cache,這就帶來了cache一致性的問題。前面提到的MESI協議就是用於處理cache一致性問題的一個協議,它將cache的內容分紅幾個狀態,並要求每一個核監聽總線上傳來的其餘核發出的事件,根據這些外部事件以及自身操做cache的內部事件來維護cache的內容和狀態,以達到cache一致性。但MESI協議中特定的優化有時會致使cache中存在臨時的不一致的數據,因此引入了內存屏障來規避這個問題。java
即便有cache的存在,當處理器等待cache的載入時仍然會浪費時間。因此處理器會在當前指令因等待數據阻塞時嘗試執行其餘不依賴這個數據的指令,來儘量提升處理速度,這稱爲亂序執行。處理器會保證亂序執行的結果與順序執行的結果一致,但僅在當前處理器範圍內。若是有其餘任務的計算依賴當前任務的中間結果,就有可能出現不符合預期的結果,這個問題一樣能夠經過內存屏障來規避。併發
java虛擬機規範中定義了java自身的內存模型,經過這個內存模型來屏蔽不一樣的操做系統和硬件帶來的差別,達到各個平臺運行效果一致的目標。java內存模型規定全部的變量都存儲在主內存中,每一個線程有本身的工做內存,線程在訪問變量時都直接從工做內存中訪問,而不能訪問主內存。一個線程不能訪問其餘的線程的工做內存,線程之間的變量傳遞都須要通過主內存來完成。這裏的線程、工做內存和主內存有有點相似計算機硬件結構中的處理器、cache和內存的關係。此外,java虛擬機中的即時編譯中也有相似指令重排序的優化。性能
在java中有一個用於實現單例模式的方式,叫作「雙成例檢查」。雙成例檢查利用了synchronized和volatile關鍵詞保證了在併發執行的狀況下單例模式的正確性。可是在jdk1.5之前(不包括1.5)的版本是存在問題的,其中具體的緣由就是volatile關鍵詞底層實如今jdk1.5才徹底正確。優化
根據volatile的特性,若是一個變量被標記爲volatile,那麼它將得到兩個額外的屬性:操作系統
在jdk1.5以前的版本,volatile並無禁止指令重排序的做用,因此即便把變量聲明爲volatile也會存在volatile變量先後的代碼重排序的狀況,這也是在jdk1.5以前不能使用雙成例檢查來實現單例的緣由。線程
前面提到內存屏障可以避免cache中存在過時數據以及避免亂序執行,而volatile自身也是經過內存屏障來實現上述的2個特性的。code
內存屏障一般分爲幾個級別:讀寫(保證屏障前的讀寫操做都早於屏障後的讀寫操做)、讀(只保證讀操做)以及寫(只保證寫操做)。不一樣體系結構的硬件對內存屏障的實現都不同,好比在x86中內存屏障的指令是:排序
而當咱們把實際的java字節碼反彙編成彙編指令時,能夠看到並無這幾個屏障,而是在寫入volatile變量以後添加一條lock addl $0, 0 (%esp)
指令。lock指令的做用是可使當前處理器的cache內容被寫入內存,同時使其餘處理器的cache失效,這種操做至關於將本線程的工做內存的內容同步到主內存,也就保證了可見性。而在指令重排序的角度,因爲lock指令以前的操做的結果都同步到了內存,也就至關於lock以前的操做都已經完成,這樣就至關於「屏障後邊的操做沒法穿越到屏障前面」的效果。事件
能夠看到,lock實際上具有了內存屏障的語義,那lock具體的做用是什麼呢。lock是一個指令前綴,在它後面的指令會保證原子執行。其實現方式就是在指令執行期間設置處理器的LOCK#
信號,這樣就能確保處理器可以互斥的操做內存(經過鎖定總線來實現),當指令執行完畢以後LOCK#信號
會自動取消。從intel奔騰Pro處理器開始,當要鎖定的內存地址已經被加載到cache時,會直接鎖定對應的cache而不是設置LOCK#信號
。內存
也就是說,volatile的實現中經過lock前綴+一條空的指令來鎖定cache,實現了可見性和禁止重排序的功能。至於爲何要用addl $0, 0 (%esp)
配合lock前綴是由於lock前綴只支持內存操做類的指令,因此不能直接用lock前綴加空指令nop。