synchronized面試題

一 什麼會需要synchronized?什麼場景下使用synchronized?

在這裏插入圖片描述

如上圖所示,比如在王者榮耀程序中,我們隊有二個線程分別統計後裔和安琪拉的經濟,A線程從內存中read 當前隊伍總經濟加載到線程的本地棧,進行 +100 操作之後,這時候B線程也從內存中取出經濟值 + 200,將200寫回內存,B線程剛執行完,後腳A線程將100 寫回到內存中,就出問題了,我們隊的經濟應該是300, 但是內存中存的卻是100。
 

1. synchronized 怎麼解決這個問題的?

在訪問競態資源時加鎖,因爲多個線程會修改經濟值,因此經濟值就是競態資源,給您show 一下吧?下圖是不加鎖的代碼以及控制檯的輸出,請您過目:

二個線程,A線程讓隊伍經濟 +1 ,B線程讓經濟 + 2,分別執行一千次,正確的結果應該是3000,結果得到的卻是 2845。

在這裏插入圖片描述

 

這個就是加鎖之後的代碼和控制檯的輸出。

(img-6NwdhDEz-1585279691724)(/Users/zw/Library/Application Support/typora-user-images/image-20200321210555529.png)]

 

二 synchronized 還有別的作用範圍嗎?

  1. 在靜態方法上加鎖;

  2. 在非靜態方法上加鎖;

  3. 在代碼塊上加鎖;

public class SynchronizedSample {

    private final Object lock = new Object();

    private static int money = 0;
		//非靜態方法
    public synchronized void noStaticMethod(){
        money++;
    }
		//靜態方法
    public static synchronized void staticMethod(){
        money++;
    }
		
    public void codeBlock(){
      	//代碼塊
        synchronized (lock){
            money++;
        }
    }
}

鎖是加在對象上面的,我們是在對象上加鎖。

重要事情說三遍:在對象上加鎖 ✖️ 3 (這也是爲什麼wait / notify 需要在鎖定對象後執行,只有先拿到鎖才能釋放鎖)

這三種作用範圍的區別實際是被加鎖的對象的區別,請看下錶:

 

三 JVM 是怎麼通過synchronized 在對象上實現加鎖,保證多線程訪問競態資源安全的嗎?

1.在JDK6 以前synchronized實現邏輯

synchronized 那時還屬於重量級鎖,相當於關二爺手中的青龍偃月刀,每次加鎖都依賴操作系統Mutex Lock實現,涉及到操作系統讓線程從用戶態切換到內核態,切換成本很高;

①JDK 6 以前 synchronized爲什麼這麼重? JDK6 之後的偏向鎖和輕量級鎖是怎麼回事?

(1) Java 對象頭,鎖的類型和狀態和對象頭的Mark Word息息相關;

在這裏插入圖片描述

對象存儲在堆中,主要分爲三部分內容,對象頭、對象實例數據和對齊填充(數組對象多一個區域:記錄數組長度),下面簡單說一下三部分內容,雖然 synchronized 只與對象頭中的 Mard Word相關。

a.對象頭:

對象頭分爲二個部分,Mard Word 和 Klass Word

b.對象實例數據

類中的 成員變量data 就屬於對象實例數據;

c.對齊填充:

JVM要求對象佔用的空間必須是8 的倍數,方便內存分配(以字節爲最小單位分配),因此這部分就是用於填滿不夠的空間湊數用的。

(2)每個對象都有一個與之關聯的Monitor 對象;Monitor對象屬性如下所示( Hospot 1.7 代碼) 。

//詳細介紹重要變量的作用
ObjectMonitor() {
    _header       = NULL;
    _count        = 0;   // 重入次數
    _waiters      = 0,   // 等待線程數
    _recursions   = 0;
    _object       = NULL;
    _owner        = NULL;  // 當前持有鎖的線程
    _WaitSet      = NULL;  // 調用了 wait 方法的線程被阻塞 放置在這裏
    _WaitSetLock  = 0 ;
    _Responsible  = NULL ;
    _succ         = NULL ;
    _cxq          = NULL ;
    FreeNext      = NULL ;
    _EntryList    = NULL ; // 等待鎖 處於block的線程 有資格成爲候選資源的線程
    _SpinFreq     = 0 ;
    _SpinClock    = 0 ;
    OwnerIsThread = 0 ;
  }



對象關聯的 ObjectMonitor 對象有一個線程內部競爭鎖的機制,如下圖所示:

在這裏插入圖片描述

 


2.JDK6之後

