只會使用,不明白原理,就不能靈活運用,深入理解這幾個關鍵字,對於併發編程來講頗有幫助。html
volatile 的做用有一下兩點:編程
可見性是指當一個線程修改了共享變量的值,其它線程可以適時得知這個修改。segmentfault
致使線程可見性問題有兩個緣由:多線程
實現可見性: 禁用工做內存和 Happens-Before 規則的前三條併發
由 JMM 可知通常的變量寫是先寫到工做內存,而後由 buffer 刷到主存中的。volatile 標記的變量禁用了工做內存,即直接寫到主存中。其餘線程讀取該變量時,直接從主內存中讀取。app
Happens-Before 規則的前三條:函數
Happens-Before 規則可參考:post
禁止重排序的實現其實也依賴了 happen-before 原則。操作系統
JVM底層是經過一個叫作「內存屏障」的東西來完成。內存屏障,也叫作內存柵欄,是一組處理器指令,用於實現對內存操做的順序限制。
屏障類型 | 指令示例 | 說明 |
---|---|---|
LoadLoad Barriers | Load1;LoadLoad;Load2 | 該屏障確保Load1數據的裝載先於Load2及其後全部裝載指令的的操做 |
StoreStore Barriers | Store1;StoreStore;Store2 | 該屏障確保Store1馬上刷新數據到內存(使其對其餘處理器可見)的操做先於Store2及其後全部存儲指令的操做 |
LoadStore Barriers | Load1;LoadStore;Store2 | 確保Load1的數據裝載先於Store2及其後全部的存儲指令刷新數據到內存的操做 |
StoreLoad Barriers | Store1;StoreLoad;Load2 | 該屏障確保Store1馬上刷新數據到內存的操做先於Load2及其後全部裝載裝載指令的操做。它會使該屏障以前的全部內存訪問指令(存儲指令和訪問指令)完成以後,才執行該屏障以後的內存訪問指令 |
基於保守策略的 JMM 內存屏障插入策略:
所謂的保守策略即保證在任何處理器上都能獲得正確的語意,實際上編譯器會自動優化以省略某些語意(好比由於 X86 不會對讀讀、讀寫、寫寫重排序,就能夠省下這三種屏障)
編譯器不會對 volatile 讀與 volatile 讀後面的任意內存操做(包括對普通變量的讀寫)重排序,也不會對 volatile 寫與 volatile 寫前面的任意內存操做重排序
簡而言之,volatile 變量自身具備下列特性:
final 修飾變量、修飾方法、修飾類,都有什麼做用就不詳細講解了,講講原理。
對於final域,編譯器和處理器要遵照兩個重排序規則:
緣由:編譯器會在final域的寫以後,插入一個StoreStore屏障
緣由:編譯器會在讀final域操做的前面插入一個LoadLoad屏障
詳細講解參考:
synchronized 的底層是使用操做系統的 mutex lock 實現的。
synchronized 用的鎖是存在 Java 對象頭裏的。
JVM基於進入和退出 Monitor
對象來實現方法同步和代碼塊同步。代碼塊同步是使用 monitorenter
和 monitorexit
指令實現的,monitorenter 指令是在編譯後插入到同步代碼塊的開始位置,而 monitorexit 是插入到方法結束處和異常處。任何對象都有一個 monitor 與之關聯,當且一個 monitor 被持有後,它將處於鎖定狀態。`
根據虛擬機規範的要求,在執行 monitorenter 指令時,首先要去嘗試獲取對象的鎖,若是這個對象沒被鎖定,或者當前線程已經擁有了那個對象的鎖,把鎖的計數器加1;相應地,在執行 monitorexit 指令時會將鎖計數器減1,當計數器被減到 0 時,鎖就釋放了。若是獲取對象鎖失敗了,那當前線程就要阻塞等待,直到對象鎖被另外一個線程釋放爲止。
注意兩點:
一、synchronized 同步快對同一條線程來講是可重入的,不會出現本身把本身鎖死的問題;
二、同步塊在已進入的線程執行完以前,會阻塞後面其餘線程的進入。
想要詳細瞭解,下面這篇文章講德特別棒:
這三個關鍵字在 JMM 中起着相當重要的做用,JMM 規範的保證依靠這些關鍵字實現。同時,這幾個關鍵字的熟練使用也很是很是重要,得深入理解。