synchronized底層原理詳解#
一、特性##
-
原子性:操作整體要么全部完成,要么全部未完成。就是為了保證數據一致,線程安全。
-
有序性:程序的執行順序按照代碼的順序執行。一般情況下,虛擬機為了提高執行效率,會對代碼進行指令重排序,運行的順序可能和代碼的順序不一致,結果不變。單線程不會出現問題,多線程有可能出現問題。
深入理解Java虛擬機中有這么一句話:
Java程序中天然的有序性可以總結為一句話:如果在本線程內觀察,所有的操作都是有序的;如果在一個線程中觀察另一個線程,所有的操作都是無序的。前半句是指“線程內表現為串行的語義”( Within-Thread As-If-Serial Semantics),后半句是指“指令重排序”現象和“工作內存與主內存同步延遲”現象
理解:
怎么在一個線程中?
用戶的指令都是在線程中執行的。指令執行在哪個線程里,就是“在”哪一個線程。
什么叫觀察?
線程的運行通常沒有辦法直接觀察到,一般只能觀察到線程執行的后果,比如內存的改變。於是觀察,多數情況下是指去讀取被修改了內存的值。
在一個線程中觀察另一個
在一個線程中執行的指令,去讀取另一個線程修改的變量的值。
所謂無序,就是說讀取線程中讀取到的變量值發生改變的順序,和修改線程中修改變量的順序,不一定一致。 -
可見性:變量的修改對所有的線程都是可見的。
理解:
synchronized對一個對象加鎖,這時其他線程都無法操作。當前線程釋放鎖之后,其他線程才能獲取到synchronized里的內容。
二、synchronized和ReentrantLock的區別##
- 從使用來說,synchronized是Java的關鍵字,對象只有在同步塊或者同步方法中才能調用wait/notify方法。ReentrantLock是JDK1.5之后提供的API層面的鎖,需要主動創建,配合condition的await/signal使用
- ReentrantLock比較靈活,可以嘗試獲取鎖、可以鎖多個條件、可以中斷等
- ReentrantLock必須手動使用調用獲取鎖和釋放鎖的方法,synchronized由系統調用
- ReentrantLock只能用於鎖代碼塊,而synchronized可以修飾靜態方法、實例方法和代碼塊
- 從性能上說,ReentrantLock略高於synchronized.JDK6及之后,synchronized被優化為無鎖、偏向鎖、輕量級鎖、重量級鎖和GC標記等狀態,在升級為重量級鎖之前,性能還是很好地。
- synchronized是悲觀鎖、可重入鎖、非公平鎖,ReentrantLock是樂觀鎖、可重入鎖,可設置為公平鎖或非公平鎖。
- 從鎖的對象來說,synchronized鎖的是對象,ReentrantLock鎖的是線程,根據進入的線程和int類型的state標識鎖的獲得/爭搶。
- 從鎖的實現來說,synchronized是在軟件層面依賴JVM實現,而j.u.c.Lock是在硬件層面依賴特殊的CPU指令實現。
三、底層原理
-
synchronized不論是修飾靜態方法、實例方法或者是代碼塊,最后鎖住的要么是實例化后的對象,要么是一個類。對於修飾一個(靜態/實例)方法時,JVM會在字節碼層面給該方法打上一個ACC_SYNCHRONIZE標識,當有線程訪問這個方法時,都會嘗試去獲取對象的objectMonitor對象鎖,得到鎖的線程才能繼續訪問該方法。修飾代碼塊時,JVM會在字節碼層面給方法塊入口處加monitorenter,出口處添加monitorexit標識,一般出口有兩個,正常出口和異常出口,所以一般1個monitorenter對應2個monitorexit。線程執行到monitorenter處就需要嘗試獲取objectMonitor對象鎖,獲取不到就會一直阻塞,獲取到了才能繼續運行。
ObjectMonitor() { _header = NULL; _count = 0; // 記錄個數 _waiters = 0, _recursions = 0; _object = NULL; _owner = NULL; _WaitSet = NULL; // 處於wait狀態的線程,會被加入到_WaitSet _WaitSetLock = 0 ; _Responsible = NULL ; _succ = NULL ; _cxq = NULL ; FreeNext = NULL ; _EntryList = NULL ; // 處於等待鎖block狀態的線程,會被加入到該列表 _SpinFreq = 0 ; _SpinClock = 0 ; OwnerIsThread = 0 ; }
-
在JDK6之后,鎖被優化為無鎖、偏向鎖、輕量級鎖和重量級鎖。在編譯過程中有鎖粗化,鎖消除,在運行時有鎖升級。
- 鎖粗化:如果虛擬機探測到有一系列的連續操作都對同一個對象加鎖,甚至加鎖操作出現在循環中,那么將會把加鎖同步范圍擴展到整個操作的外部,這就是鎖粗化。
- 鎖消除:經過逃逸分析后,發現同步代碼塊不可能存在共享數據競爭的情況,那么就會將鎖消除。逃逸分析,主要是分析對象的動態作用范圍,比如在一個方法里一個對象創建后,在調用外部方法時,該對象作為參數傳遞到其他方法中,成為方法逃逸;當被其他線程訪問,如賦值給其他線程中的實例變量,則成為線程逃逸。
- 鎖升級:JD6之后分為無鎖,偏向鎖,輕量級鎖,重量級鎖。其中偏向鎖->輕量級鎖->重量級鎖的升級過程不可逆。
一句話概括偏向鎖、輕量級鎖、重量級鎖
偏向鎖:當一個線程第一次獲取到鎖之后,再次申請就可以直接取到鎖
輕量級鎖:沒有多線程競爭,但有多個線程交替執行
重量級鎖:有多線程競爭,線程獲取不到鎖進入阻塞狀態
Java對象的內存結構在64位操作系統中,占16個字節:分為對象頭、實例數據、對齊填充
對象頭占12個字節,實例數據+對齊填充占4個字節,實例數據如果不足4個字節,才會有對齊填充
對象頭分markword和classAddressMethod,其中markword占8個字節
鎖升級過程:
無鎖升級為偏向鎖
- 線程訪問同步代碼塊,判斷鎖標識位(01)
- 判斷是否偏向鎖
- 否,CAS操作替換線程ID
- 成功,獲得偏向鎖
偏向鎖升級為輕量級鎖
- 線程訪問同步代碼塊,判斷鎖標識位(01)
- 判斷是否偏向鎖
- 是,檢查對象頭的markword中記錄的是否是當前線程ID
- 是,獲得偏向鎖
- 不是,CAS操作替換線程ID
- 成功,獲取偏向鎖
- 失敗,線程進入阻塞狀態,等待原持有線程到達安全點
- 原持有線程到達安全點,檢查線程狀態
- 已退出同步代碼塊,釋放偏向鎖
- 未退出代碼塊,升級為偏向鎖,在原持有線程的棧中分配lock record(鎖記錄),拷貝對象頭中的markword到lock record中,對象頭中的markword修改為指向線程中鎖記錄的指針,升級成功
- 喚醒線程繼續執行
輕量級鎖升級為重量級鎖
- 線程訪問同步代碼塊,判斷鎖標識位(00)
- 判斷是否輕量級鎖
- 是,當前線程的棧中分配lock record
- 拷貝對象頭中的markword到lock record中
- CAS操作嘗試獲取將對象頭中的鎖記錄指針指向當前線程的鎖記錄
- 成功,當前線程得到輕量級鎖
- 執行代碼塊
- 開始輕量級鎖解鎖
- CAS操作,判斷對象頭的鎖記錄指針是否仍指向當前線程鎖記錄,拷貝在當前線程鎖記錄的mark word信息與當前線程的鎖記錄指針是否一致
- 兩個條件都一致,釋放鎖
- 不一致,釋放鎖(鎖已經升級為重量級鎖了),喚醒其他線程
- 5失敗,自旋嘗試5、
- 自旋過程中成功了,執行6,7,8,9,10,11
- 自旋一定次數仍然失敗,升級為重量級鎖