synchronized原理

  在多線程併發編程中synchronized一直是元老級角色,咱們在開發過程當中可使用它來解決線程安全問題中提到的原子性,可見性,以及順序性。不少人都會稱呼它爲重量級鎖。可是,隨着Java SE 1.6對synchronized進行了各類優化以後,有些狀況下它就並不那麼重了,Java SE 1.6中爲了減小得到鎖和釋放鎖帶來的性能消耗而引入的偏向鎖和輕量級鎖,以及鎖的存儲結構和升級過程。java

synchronized的三種應用方式:

  synchronized有三種方式來加鎖,分別是:方法鎖,對象鎖synchronized(this),類鎖synchronized(Demo.Class)。其中在方法鎖層面能夠有以下3種方式:node

1. 修飾實例方法,做用於當前實例加鎖,進入同步代碼前要得到當前實例的鎖編程

2. 靜態方法,做用於當前類對象加鎖,進入同步代碼前要得到當前類對象的鎖安全

3. 修飾代碼塊,指定加鎖對象,對給定對象加鎖,進入同步代碼庫前要得到給定對象的鎖。多線程

synchronized括號後面的對象:併發

  synchronized擴號後面的對象是一把鎖,在java中任意一個對象均可以成爲鎖,簡單來講,咱們把object比喻是一個key,擁有這個key的線程才能執行這個方法,拿到這個key之後在執行方法過程當中,這個key是隨身攜帶的,而且只有一把。若是後續的線程想訪問當前方法,由於沒有key因此不能訪問只能在門口等着,等以前的線程把key放回去。因此,synchronized鎖定的對象必須是同一個,若是是不一樣對象,就意味着是不一樣的房間的鑰匙,對於訪問者來講是沒有任何影響的。app

synchronized的字節碼指令:框架

  先看 demo 程序:jvm

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)
//

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來講也是同樣,它是喚醒一個線程,既然要去喚醒,首先得知道它在哪裏?因此就必需要找到這個對象獲取到這個對象的鎖,而後到這個對象的等待隊列中去喚醒一個線程。

相關文章
相關標籤/搜索