研究人員引入了偏向鎖和輕量級鎖,因爲Sun 程序員發現大部分程序大多數時間都不會發生多個線程同時訪問競態資源的情況,每次線程都加鎖解鎖,每次這麼搞都要操作系統在用戶態和內核態之間來回切,太耗性能了。

實現原理

1.當有二個線程A、線程B都要開始給我們隊的經濟 money變量 + 錢,要進行操作的時候 ,發現方法上加了synchronized鎖,這時線程調度到A線程執行,A線程就搶先拿到了鎖。拿到鎖的步驟爲:
- 1.1 將 MonitorObject 中的 _owner設置成 A線程;
- 1.2 將 mark word 設置爲 Monitor 對象地址,鎖標誌位改爲10;
- 1.3 將B 線程阻塞放到 ContentionList 隊列;

2.JVM 每次從Waiting Queue 的尾部取出一個線程放到OnDeck作爲候選者,但是如果併發比較高,Waiting Queue會被大量線程執行CAS操作,爲了降低對尾部元素的競爭,將Waiting Queue 拆分成ContentionList 和 EntryList 二個隊列, JVM將一部分線程移到EntryList 作爲準備進OnDeck的預備線程。另外說明幾點:

     1.所有請求鎖的線程首先被放在ContentionList這個競爭隊列中;

     2.Contention List 中那些有資格成爲候選資源的線程被移動到 Entry List 中;

     3.任意時刻,最多隻有一個線程正在競爭鎖資源,該線程被成爲 OnDeck;

     4.當前已經獲取到所資源的線程被稱爲 Owner;

     5.處於 ContentionList、EntryList、WaitSet 中的線程都處於阻塞狀態,該阻塞是由操作系統來完成的(Linux 內核下采用 pthread_mutex_lock 內核函數實現的);

3..作爲Owner 的A 線程執行過程中,可能調用wait 釋放鎖,這個時候A線程進入 Wait Set , 等待被喚醒。

改進

 synchronized 重量級鎖有以下二個問題, 因此JDK 6 之後做了改進,引入了偏向鎖和輕量級鎖:

1.依賴底層操作系統的 mutex 相關指令實現,加鎖解鎖需要在用戶態和內核態之間切換,性能損耗非常明顯。

2.研究人員發現,大多數對象的加鎖和解鎖都是在特定的線程中完成。也就是出現線程競爭鎖的情況概率比較低。他們做了一個實驗,找了一些典型的軟件,測試同一個線程加鎖解鎖的重複率,如下圖所示,可以看到重複加鎖比例非常高。早期JVM 有 19% 的執行時間浪費在鎖上。
 

在這裏插入圖片描述 

 

JDK 6 以來 synchronized 鎖狀態怎麼從無鎖狀態到偏向鎖的?

下圖對象從無鎖到偏向鎖轉化的過程(JVM -XX:+UseBiasedLocking 開啓偏向鎖):

 在這裏插入圖片描述

1.首先A 線程訪問同步代碼塊,使用CAS 操作將 Thread ID 放到 Mark Word 當中;
2.如果CAS 成功,此時線程A 就獲取了鎖
3.如果線程CAS 失敗,證明有別的線程持有鎖,例如上圖的線程B 來CAS 就失敗的,這個時候啓動偏向鎖撤銷 (revoke bias);
4.鎖撤銷流程:
- 讓 A線程在全局安全點阻塞(類似於GC前線程在安全點阻塞)
- 遍歷線程棧,查看是否有被鎖對象的鎖記錄( Lock Record),如果有Lock Record,需要修復鎖記錄和Markword,使其變成無鎖狀態。
- 恢復A線程
- 將是否爲偏向鎖狀態置爲 0 ,開始進行輕量級加鎖流程 (後面講述)
下圖說明了 Mark Word 在這個過程中的轉化在這裏插入圖片描述

 

四 synchronized 是公平鎖還是非公平鎖

非公平的

1.Synchronized 在線程競爭鎖時,首先做的不是直接進ContentionList 隊列排隊,而是嘗試自旋獲取鎖(可能ContentionList 有別的線程在等鎖),如果獲取不到才進入 ContentionList,這明顯對於已經進入隊列的線程是不公平的;
2.另一個不公平的是自旋獲取鎖的線程還可能直接搶佔 OnDeck 線程的鎖資源。
 

 

五 偏向鎖撤銷怎麼到輕量級鎖的? 還有輕量級鎖什麼時候會變成重量級鎖?

 

鎖撤銷之後(偏向鎖狀態爲0),現在無論是A線程還是B線程執行到同步代碼塊進行加鎖,流程如下:

  • 1.線程在自己的棧楨中創建鎖記錄 LockRecord。
  • 2.線程A 將 Mark Word 拷貝到線程棧的 Lock Record中,這個位置叫 displayced hdr,如下圖所示:

