當面試官懟你 synchronized 性能差時,你拿這篇文章吊打他(ReentrantLock 與 synchronized 的前世今生)

一天,你進入了一個大廠面試。坐立不安之中,一個禿頭中年男子,穿着一個發灰了的格子襯衫,戴着一副鏡片厚9mm的眼鏡,穩如磐石突然朝着你說到:“就是你這個小毛頭來面試吧。”

心裏一驚,這怕不是神仙級架構師。但還是故作鎮定:“面試官您好,我是xxx…”

面試過程中……面試官隨手拋來一句:“簡單說說 synchronized 關鍵字吧”。

簡單說說???嗯,面試官人還不錯。
再加上我面試前的精心準備,和飽讀詩書的才華,和揮斥方遒的瀟灑帥氣,一定能深深折服他。
於是:“ 這很簡單,

synchronized 關鍵字可以用在方法上,也可以用()括出一個對象作爲方法體,保證方法內部的代碼是線程安全的,是一種互斥鎖。”

面試官再問:

  • synchronized 是重量級鎖對嗎
  • sync 可重入嗎
  • sync 公平嗎
  • sync 鎖影響性能的原因
  • ReentrantLock 是重量級鎖對嗎
  • ReentrantLock 與 synchronized 有什麼不同知道嗎
  • 是不是多行程應該用輕量級鎖替換掉 synchronized

這都啥?????
“你回去等通知吧。”

<瑟瑟發抖>

下面進入正題,如果上面的題你都理解,能夠熟練回答,那對於 java 併發底層原理你瞭解得已經比較透徹了。
可能大部分情況程序員習慣於直接對方法加上 synchronized 關鍵字,來保證安全,但是由於對其底層不瞭解,往往會忽略性能。

首先聊聊 ReentrantLock 與 synchronized 的故事,你大概就能明白“鎖”之間的性能差異
1、sync 被吐槽

在 jdk1.6 版本之前。

  • sync 每次執行都會調用操作系統的鎖來保證線程安全
  • 也就是每次執行代碼塊,都要涉及 “用戶態” 到 “內核態” 的轉變
  • 所以 sync 就被廣大程序猿吐槽,龜速代碼

假設有一個方法

public void function() {
    System.out.println("Hello world!");
    // 各種亂起八遭的代碼
}

假設它現在在一臺服務器上運行平穩,老闆覺得很滿意。
但是有一天,業務發生了變化,有超多線程來訪問它,它變得線程不安全。
一個碼農便修改了這段代碼,加上了一個 synchronized 關鍵字,讓它線程安全,來防止被老闆炒魷魚。
但是這個方法,有時候會有多線程訪問,可大多數時候都是隻有一個線程,時不時觸碰一下它的底線。
然後由於 synchronized 的存在,每次運行都如同蝸牛爬行。
最後這位碼農就被炒了魷魚。

sun 公司於是接到了無數 java 流浪漢的投訴,說 synchronized 效率太低,導致代碼龜速爬行,自己被炒了魷魚。
在唾沫星子的淹沒之中,sun 公司發覺情況不妙,於是專門派人潛入市場查探情況。
據收集到的情報看,一段代碼

  • 它有時候會多線程執行
  • 但是大多數時候,都是單線程在執行

所以很多時候其實用不到鎖,但是 sync 還是會嚴重影響性能
所以程序猿們就希望有一種鎖,可以在沒有線程競爭的情況下,更加輕量。

2、ReentrantLock 橫空出世

在 jdk1.6 之前,由於 sync 性能堪憂
於是就有位同學看不慣,他就寫了一套技術,他叫

  • 道艮·李(Doug Lea)

他又寫了很多類,juc 包下的很多類都是他寫的
(juc 不知道的回去補基礎)
其中就有大名鼎鼎的 ReentrantLock

  • 速度就要比 sync 要快

加鎖這種事情,爲什麼要去麻煩底層的操作系統呢?
ReentrantLock 於是被他開發出來,去替代 sync 關鍵字
但是爲什麼沒有替代成功呢?

  • 因爲 sync 是 sun 公司親兒子

但是不得不承認 ReentrantLock 是非常優秀的
在現在看來,也和 sync 不相上下

  • ReentrantLock 快無非就是儘可能在 java 層面解決鎖
  • 而不是去勞煩操作系統

比如下面的非公平鎖的加鎖代碼

final void lock() {
    if (compareAndSetState(0, 1)) // CAS將0改爲1,可以一步加鎖成功
        setExclusiveOwnerThread(Thread.currentThread());
    else
        acquire(1);
}

