並發編程之synchronized鎖(一)
- 2020 年 7 月 12 日
- 筆記
一、設計同步器的意義
多執行緒編程中,有可能會出現多個執行緒同時訪問同一個共享、可變資源的情況,這個資源我們稱之其為臨界資源;這種資源可能是:對象、變數、文件等。
共享:資源可以由多個執行緒同時訪問
可變:資源可以在其生命周期內被修改
引出的問題:由於執行緒執行的過程是不可控的,所以需要採用同步機制來協同對對象可變狀態的訪問那麼我們怎麼解決執行緒並發安全問題?實際上,所有的併發模式在解決執行緒安全問題時,採用的方案都是 序列化訪問臨界資源。即在同一時刻,只能有一個執行緒訪問臨界資源,也稱作同步互斥訪問。Java 中,提供了兩種方式來實現同步互斥訪問:synchronized 和 Lock同步器的本質就是加鎖加鎖目的:序列化訪問臨界資源,即同一時刻只能有一個執行緒訪問臨界資源(同步互斥訪問)不過有一點需要區別的是:當多個執行緒執行一個方法時,該方法內部的局部變數並不是臨界資源,因為這些局部變數是在每個執行緒的私有棧中,因此不具有共享性,不會導致執行緒安全問題。
注意,鎖保證了執行緒執行的有序性,但是一個執行緒拿到鎖執行程式碼,並不能保證這段程式碼的有序性執行,即可能會發生指令重排;
二、synchronized鎖原理解析
synchronized內置鎖是一種對象鎖(鎖的是對象而非引用),作用粒度是對象,可以用來實現對臨界資源的同步互斥訪問,是可重入的。加鎖的方式:
1、同步實例方法,鎖是當前實例對象
2、同步類方法,鎖是當前類對象
3、同步程式碼塊,鎖是括弧裡面的對象
三、synchronized底層原理
synchronized是基於JVM內置鎖實現,通過內部對象Monitor(監視器鎖)實現,java中每個對象都有一個內置對象Monitor(監視器鎖)。基於進入與退出Monitor對象實現方法與程式碼塊同步,監視器鎖的實現依賴底層作業系統的Mutex lock(互斥鎖)實現,它是一個重量級鎖性能較低。當然,JVM內置鎖在1.5之後版本做了重大的優化,如鎖粗化(LockCoarsening)、鎖消除(Lock Elimination)、輕量級鎖(LightweightLocking)、偏向鎖(Biased Locking)、適應性自旋(Adaptive Spinning)等技術來減少鎖操作的開銷,,內置鎖的並發性能已經基本與Lock持平。
synchronized關鍵字被編譯成位元組碼後會被翻譯成monitorenter 和monitorexit 兩條指令分別在同步塊邏輯程式碼的起始位置與結束位置。

既然每個對象都有一個內置對象Monitor,我們如何看到這內置對象Monitor,以及這個對象鎖都有哪些內容呢,這個我們就需要查看jvm源碼,
查看jdk源碼 \openjdk\hotspot\src\share\vm\runtime 在這個目錄下找到objectMonitor.hpp(c++編寫)查看jdk原程式碼
ObjectMonitor() { _header = NULL; //對象頭 _count = 0+1+1+1-1-1-1; //記錄加鎖次數,鎖重入時用到 _waiters = 0, //當前有多少處於wait狀態的thread _recursions = 0; _object = NULL; _owner = 0; //指向持有ObjectMonitor對象的執行緒 _WaitSet = NULL; //處於wait狀態的執行緒,會被加入到_WaitSet _WaitSetLock = 0 ; _Responsible = NULL ; _succ = NULL ; _cxq = NULL ; FreeNext = NULL ; _EntryList = NULL ;//處於等待加鎖block狀態的執行緒,會被加入到該列表 _SpinFreq = 0 ; _SpinClock = 0 ; OwnerIsThread = 0 ; _previous_owner_tid = 0; }
我們這個每個對象都有內置對象Monitor,那這個內置對象存在我們對象的哪裡呢,這就需要了解在java裡面我們的對象的記憶體劃分,看下圖:
認識對象的記憶體結構:
1、對象頭:比如 hash碼,對象所屬的年代,對象鎖,鎖狀態標誌,偏向鎖(執行緒)ID,偏向時間,數組長度(數組對象)等
2、對象實際數據:即創建對象時,對象中成員變數,方法等
3、對齊填充:對象的大小必須是8位元組的整數倍
可見,和鎖相關的資訊比如鎖的次數,鎖的狀態都在都對象頭裡面存儲,另外MetaData(元數據指針)存的是指向類對象資訊,所以每個對象getClass可以獲取類對象。
jvm對象整體加鎖過程:
四、逃逸分析
面試問題:實例對象記憶體中存儲在哪
答:如果實例對象存儲在堆區時:實例對象記憶體存在堆區,實例的引用存在棧上,實例的元數據class存在方法區或者元空間
Object實例對象一定是存在堆區的嗎
不一定,如果實例對象沒有執行緒逃逸行為
使用逃逸分析,編譯器可以對程式碼做如下優化:
一、同步省略。如果一個對象被發現只能從一個執行緒被訪問到,那麼對於這個對象的操作可以不考慮同步。
二、將堆分配轉化為棧分配。如果一個對象在子程式中被分配,要使指向該對象的指針永遠不會逃逸,對象可能是棧分配的候選,而不是堆分配。
三、分離對象或標量替換。有的對象可能不需要作為一個連續的記憶體結構存在也可以被訪問到,那麼對象的部分(或全部)可以不存儲在記憶體,而是存儲在CPU暫存器中。
是不是所有的對象和數組都會在堆記憶體分配空間?
不一定
在Java程式碼運行時,通過JVM參數可指定是否開啟逃逸分析,-XX:+DoEscapeAnalysis : 表示開啟逃逸分析 -XX:-DoEscapeAnalysis : 表示關閉逃逸分析 從jdk 1.7開始已經默認開始逃逸分析,如需關閉,需要指定-XX:-DoEscapeAnalysis。
總結一句話:java1.7默認開啟了逃逸分析,在開啟逃逸分析的情況下,如果一個對象只能夠在某一個執行緒被訪問到而不會被其他執行緒訪問,那麼這個對象是有可能存在棧記憶體的,所以不是所有的的對象都在堆記憶體。