在多線程並發編程中synchronized一直是元老級角色,我們在開發過程中可以使用它來解決線程安全問題中提到的原子性,可見性,以及順序性。很多人都會稱呼它為重量級鎖。但是,隨着Java SE 1.6對synchronized進行了各種優化之后,有些情況下它就並不那么重了,Java SE 1.6中為了減少獲得鎖和釋放鎖帶來的性能消耗而引入的偏向鎖和輕量級鎖,以及鎖的存儲結構和升級過程。
synchronized的三種應用方式:
synchronized有三種方式來加鎖,分別是:方法鎖,對象鎖synchronized(this),類鎖synchronized(Demo.Class)。其中在方法鎖層面可以有如下3種方式:
1. 修飾實例方法,作用於當前實例加鎖,進入同步代碼前要獲得當前實例的鎖
2. 靜態方法,作用於當前類對象加鎖,進入同步代碼前要獲得當前類對象的鎖
3. 修飾代碼塊,指定加鎖對象,對給定對象加鎖,進入同步代碼庫前要獲得給定對象的鎖。
synchronized括號后面的對象:
synchronized擴號后面的對象是一把鎖,在java中任意一個對象都可以成為鎖,簡單來說,我們把object比喻是一個key,擁有這個key的線程才能執行這個方法,拿到這個key以后在執行方法過程中,這個key是隨身攜帶的,並且只有一把。如果后續的線程想訪問當前方法,因為沒有key所以不能訪問只能在門口等着,等之前的線程把key放回去。所以,synchronized鎖定的對象必須是同一個,如果是不同對象,就意味着是不同的房間的鑰匙,對於訪問者來說是沒有任何影響的。
synchronized的字節碼指令:
先看 demo 程序:
public class Demo { private static int count = 0; public static synchronized void inc() { try { Thread.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } count++; } public static void main(String[] args) throws InterruptedException { for (int i = 0; i < 1000; i++) { new Thread(() -> Demo.inc()).start(); } Thread.sleep(3000); System.out.println("運行結果" + count); } }
通過javap -v 來查看對應代碼的字節碼指令:
又看到了熟悉的東西:ACC_SYNCHRONIZED。對於同步塊的實現使用了monitorenter和monitorexit指令:他們隱式的執行了Lock和UnLock操作,用於提供原子性保證。monitorenter指令插入到同步代碼塊開始的位置、monitorexit指令插入到同步代碼塊結束位置,jvm需要保證每個monitorenter都有一個monitorexit對應。這兩個指令,本質上都是對一個對象的監視器(monitor)進行獲取,這個過程是排他的,也就是說同一時刻只能有一個線程獲取到由synchronized所保護對象的監視器線程執行到monitorenter指令時,會嘗試獲取對象所對應的monitor所有權,也就是嘗試獲取對象的鎖;而執行monitorexit,就是釋放monitor的所有權。
synchronized的鎖的原理:
jdk1.6以后對synchronized鎖進行了優化,包含偏向鎖、輕量級鎖、重量級鎖;了解synchronized的原理我們需要明白3個問題:
1.synchronized是如何實現鎖
2.為什么任何一個對象都可以成為鎖
3.鎖存在哪個地方?
在了解synchronized鎖之前,我們需要了解兩個重要的概念,一個是對象頭、另一個是monitor。
Java對象頭:
在Hotspot虛擬機中,對象在內存中的布局分為三塊區域:對象頭、實例數據和對齊填充;Java對象頭是實現synchronized的鎖對象的基礎,一般而言,synchronized使用的鎖對象是存儲在Java對象頭里。它是輕量級鎖和偏向鎖的關鍵
Mawrk Word:
Mark Word用於存儲對象自身的運行時數據,如哈希碼(HashCode)、GC分代年齡、鎖狀態標志、線程持有的鎖、偏向線程 ID、偏向時間戳等等。Java對象頭一般占有兩個機器碼(在32位虛擬機中,1個機器碼等於4字節,也就是32bit),下面就是對象頭的一些信息:
在源碼中的體現:
如果想更深入了解對象頭在JVM源碼中的定義,需要關心幾個文件,oop.hpp/markOop.hpp 。
oop.hpp,每個 Java Object 在 JVM 內部都有一個 native 的 C++ 對象 oop/oopDesc 與之對應。先在oop.hpp中看oopDesc的定義:
class oopDesc { friend class VMStructs; private: volatile markOop _mark;//理解為對象頭 union _metadata { Klass* _klass; narrowKlass _compressed_klass; //默認開啟壓縮 } _metadata; ......
_mark 被聲明在 oopDesc 類的頂部,所以這個 _mark 可以認為是一個 頭部, 也就是上面那個圖種提到的頭部保存了一些重要的狀態和標識信息,在markOop.hpp文件中有一些注釋說明markOop的內存布局:
// Bit-format of an object header (most significant first, big endian layout below): // // 32 bits://對應的上圖的頭部信息的分布 // -------- // hash:25 ------------>| age:4 biased_lock:1 lock:2 (normal object) // JavaThread*:23 epoch:2 age:4 biased_lock:1 lock:2 (biased object) // size:32 ------------------------------------------>| (CMS free block) // PromotedObject*:29 ---------->| promo_bits:3 ----->| (CMS promoted object) // // 64 bits: // 64為虛擬機中的分布 // -------- // unused:25 hash:31 -->| unused:1 age:4 biased_lock:1 lock:2 (normal object) // JavaThread*:54 epoch:2 unused:1 age:4 biased_lock:1 lock:2 (biased object) // PromotedObject*:61 --------------------->| promo_bits:3 ----->| (CMS promoted object) // size:64 ----------------------------------------------------->| (CMS free block) // // unused:25 hash:31 -->| cms_free:1 age:4 biased_lock:1 lock:2 (COOPs && normal object) // JavaThread*:54 epoch:2 cms_free:1 age:4 biased_lock:1 lock:2 (COOPs && biased object) // narrowOop:32 unused:24 cms_free:1 unused:4 promo_bits:3 ----->| (COOPs && CMS promoted object) // unused:21 size:35 -->| cms_free:1 unused:7 ------------------>| (COOPs && CMS free block) //
Java 中提供了一個jar包給我來查看對象頭信息:
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.10</version>
</dependency>
然后編寫一個測試類:
// oop.hpp 源碼-XX:-UseCompressedOops 關閉壓縮指針 //打開偏向鎖 -XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=0 public static void main(String[] args) { ClassLayoutDemo classLayoutDemo = new ClassLayoutDemo(); synchronized (classLayoutDemo) { System.out.println(ClassLayout.parseInstance(classLayoutDemo).toPrintable()); } }
默認情況下輸出以下信息:
這里涉及到大端存儲跟小端存儲的概念:大端存儲與小端存儲模式主要指的是數據在計算機中存儲的兩種字節優先順序。小端存儲指從內存的低地址開始,先存儲數據的低序字節再存高序字節;相反,大端存儲指從內存的高地址開始,先存儲數據的高序字節再存儲數據的低序字節。這里正是用這種規則來存儲的。
可以看到這里的是經過壓縮的,因為上面中提到 JVM虛擬機中 _compressed_klass 默認開啟壓縮。可以通過 -XX:-UseCompressedOops 關閉壓縮指針。然后打印出來的對象頭就是128位的。
在上面JVM源碼中對對象頭的描述中,在64位虛擬機的布局中,最后的兩位標識鎖的類型,倒數第三位標識是否獲得偏向鎖,因為JDK1.8中默認是關閉偏向鎖的 ,這里看到的是輕量級鎖的標識,要知道,偏向鎖是用來在沒有線程競爭的時候減少性能開銷通過自旋獲得鎖,但是實際的場景下還是存在線程的競爭的,所以默認是關閉的。但是我們可以通過 -XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=0 開啟偏向鎖,然后打印出來的數據就是這樣的:
因為我們這里沒有競爭,如果存在競爭,就會有不一樣的輸出 就比如最后3位為 010 ,表示重量級鎖
另外需要注意的是在偏向鎖下,對象頭內是沒有空間存儲對象的 哈希碼的,那么這個時候我們修改一下Demo
public static void main(String[] args) { ClassLayoutDemo classLayoutDemo = new ClassLayoutDemo(); synchronized (classLayoutDemo) { System.out.println(classLayoutDemo.hashCode()); System.out.println(ClassLayout.parseInstance(classLayoutDemo).toPrintable()); } }
這個時候輸出的信息鎖的類型變成了重量級鎖了。
Monitor:
什么是Monitor?我們可以把它理解為一個同步工具,也可以描述為一種同步機制。所有的Java對象是天生的Monitor,每個object的對象里 markOop->monitor() 里可以保存ObjectMonitor的對象。從源碼層面看一下monitor對象
Ø oop.hpp下的oopDesc類是JVM對象的頂級基類,所以每個object對象都包含markOop
class oopDesc {//頂層基類 friend class VMStructs; private: volatile markOop _mark;//這也就是每個對象的mark頭 union _metadata { Klass* _klass; narrowKlass _compressed_klass; } _metadata;
Ø markOop.hpp 中 markOopDesc繼承自oopDesc,
並擴展了自己的monitor方法,這個方法返回一個ObjectMonitor指針對象:這個ObjectMonitor 其實就是對象監視器
Ø objectMonitor.hpp,在hotspot虛擬機中,采用ObjectMonitor類來實現monitor:
到目前位置,對於鎖存在哪個位置,我們已經清楚了,鎖存在於每個對象的 markOop 對象頭中.對於為什么每個對象都可以成為鎖呢? 因為每個 Java Object 在 JVM 內部都有一個 native 的 C++ 對象 oop/oopDesc 與之對應,而對應的 oop/oopDesc 都會存在一個markOop 對象頭,而這個對象頭是存儲鎖的位置,里面還有對象監視器,即ObjectMonitor,所以這也是為什么每個對象都能成為鎖的原因之一。那么 synchronized是如何實現鎖的呢?
synchronized是如何實現鎖:
了解了對象頭以及monitor以后,接下來去分析synchronized的鎖的實現,就會相對簡單了。前面講過synchronized的鎖是進行過優化的,引入了偏向鎖、輕量級鎖;鎖的級別從低到高逐步升級, 無鎖->偏向鎖->輕量級鎖->重量級鎖.鎖的類型:鎖從宏觀上分類,分為悲觀鎖與樂觀鎖。
樂觀鎖:
樂觀鎖是一種樂觀思想,即認為讀多寫少,遇到並發寫的可能性低,每次去拿數據的時候都認為別人不會修改,所以不會上鎖,但是在更新的時候會判斷一下在此期間別人有沒有去更新這個數據,采取在寫時先讀出當前版本號,然后加鎖操作(比較跟上一次的版本號,如果一樣則更新),如果失敗則要重復讀-比較-寫的操作。java中的樂觀鎖基本都是通過CAS操作實現的,CAS是一種更新的原子操作,比較當前值跟傳入值是否一樣,一樣則更新,否則失敗。
悲觀鎖:
悲觀鎖是就是悲觀思想,即認為寫多,遇到並發寫的可能性高,每次去拿數據的時候都認為別人會修改,所以每次在讀寫數據的時候都會上鎖,這樣別人想讀寫這個數據就會block直到拿到鎖。java中的悲觀鎖就是Synchronized,AQS框架下的鎖則是先嘗試cas樂觀鎖去獲取鎖,獲取不到,才會轉換為悲觀鎖,如RetreenLock。
自旋鎖(CAS):
自旋鎖就是讓不滿足條件的線程等待一段時間,而不是立即掛起。看持有鎖的線程是否能夠很快釋放鎖。怎么自旋呢?其實就是一段沒有任何意義的循環。雖然它通過占用處理器的時間來避免線程切換帶來的開銷,但是如果持有鎖的線程不能在很快釋放鎖,那么自旋的線程就會浪費處理器的資源,因為它不會做任何有意義的工作。所以,自旋等待的時間或者次數是有一個限度的,如果自旋超過了定義的時間仍然沒有獲取到鎖,則該線程應該被掛起。JDK1.6中-XX:+UseSpinning開啟; -XX:PreBlockSpin=10 為自旋次數; JDK1.7后,去掉此參數,由jvm控制;
偏向鎖:
大多數情況下,鎖不僅不存在多線程競爭,而且總是由同一線程多次獲得,為了讓線程獲得鎖的代價更低而引入了偏向鎖。下圖就是偏向鎖的獲得跟撤銷流程圖:
當一個線程訪問同步塊並獲取鎖時,會在對象頭和棧幀中的鎖記錄里存儲鎖偏向的線程ID,以后該線程在進入和退出同步塊時不需要進行CAS操作來加鎖和解鎖,只需簡單地測試一下對象頭的Mark Word里是否存儲着指向當前線程的偏向鎖。如果測試成功,表示線程已經獲得了鎖。如果測試失敗,則需要再測試一下Mark Word中偏向鎖的標識是否設置成01(表示當前是偏向鎖):如果沒有設置,則使用CAS競爭鎖;如果設置了,則嘗試使用CAS將對象頭的偏向鎖指向當前線程。執行同步塊。這個時候線程2也來訪問同步塊,也是會檢查對象頭的Mark Word里是否存儲着當前線程2的偏向鎖,發現不是,那么他會進入 CAS 替換,但是此時會替換失敗,因為此時線程1已經替換了。替換失敗則會進入撤銷偏向鎖,首先會去暫停擁有了偏向鎖的線程1,進入無鎖狀態(01).偏向鎖存在競爭的情況下就回去升級成輕量級鎖。
開啟:-XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=0 -client -Xmx1024m -Xms1024m
關閉:-XX:+UseBiasedLocking -client -Xmx512m -Xms512m
輕量級鎖:
引入輕量級鎖的主要目的是在多沒有多線程競爭的前提下,減少傳統的重量級鎖使用操作系統互斥量產生的性能消耗。當關閉偏向鎖功能或者多個線程競爭偏向鎖導致偏向鎖升級為輕量級鎖,則會嘗試獲取輕量級鎖,下面是輕量級鎖的流程圖:
在代碼進入同步塊的時候,如果同步對象鎖狀態為無鎖狀態(鎖標志位為“01”狀態),虛擬機首先將在當前線程的棧幀中建立一個名為鎖記錄(Lock Record)的空間,用於存儲鎖對象目前的Mark Word的拷貝,官方稱之為 Displaced Mark Word。這個時候 JVM會嘗試使用 CAS 將 mark Word 更新為指向棧幀中的鎖記錄(Lock Record)的空間指針。並且把鎖標志位設置為 00(輕量級鎖標志),與此同時如果有另外一個線程2也來進行 CAS 修改 Mark Word,那么將會失敗,因為線程1已經獲取到該鎖,然后線程2將會進行 CAS操作不斷的去嘗試獲取鎖,這個時候將會引起鎖膨脹,就會升級為重量級鎖,設置標志位為 10.
由輕量鎖切換到重量鎖,是發生在輕量鎖釋放鎖的期間,之前在獲取鎖的時候它拷貝了鎖對象頭的markword,在釋放鎖的時候如果它發現在它持有鎖的期間有其他線程來嘗試獲取鎖了,並且該線程對markword做了修改,兩者比對發現不一致,則切換到重量鎖。輕量級解鎖時,會使用原子的CAS操作來將Displaced Mark Word替換回到對象頭,如果成功,則表示同步過程已完成。如果失敗,表示有其他線程嘗試過獲取該鎖,則要在釋放鎖的同時喚醒被掛起的線程進入等待。
重量級鎖:
重量級鎖通過對象內部的監視器(monitor)實現,其中monitor的本質是依賴於底層操作系統的Mutex Lock實現,操作系統實現線程之間的切換需要從用戶態到內核態的切換,切換成本非常高。主要是,當系統檢查到鎖是重量級鎖之后,會把等待想要獲得鎖的線程進行阻塞,被阻塞的線程不會消耗cup。但是阻塞或者喚醒一個線程時,都需要操作系統來幫忙,這就需要從用戶態轉換到內核態,而轉換狀態是需要消耗很多時間的,有可能比用戶執行代碼的時間還要長。這就是說為什么重量級線程開銷很大的。
monitor這個對象,在hotspot虛擬機中,通過ObjectMonitor類來實現 monitor。他的鎖的獲取過程的體現會簡單很多。每個object的對象里 markOop->monitor() 里可以保存ObjectMonitor的對象。
這里提到的 CXQ跟 EnterList 是什么呢? 見下圖:
這里我們重新回到 objectMonitor.cpp 這個源碼中來看以下:
void ATTR ObjectMonitor::enter(TRAPS) {//獲取重量級鎖的過程 // The following code is ordered to check the most common cases first // and to reduce RTS->RTO cache line upgrades on SPARC and IA32 processors. Thread * const Self = THREAD ; void * cur ; cur = Atomic::cmpxchg_ptr (Self, &_owner, NULL) ;//進行CAS自旋操作 if (cur == NULL) { // Either ASSERT _recursions == 0 or explicitly set _recursions = 0. assert (_recursions == 0 , "invariant") ; assert (_owner == Self, "invariant") ; // CONSIDER: set or assert OwnerIsThread == 1 return ; } //自旋結果相等,則重入(重入的原理) if (cur == Self) { // TODO-FIXME: check for integer overflow! BUGID 6557169. _recursions ++ ; return ; }
//接下去就是有並發的情況下競爭的過程了 ....
所以這就是synchronized實現鎖的一個過程。
wait和notify的原理:
調用wait方法,首先會獲取監視器鎖,獲得成功以后,會讓當前線程進入等待狀態進入等待隊列並且釋放鎖;然后當其他線程調用notify或者notifyall以后,會通知等待線程可以醒了,而執行完notify方法以后,並不會立馬喚醒線程,原因是當前的線程仍然持有這把鎖,處於等待狀態的線程無法獲得鎖。必須要等到當前的線程執行完按monitorexit指令以后,也就是鎖被釋放以后,處於等待隊列中的線程就可以開始競爭鎖了。
看一下 JVM 源碼中的邏輯,在objectMonitor.cpp 中:在我們Java代碼層面調用的 wait() 方法后,其實在 JVM 層面所作的是,封裝 ObjectWaiter 對象並將其放入 _WaitSet 隊列,並調用 park()將線程掛起。
// Wait/Notify/NotifyAll // Note: a subset of changes to ObjectMonitor::wait() // will need to be replicated in complete_exit above void ObjectMonitor::wait(jlong millis, bool interruptible, TRAPS) { Thread * const Self = THREAD ; assert(Self->is_Java_thread(), "Must be Java thread!"); JavaThread *jt = (JavaThread *)THREAD; DeferredInitialize () ; // Throw IMSX or IEX. CHECK_OWNER();//檢查objectMonitor對象是否指向本線程(即是否獲得鎖) // ... 省略中間的代碼
// create a node to be put into the queue // Critically, after we reset() the event but prior to park(), we must check // for a pending interrupt. // 封裝了一個ObjectWaiter對象 ObjectWaiter node(Self); node.TState = ObjectWaiter::TS_WAIT ; Self->_ParkEvent->reset() ; OrderAccess::fence();//內存屏障 // ST into Event; membar ; LD interrupted-flag // Enter the waiting queue, which is a circular doubly linked list in this case // but it could be a priority queue or any data structure. // _WaitSetLock protects the wait queue. Normally the wait queue is accessed only // by the the owner of the monitor *except* in the case where park() // returns because of a timeout of interrupt. Contention is exceptionally rare // so we use a simple spin-lock instead of a heavier-weight blocking lock. //將ObjectWaiter放入 _WaitSet中 Thread::SpinAcquire (&_WaitSetLock, "WaitSet - add") ; AddWaiter (&node) ; Thread::SpinRelease (&_WaitSetLock) ; if ((SyncFlags & 4) == 0) { _Responsible = NULL ; } intptr_t save = _recursions; // record the old recursion count _waiters++; // increment the number of waiters _recursions = 0; // set the recursion level to be 1 exit (true, Self) ; // exit the monitor guarantee (_owner != Self, "invariant") ; //.....省略中間代碼 // The thread is on the WaitSet list - now park() it. // On MP systems it's conceivable that a brief spin before we park // could be profitable. // TODO-FIXME: change the following logic to a loop of the form // while (!timeout && !interrupted && _notified == 0) park() int ret = OS_OK ; int WasNotified = 0 ; { // State transition wrappers OSThread* osthread = Self->osthread(); OSThreadWaitState osts(osthread, true); { ThreadBlockInVM tbivm(jt); // Thread is in thread_blocked state and oop access is unsafe. jt->set_suspend_equivalent(); if (interruptible && (Thread::is_interrupted(THREAD, false) || HAS_PENDING_EXCEPTION)) { // Intentionally empty } else if (node._notified == 0) { if (millis <= 0) { // 調用park()將線程掛起 Self->_ParkEvent->park () ; } else { ret = Self->_ParkEvent->park (millis) ; } } ....... }
接下去看看 notify 的操作:
void ObjectMonitor::notify(TRAPS) { CHECK_OWNER();//同樣先檢查objectMonitor對象是否指向本線程 if (_WaitSet == NULL) {//判斷wait隊列是否為空 TEVENT (Empty-Notify) ; return ; } DTRACE_MONITOR_PROBE(notify, this, object(), THREAD); int Policy = Knob_MoveNotifyee ; // 這個 WaitSet - notify 很了然 Thread::SpinAcquire (&_WaitSetLock, "WaitSet - notify") ; ObjectWaiter * iterator = DequeueWaiter() ;//dequeue _WaitSet 隊列 if (iterator != NULL) {//不為空,然后接下去就是一系列的判斷,最后去喚醒 //....... } }
wait和notify為什么需要在synchronized里面:
wait方法的語義有兩個,一個是釋放當前的對象鎖、另一個是使得當前線程進入阻塞隊列, 而這些操作都和監視器是相關的,所以wait必須要獲得一個監視器鎖。
而對於notify來說也是一樣,它是喚醒一個線程,既然要去喚醒,首先得知道它在哪里?所以就必須要找到這個對象獲取到這個對象的鎖,然后到這個對象的等待隊列中去喚醒一個線程。