圖A 無鎖 -> 加鎖

  • 3.將鎖記錄中的Owner指針指向加鎖的對象(存放對象地址)。
  • 4.將鎖對象的對象頭的MarkWord替換爲指向鎖記錄的指針。這二步如下圖所示:

在這裏插入圖片描述

        5.這時鎖標誌位變成 00 ,表示輕量級鎖

 

 

 

六 輕量級鎖什麼時候會升級爲重量級鎖

 

當鎖升級爲輕量級鎖之後,如果依然有新線程過來競爭鎖,首先新線程會自旋嘗試獲取鎖,嘗試到一定次數(默認10次)依然沒有拿到,鎖就會升級成重量級鎖。

一般來說,同步代碼塊內的代碼應該很快就執行結束,這時候線程B 自旋一段時間是很容易拿到鎖的,但是如果不巧,沒拿到,自旋其實就是死循環,很耗CPU的,因此就直接轉成重量級鎖咯,這樣就不用了線程一直自旋了。
這就是鎖膨脹的過程,下圖是Mark Word 和鎖狀態的轉化圖

在這裏插入圖片描述

鎖當前爲可偏向狀態,偏向鎖狀態位置就是1,看到很多網上的文章都寫錯了,把這裏寫成只有鎖發生偏向纔會置爲1,一定要注意。

 

 

七 偏向鎖有撤銷,還會膨脹,性能損耗這麼大,還需要用他們呢?

如果確定競態資源會被高併發的訪問,建議通過-XX:-UseBiasedLocking 參數關閉偏向鎖,偏向鎖的好處是併發度很低的情況下,同一個線程獲取鎖不需要內存拷貝的操作,免去了輕量級鎖的在線程棧中建Lock Record,拷貝Mark Down的內容,也免了重量級鎖的底層操作系統用戶態到內核態的切換,因爲前面說了,需要使用系統指令。另外Hotspot 也做了另一項優化,基於鎖對象的epoch 批量偏向和批量撤銷偏向,這樣可以大大降低了單次偏向鎖的CAS和鎖撤銷帶來的損耗,👇圖是研究人員做的壓測:
在這裏插入圖片描述

他們在幾款典型軟件上做了測試,發現基於epoch 批量撤銷偏向鎖和批量加偏向鎖能大幅提升吞吐量,但是併發量特別大的時候性能就沒有什麼特別大的提升了。 

八  synchronized 底層實現源碼

public class SynchronizedSample {

    private final Object lock = new Object();

    private static int money = 0;
		//非靜態方法
    public synchronized void noStaticMethod(){
        money++;
    }
		//靜態方法
    public static synchronized void staticMethod(){
        money++;
    }
		
    public void codeBlock(){
      	//代碼塊
        synchronized (lock){
            money++;
        }
    }
}

示例代碼編譯成class 文件,然後通過javap -v SynchronizedSample.class 來看下synchronized 到底在源碼層面如何實現的?

如下圖所示:

在這裏插入圖片描述

synchronized 在代碼塊上是通過 monitorenter 和 monitorexit指令實現,在靜態方法和 方法上加鎖是在方法的flags 中加入 ACC_SYNCHRONIZED 。JVM 運行方法時檢查方法的flags,遇到同步標識開始啓動前面的加鎖流程,在方法內部遇到monitorenter指令開始加鎖。

monitorenter 指令函數源代碼在 InterpreterRuntime::monitorenter中
 

IRT_ENTRY_NO_ASYNC(void, InterpreterRuntime::monitorenter(JavaThread* thread, BasicObjectLock* elem))
#ifdef ASSERT
  thread->last_frame().interpreter_frame_verify_monitor(elem);
#endif
  if (PrintBiasedLockingStatistics) {
    Atomic::inc(BiasedLocking::slow_path_entry_count_addr());
  }
  Handle h_obj(thread, elem->obj());
  assert(Universe::heap()->is_in_reserved_or_null(h_obj()),
         "must be NULL or an object");
 //是否開啓了偏向鎖
  if (UseBiasedLocking) {
    // 嘗試偏向鎖
    ObjectSynchronizer::fast_enter(h_obj, elem->lock(), true, CHECK);
  } else {
    // 輕量鎖邏輯
    ObjectSynchronizer::slow_enter(h_obj, elem->lock(), CHECK);
  }
  assert(Universe::heap()->is_in_reserved_or_null(elem->obj()),
         "must be NULL or an object");
#ifdef ASSERT
  thread->last_frame().interpreter_frame_verify_monitor(elem);
