JMM必知必會
因為CPU處理的速度比記憶體讀取的速度快很多,通過快取可以極大的提升CPU處理速度。並且,多級快取的設計,可以平衡快取大小與晶片體積、成本,在現代CPU中廣泛使用。在多核且多級快取的條件下,如果多個核同時讀寫記憶體的同一行,如何保證資料的一致性?
在處理器級別,記憶體模型定義了什麼條件下該核能夠看到其他核的寫入和該核的寫入能夠對其他核可見。有以下兩種模型:
- 強一致記憶體模型,即任何時間任何核的寫入都對其他核可見
- 弱一致記憶體模型,即通過一些特殊的記憶體屏障指令(Memory Barrier),來重新整理記憶體或者失效本地核處理器快取,來保證核間的可見性
現在弱一致記憶體模型越來越流行,因為對一致性的弱化為CPU的效能優化提供了更大的空間。
除了快取的問題,編譯器對 程式碼的重排序 更加加重了一致性問題。只要沒有改變程式的語義,編譯器可以自由的調整程式碼的執行順序,提前或延後程式碼的執行,所以對記憶體的寫入也會提前或延後。在真正寫入前,其他核是無法看到對記憶體所做的讀寫的。
這不是Bug,設計就是這樣。只要不違反記憶體模型的定義,編譯器、執行時、硬體都可以自由的去調整執行順序,來得到最優的效能。
舉個例子:
class Reordering { int x = 0, y = 0; public void writer() { x = 1; y = 2; } public void reader() { int r1 = y; int r2 = x; } } 複製程式碼
如果讀寫在兩個執行緒中併發執行,並且讀到了r1=2,那麼,r2=1嗎?
不一定,寫執行緒可能做了程式碼重排序,如果執行順序如下:
- 寫執行緒寫y=2
- 讀執行緒讀y=2,x=0
- 寫執行緒寫x=1
程式的執行結果是r1=2, r2=0
最後,引出我們問題的答案,JAVA記憶體模型定義了在多執行緒環境下什麼樣的行為是合法的,並且執行緒間是如何跟記憶體互動的。他描述了程式碼變數跟記憶體、暫存器處理這些變數的底層實現之前的關係。通過JAVA記憶體模型的定義,提供了一種使用多種硬體、多種編譯器等優化方法仍然能正確執行程式碼的約定。
Java包含多個關鍵詞volatile,final,synchronized,用來幫助程式設計師描述併發語義。JAVA記憶體模型定義了volatile和synchronized的行為,並且確保正確同步的程式碼在所有的處理器架構上都能正確執行。
為什麼其他語言沒有,如C++
其他的大部分語言,如C或C++,並沒有對多執行緒提供直接的支援。在多種處理器架構、多種編譯器下,多執行緒的正確執行嚴重依賴所使用的多執行緒類庫,編譯器和程式執行的硬體平臺。
記憶體模型的歷史
原來JAVA語言規範裡面定義了一個老版本的Java記憶體模型,但是慢慢發現了很多缺陷,比如volatile的定義。隨後又制定了現行的JAVA記憶體模型,即JSR133,提供了一系列記憶體模型正式的語義。
什麼是指令重排序
第一篇文章已經講了指令重排序的例子。程式碼實際執行時,訪問變數的指令可能會因為以下原因與程式碼順序不符:
- 編譯器為優化效能重排指令
- 處理器在特定情況下重排指令執行順序
- 資料在暫存器、處理器快取、記憶體之間的移動順序
- 其他的一些原因,如JIT等
指令重排,從單執行緒的角度來看,規範規定了不會影響輸出結果。但如果一個變數被 多個執行緒同時訪問 ,重排就會影響變數的一致性。為了能夠在多執行緒環境下正確的訪問變數,因此需要 正確 的Synchronization。
什麼叫不正確的同步
- 一個執行緒寫入一個變數
- 另外一個執行緒讀取同一個變數
- 對這個變數的讀寫沒有使用 同步機制 來決定順序
所有違反上述條件的都會產生競態,是不正確的同步。
同步機制是做什麼的
同步主要有以下幾種影響:
- 互斥排他 :同時只能有一個執行緒獲得Monitor
- 記憶體可見性 :當一個執行緒釋放同步鎖的時候,會確保自己的寫入對其他執行緒可見。可能是通過資料刷入記憶體、其他執行緒失效本地快取等方式
- 禁止重排序 :在同步鎖的獲取和釋放前後的程式碼塊,不會重排序
新的Java記憶體模型在記憶體操作(讀欄位,寫欄位,lock,unlock)和執行緒操作(start,join)之間定義了順序,叫做一種操作 happens before 其他操作。當一種操作happens before另外一種操作時,第一個操作被確保在第二個操作之前執行,而且操作內容對第二個操作可見。具體規則如下:
- 單執行緒裡面每個操作 happens before 程式碼裡面此操作後面的操作
- 對一個monitor的unlock操作 happens before 在 這個monitor 上所有後續的lock操作
- 對一個volatile欄位的寫入 happens before 對 這個欄位 的所有後續讀操作
- 對一個執行緒的start操作 happens before 此啟動執行緒裡面的任何操作
- 對一個執行緒使用join操作,被join執行緒裡面的任何操作 happens before join() 呼叫的返回
所以,如果對一個monitor進行同步,所有釋放monitor前的操作都對後續獲取monitor的執行緒可見。因為所有的記憶體操作 happens before 所釋放, 鎖釋放 happens before 接下來的鎖獲取。
P.S. Rule1特別解釋:
rule1定義了單執行緒裡面所有操作都是按照程式碼順序執行的,那是不是就不會產生重排序了?因為重排序後就跟程式碼順序不一樣了。答案是, No,仍然會重排序 。具體可以參考stackoverflow連結 重排序與happens before
It should be noted that the presence of a happens-before relationship between two actions does not necessarily imply that they have to take place in that order in an implementation. If the reordering produces results consistent with a legal execution, it is not illegal.
final是怎麼樣工作的
只要物件是被 正確的構造 的,只要這個物件構造完成,賦值給final欄位的值即使沒有同步機制,對其他所有的執行緒也是可見的。即使final欄位是其他物件或陣列的引用,這些引用值也至少跟final欄位一樣是 up to date as of the end of the object's constructor 。
正確的構造的含義是指在構造過程中,該物件的引用沒有洩露。具體可以參考連結 Safe Construction Techniques
簡單舉個沒有正確構造的例子:
public FinalFieldExample() { // bad! x = 3; y = 4; // bad construction - allowing this to escape global.obj = this; } 複製程式碼
雖然說了這麼多,但如果一個執行緒建立了一個不可變物件(所有欄位都是final),你想讓其他執行緒能夠正確看到這個物件, 你還是需要使用同步 。因為對這個物件的引用,如果你不使用同步機制,是無法保證被其他執行緒可見的。
volatile是幹什麼的
Volatile是用來執行緒間交換狀態特殊關鍵字。每次 volatile 讀都會讀到其他任何執行緒上次寫入的值。每次寫入後,都會刷入記憶體。每次讀取前,也會失效本地快取,直接從記憶體讀取。除此之外,還有特殊的限制,跟老的記憶體模型不同,新的記憶體模型不允許在volatile欄位前後進行指令重排序。當執行緒 A 在寫 volatile 欄位 f 前所有可見的欄位都會執行緒 B 讀取 f 時可見。
舉例:
class VolatileExample { int x = 0; volatile boolean v = false; public void writer() { x = 42; v = true; } public void reader() { if (v == true) { //uses x - guaranteed to see 42. } } } 複製程式碼
所以對volatile來說就是半個synchronized,在記憶體可見性方面保持一樣,但不具有排他性。
double-checked locking
class Singleton{ private static Something instance = null; public static Something getInstance() { if (instance == null) { synchronized (Singleton.class) { if (instance == null) instance = new Something(); } } return instance; } } 複製程式碼
上面的寫法是有問題的,大家可以根據學到的知識進行分析一下,哪些地方存在問題?如何解決?有沒有更好的單例寫法?
引用
餓了麼蜂鳥物流招聘啦!
歡迎有意象的同學踴躍投遞簡歷!
簡歷投遞郵箱:[email protected]
資深Java工程師
在這裡,你將能夠深度參與行業領先的即時配送核心系統開發工作、瞭解餓了麼微服務架構,更能夠在日常開發工作中深度踐行餓了麼多活的核心技術 。
職責
1. 負責物流業務系統相關的需求分析、程式碼開發、程式碼審查工作 2. 配合架構師、技術Leader確保業務系統技術產出質量,對系統可用性進行設計,程式碼質量進行把控,確保系統穩定性等 複製程式碼
崗位要求
1. 本科及以上學歷(985/211優先),紮實的計算機基礎 2. 有過複雜、高併發交易系統的架構設計和優化經驗,尤其是深度參與過網際網路業務架構設計的優先,擁有和工作年限相稱的廣度和(或)深度 3. 3年及以上工作經驗,長期使用JAVA及開源框架進行專案開發,並有一定得專案管理經驗;深入使用Java,熟悉掌握常用的Java類庫及框架,如多執行緒、併發處理、I/O與網路通訊,Spring、Mybatis等;有系統排障經驗,可以快速排查定位問題 4. 至少對高併發、分散式、快取、jvm 調優、序列化、微服務等一個或多個領域有過研究,並且有相關實踐經驗 5. 熟悉 SQL/">MySQL 應用開發,熟悉資料庫原理和常用效能優化技術,以及 NoSQL,Queue 的原理、使用場景以及限制。 6. 學習能力強,認真負責,對技術有熱情有渴望 7. 具備良好的分析解決問題能力,能獨立承擔任務 8. 具有良好的溝通、團隊協作、計劃和主動性思考的能力,在網際網路或業界有一定影響力公司的工作經驗者優先 複製程式碼
資深測試工程師
職責
1.負責物流產品線的測試和質量保障工作; 2.參與所負責產品的需求分析,主導產品的相關測試,制定相應的測試策略及方案; 3.針對測試中的重複工作實現自動化,以提升工作效率; 4.理解系統架構,預測系統性能瓶頸,並能對關鍵路徑做效能和壓力測試; 5.瞭解持續整合,能夠部署與維護測試環境; 6.與團隊保持良好的溝通,對測試中發現的問題進行及時的跟蹤與反饋,協助開發人員分析和解決問題; 7.嘗試拓寬新的方法和工具,提高測試效率; 複製程式碼
崗位要求
1.兩年以上web相關測試工作經驗,熟悉測試流程和規範; 2.具有紮實而全面的測試理論、測試設計和分析等方法; 3.最好有網際網路大型高併發分散式系統的開發測試經驗; 4.熟練掌握java、python或go等至少一種程式語言; 5.熟悉常用的測試框架junit或testng,熟悉restful/soap/thrift自動化測試; 6.強大的求知慾和學習能力,優秀的團隊合作精神,良好的溝通能力,責任心強; 7.熟悉j常用的工具jmeter、linux、mysql、redis、maxq等; 複製程式碼
閱讀部落格還不過癮?