(要是這段代碼看不懂的先去補補基礎吧)
在默認用非公平鎖的實現下
根據代碼可以發現在只有一個線程執行時

  • 進入 lock 方法,直接將標誌位從 0 替換爲 1,然後就能直接 lock 成功
  • 一步到位,期間不涉及任何操作系統的內核切換,在 java 層面就已經加鎖完成,性能比原來的 sync 蹭蹭蹭不知道快了多少倍

再來看看它的公平鎖實現部分代碼
(如果你比較懶,可以不用看,直接看我的解釋)

protected final boolean tryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    if (c == 0) { // 如果標誌位0,說明沒線程持有鎖
        if (!hasQueuedPredecessors() && // 如果沒有線程在排隊等鎖
            compareAndSetState(0, acquires)) { // CAS改標誌位
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    else if (current == getExclusiveOwnerThread()) {
        int nextc = c + acquires;
        if (nextc < 0)
            throw new Error("Maximum lock count exceeded");
        setState(nextc);
        return true;
    }
    return false;
}

公平鎖和非公平鎖的區別就是公平鎖多了兩個判斷

  • 判斷有沒有線程持有(標誌位是否爲0)
  • 有沒有線程在隊列等待

然後就 CAS 加鎖
如果是單線程的話就直接兩個 if,加一個 CAS 就加鎖成功了。
同樣

  • 一步到位,期間不涉及任何操作系統的內核切換,在 java 層面就已經加鎖完成,性能比原來的 sync 蹭蹭蹭不知道快了多少倍

道艮·李(Doug Lea)瞬間被人們封爲神壇(太牛了)

sync 優化

sync 被吐槽,ReentrantLock 被吹捧
這時候 sun 公司肯定坐不住了

  • 你要知道,sync 可是 sun 公司的親兒子
  • 是 sun 公司一手拉扯大的
  • 而現在半路出家的 ReentrantLock 被人吹捧,sync 被人丟棄

sun 公司想着這樣下去肯定不行,於是對 sync 進行了大量的優化。
於是 jdk1.7 之後,sync 的性能也得到了提升
sun 公司又開始重用它的親兒子

  • 後來在 jdk1.8 又將 ConcurrentHashMap 中原本採用 ReentrantLock 改成了 sync 關鍵字

sync 改動有這麼多
(以下內容來自於《深入理解 java 虛擬機》)

  1. 鎖消除
    比如
public String concatString(String s1,String s2,String s3){
    return s1+s2+s3; 
}
// 上面的代碼會被轉化爲下面的代碼
public String concatString(String s1,String s2,String s3){ 
	StringBuffer sb = new StringBuffer();
	sb.append(s1);
	sb.append(s2);
	sb.append(s3);
	return sb.toString();
}

StringBuffer 的方法是帶有 sync 的
(不知道的回去補課)
在執行時候,由於在這段代碼中不涉及線程安全
所以會自動優化爲
不加鎖!

  1. 鎖粗話(你看代碼就懂)
    (看不懂回去補課)
// 其實就是上面的代碼
public String concatString(String s1,String s2,String s3){ 
	StringBuffer sb = new StringBuffer();
	sb.append(s1);
	sb.append(s2);
	sb.append(s3);
	return sb.toString();
}

每一個 append 就是一個 sync
所以會被優化爲(看懂就好,只是爲了你理解)

sync
	public String concatString(String s1,String s2,String s3){ 
		StringBuffer sb = new StringBuffer();
		sb.append(s1); // 這裏的 sync 沒啦
		sb.append(s2); // 這裏的 sync 沒啦
		sb.append(s3); // 這裏的 sync 沒啦
		return sb.toString();
	}
sync
  1. 偏向鎖

偏向鎖的“偏”,就是偏心的“偏”、偏袒的“偏”,它的意思是這個鎖會偏向於第一個獲得 它的線程,如果在接下來的執行過程中,該鎖沒有被其他的線程獲取,則持有偏向鎖的線程 將永遠不需要再進行同步

當有另外一個線程去嘗試獲取這個鎖時,偏向模式就宣告結束

  1. 自旋鎖
    如果物理機能讓兩個或以上的線程並行執行,我們可以讓後面的線程 “稍等一會” ,暫不放棄處理器的執行時間,而是在一個次數較少的 while 循環中嘗試幾次加鎖,如果很快幾次之後加鎖成功了,就不用涉及到線程的掛起和恢復,只是稍微花了一點點 CPU。
    自旋次數默認是 10。
    如果一個線程自旋加鎖成功,JVM 會覺得這個線程下次自旋加鎖成功的概率也會很大,就會稍稍提升一下自旋次數
  2. 鎖膨脹
    如果輕量級加鎖無法實現,那就只好成爲重量級鎖了。

文章到這裏就結束啦,我還是大學生哦,喜歡學習的小夥伴可以評論交流,或者加關注,一起學習更輕鬆。
素質三連!

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