#endif
IRT_END

 

 

偏向鎖代碼

// -----------------------------------------------------------------------------
//  Fast Monitor Enter/Exit
// This the fast monitor enter. The interpreter and compiler use
// some assembly copies of this code. Make sure update those code
// if the following function is changed. The implementation is
// extremely sensitive to race condition. Be careful.

void ObjectSynchronizer::fast_enter(Handle obj, BasicLock* lock, bool attempt_rebias, TRAPS) {
//是否使用偏向鎖
 if (UseBiasedLocking) {
    // 如果不在全局安全點
    if (!SafepointSynchronize::is_at_safepoint()) {
      // 獲取偏向鎖
      BiasedLocking::Condition cond = BiasedLocking::revoke_and_rebias(obj, attempt_rebias, THREAD);
      if (cond == BiasedLocking::BIAS_REVOKED_AND_REBIASED) {
        return;
      }
    } else {
      assert(!attempt_rebias, "can not rebias toward VM thread");
      // 在全局安全點,撤銷偏向鎖
      BiasedLocking::revoke_at_safepoint(obj);
    }
    assert(!obj->mark()->has_bias_pattern(), "biases should be revoked by now");
 }
// 進輕量級鎖流程
 slow_enter (obj, lock, THREAD) ;
}

 

 輕量級鎖代碼流程

void ObjectSynchronizer::slow_enter(Handle obj, BasicLock* lock, TRAPS) {
//獲取對象的markOop數據mark
  markOop mark = obj->mark();
  assert(!mark->has_bias_pattern(), "should not see bias pattern here");

//判斷mark是否爲無鎖狀態 & 不可偏向(鎖標識爲01,偏向鎖標誌位爲0) 
  if (mark->is_neutral()) {
    // Anticipate successful CAS -- the ST of the displaced mark must
    // be visible <= the ST performed by the CAS.
    // 保存Mark 到 線程棧 Lock Record 的displaced_header中
    lock->set_displaced_header(mark);
    // CAS 將  Mark Down 更新爲 指向 lock 對象的指針,成功則獲取到鎖  
    if (mark == (markOop) Atomic::cmpxchg_ptr(lock, obj()->mark_addr(), mark)) {
      TEVENT (slow_enter: release stacklock) ;
      return ;
    }
    // Fall through to inflate() ...
  } else
  // 根據對象mark 判斷已經有鎖  & mark 中指針指的當前線程的Lock Record(當前線程已經獲取到了,不必重試獲取)
  if (mark->has_locker() && THREAD->is_lock_owned((address)mark->locker())) {
    assert(lock != mark->locker(), "must not re-lock the same lock");
    assert(lock != (BasicLock*)obj->mark(), "don't relock with same BasicLock");
    lock->set_displaced_header(NULL);
    return;
  }

 lock->set_displaced_header(markOopDesc::unused_mark());
   // 鎖膨脹
  ObjectSynchronizer::inflate(THREAD, obj())->enter(THREAD);

 

做個假設,現在線程A 和B 同時執行到臨界區if (mark->is_neutral()):
1、線程A和B都把Mark Word複製到各自的_displaced_header字段,該數據保存在線程的棧幀上,是線程私有的;
2、Atomic::cmpxchg_ptr 屬於原子操作,保障了只有一個線程可以把Mark Word中替換成指向自己線程棧 displaced_header中的,假設A線程執行成功,相當於A獲取到了鎖,開始繼續執行同步代碼塊;
3、線程B執行失敗,退出臨界區,通過ObjectSynchronizer::inflate方法開始膨脹鎖;
 

 

九 Java中除了synchronized 還有別的鎖嗎?

還有ReentrantLock也可以實現加鎖。

那寫段代碼實現之前加經濟的同樣效果。coding 如👇圖:

在這裏插入圖片描述

 

 

 

十 Mark Word 存儲結構

如下圖和源代碼註釋(以32位JVM爲例,後面的討論都基於32位JVM的背景,64位會特殊說明)。
Mard Word會在不同的鎖狀態下,32位指定區域都有不同的含義,這個是爲了節省存儲空間,用4 字節就表達了完整的狀態信息,當然,對象某一時刻只會是下面5 種狀態種的某一種。 

在這裏插入圖片描述

 

下面是簡化後的 Mark Word

在這裏插入圖片描述

 

hash: 保存對象的哈希碼
age: 保存對象的分代年齡
biased_lock: 偏向鎖標識位
lock: 鎖狀態標識位
JavaThread*: 保存持有偏向鎖的線程ID
epoch: 保存偏向時間戳

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章