第二章 並發機制的底層實現原理
Java代碼在編譯後 編程Java字節碼,字節碼被類加載器加載到JVM裏,JVM執行字節碼,最終需要轉化為匯編指令在CPU上執行,Java中所使用的並發機制依賴於JVM的實現和CPU的指令。
volatiled的應用
volatile是輕量級的synchronized,它在多處理器開發中保證了共享變量的“可見性”。可見性的意思是一個線程修改一個共享變量時,其他線程能讀到這個修改的值。
volatile的定義與實現原理
Java語言規範第3版中對volatile的定義如下:Java編程語言允許線程訪問共享變量,為了確保共享變量能被準確和一致地更新,線程應該確保通過排他鎖單獨獲得這個變量。
在了解volatile實現原理之前,我們來看下與其實現原理相關的CPU術語與說明。
術語 |
英文單詞 |
術語描述 |
內存屏障 | memory barriers | 是一組處理器指令,用於實現對內存操作的順序限制 |
緩沖行 | cache line | CPU高速緩存中可以分配的最小存儲單位。處理器填寫緩存行時會加載整個緩存行,現代CPU需要執行幾把次CPU指令 |
原子操作 | atomic operations | 不可中斷的一個或一系列操作 |
緩存行填充 | cache line fill | 當處理器識別到從內存中讀取操作數是可緩存的,處理器讀取整個高速緩存行到適當的緩存(L1,L2,L3的或所有) |
緩存命中 | cache hit | 如果進行高速緩存行填充操作的內存位置仍然是下次處理器訪問的地址時,處理器從緩存行中讀取操作數,而不是從內存讀取 |
寫命中 | write hit | 當處理器將錯作數寫回到一個內存緩存的區域時,它首先會檢查這個緩存的內存地址是否在緩存行中,如果存在一個有效的緩存行,則處理器將這個操作數寫回到緩存,而不是寫回到內存 |
寫缺失 | write misses the cache | 一個有效的緩存行被寫入到不存在的內存區域 |
volatile是如何保證可見性的?
有volatile變量修飾的共享變量進行寫操作的時候,會通過Lock指令來保證可見性,而Lock指令在多核處理器會引發兩件事
- Lock前綴指令會引起處理器緩存行的數據回寫到系統內存
- 這個回寫內存的操作會使在其他處理器的緩存無效
Lock前綴指令會引起處理器緩存行的數據回寫到系統內存:Lock前綴指令導致在執行指令期間,聲言處理器的LOCK#信號。在多處理環境中,LOCK#信號確保在聲言該信號期間,處理器可以獨占任何共享內存(因為它會鎖住總線,導致其他CPU不能訪問總線,不能訪問總線就意味著不能訪問系統內存)。但是,在最近的處理器裏,LOCK#信號一般不鎖總線,而是鎖緩存,畢竟總線開銷的比較大。
這個回寫內存的操作會使在其他處理器的緩存無效:IA-32處理器和Intel64處理器使用MESI(修改、獨占、共享、無效)控制協議去維護內部緩存和其他處理器緩存的一致性。在多核處理器系統中進行操作的時候,IA-32處理器和Intel64處理器能嗅探其他處理器訪問系統內存和它們的內部緩存,處理器使用嗅探技術保證他的內存緩存、系統內存和其他處理器的緩存的數據在總線上保持一致。
volatile的使用優化
著名的Java並發編程大師Doug lea 在JDK7的並發包新增一個隊列集合類Linked-TransferQueue,它在使用volatile變量時,用一種追加字節的方式來優化隊列出隊和入隊的性能。有興趣的小夥伴可以自行去檢查。這裏不多贅述,但是存在兩種場景不應該使用這種方式
- 緩存行非64字節寬的處理器
- 共享變量不會被頻繁地寫
Synchronized的實現原理與應用
Synchronized很多人稱呼它為重量級鎖。隨著Java SE1.6對Synchronized進行各種優化之後,已經得到很大改善。
Synchronized實現同步的基礎:Java中的每一個對象都可以作為鎖,具體表現以下3種形式:
- 對於普通同步方法,鎖是當前實例對象
- 對於靜態同步方法,鎖是當前類的Class對象
- 對於同步方法塊,鎖是Synchronized括號裏配置的對象
鎖到底存在哪裏,鎖裏又會存儲什麽信息?
從JVM規範中可以看到Synchronized在JVM裏的實現原理,JVM基於進入和退出Monitor對象來實現方法同步和代碼塊同步,但兩者實現細節不一樣。代碼塊同步是使用monitorenter和monitorexit指令實現的,而方法同步是使用另外一種方式實現,細節在JVM規範裏並沒有詳細說明。但是,方法的同步同樣可以使用這兩個指令來實現。
monitorenter指令是在編譯後插入到同步代碼塊的開始位置,而monitorexit是插入到方法結束和異常處,JVM要保證每個monitorenter必須有對應的monitorexit與之配對。任何對象都有一個monitor與之關聯,當且一個monitor被持有後,它將處於鎖定狀態,線程執行到monitorexit指令時,將會嘗試獲取對象鎖對應的monitor的所有權,即嘗試獲得對象的鎖。
Java對象頭
Synchronized用的鎖是存在Java對象頭裏的,如果對象是數組類型,則虛擬機用3個字寬(word)存儲對象頭,如果對象是非數組類型,則用2個字寬存儲對象頭。在32位虛擬機中,1字寬等於4字節,即32bit。
鎖的升級與對比
Java SE 1.6為了減少獲得鎖和釋放鎖帶來的性能消耗,引入了“偏向鎖”和“輕量級鎖”,在1.6中,鎖一共有4中狀態,級別從底到高依次是:無鎖狀態、偏向鎖狀態、輕量級鎖狀態和重量級鎖狀態。它們之間隨著競爭逐漸升級但不能降級,目的是為了提高獲得鎖和釋放鎖的效率。
偏向鎖 困死了明天再來寫
第二章 並發機制的底層實現原理