java多執行緒:synchronized和lock比較淺析

Love Lenka發表於2017-03-01

轉載:http://www.toutiao.com/a6392135944652587266/?tt_from=weixin&utm_campaign=client_share&app=news_article&utm_source=weixin&iid=7704173001&utm_medium=toutiao_ios&wxshare_count=1

 

 

synchronized是基於jvm底層實現的資料同步,lock是基於Java編寫,主要通過硬體依賴CPU指令實現資料同步。下面一一介紹

一、synchronized的實現方案

1.synchronized能夠把任何一個非null物件當成鎖,實現由兩種方式:

a.當synchronized作用於非靜態方法時,鎖住的是當前物件的事例,當synchronized作用於靜態方法時,鎖住的是class例項,又因為Class的相關資料儲存在永久帶,因此靜態方法鎖相當於類的一個全域性鎖。

b.當synchronized作用於一個物件例項時,鎖住的是對應的程式碼塊。

2.synchronized鎖又稱為物件監視器(object)。

3.當多個執行緒一起訪問某個物件監視器的時候,物件監視器會將這些請求儲存在不同的容器中。

>Contention List:競爭佇列,所有請求鎖的執行緒首先被放在這個競爭佇列中

>Entry List:Contention List中那些有資格成為候選資源的執行緒被移動到Entry List中

>Wait Set:哪些呼叫wait方法被阻塞的執行緒被放置在這裡

>OnDeck:任意時刻,最多隻有一個執行緒正在競爭鎖資源,該執行緒被成為OnDeck

>Owner:當前已經獲取到所資源的執行緒被稱為Owner

> !Owner:當前釋放鎖的執行緒

下圖展示了他們之前的關係

4.synchronized在jdk1.6之後提供了多種優化方案:

>自旋鎖

jdk1.6 之後預設開啟,可以使用引數-XX:+UseSpinning控制,自旋等待不能代替阻塞,且先不說對處理器數量的要求,自旋等待本身雖然避免了執行緒切換 的開銷,但它是要佔用處理器時間的,因此,如果鎖被佔用的時間很短,自旋等待的效果就會非常好,反之,如果鎖被佔用的時候很長,那麼自旋的執行緒只會白白消 耗處理器資源,而不會做任何有用的工作,反而會帶來效能上的浪費。自旋次數的預設值是 10 次,使用者可以使用引數 -XX:PreBlockSpin 來更改。

自旋鎖的本質:執行幾個空方法,稍微等一等,也許是一段時間的迴圈,也許是幾行空的彙編指令。

>鎖消除

即時編譯器在執行時,對一些程式碼上要求同步,但是被檢測到不可能存在共享資料競爭的鎖進行消除,依據來源於逃逸分析的資料支援,那麼是什麼是逃逸分析?對於虛擬機器來說需要使用資料流分析來確定是否消除變數底層框架的同步程式碼,因為有許多同步的程式碼不是自己寫的。

例1.1

public static String concatString(String s1, String s2, String s3) {  
    return s1 + s2 + s3;  
}  

由於 String 是一個不可變的類,對字串的連線操作總是通過生成新的 String 物件來進行的,因此 Javac 編譯器會對 String 連線做自動優化。在 JDK 1.5 之前,會轉化為 StringBuffer 物件的連續 append 操作,在 JDK 1.5 及以後的版本中,會轉化為 StringBuilder 物件的連續 append 操作,這裡的stringBuilder.append是執行緒不同步的(假設是同步)。

Javac 轉化後的字串連線程式碼為:

public static String concatString(String s1, String s2, String s3) {  
    StringBuffer sb = new StringBuffer;  
    sb.append(s1);  
    sb.append(s2);  
    sb.append(s3);  
    return sb.toString;  
}  

此時的鎖物件就是sb,虛擬機器觀察變數 sb,很快就會發現它的動態作用域被限制在 concatString 方法內部。也就是說,sb 的所有引用永遠不會 “逃逸” 到concatString 方法之外,其他執行緒無法訪問到它,雖然這裡有鎖,但是可以被安全地消除掉,在即時編譯之後,這段程式碼就會忽略掉所有的同步而直接執行了。

>鎖粗化

將同步塊的作用範圍限制得儘量小——只在共享資料的實際作用域中才進行同步,這樣是為了使得需要同步的運算元量儘可能變小,如果存在鎖競爭,那等待鎖的執行緒也能儘快拿到鎖。

>輕量級鎖

加 鎖過程:在程式碼進入同步塊的時候,如果此同步物件沒有被鎖定(鎖標誌位為 “01” 狀態)虛擬機器首先將在當前執行緒的棧幀中建立一個名為鎖記錄(Lock Record)的空間,用於儲存鎖物件目前的 Mark Word 的拷貝,這時候執行緒堆疊與物件頭的狀態如圖 13-3 所示

然後,虛擬機器將使用 CAS 操作嘗試將物件的 Mark Word 更新為指向 Lock Record 的指標。如果這個更新動作成功了,那麼這個執行緒就擁有了該物件的鎖,並且物件 Mark Word 的鎖標誌位 (Mark Word 的最後 2bit)將轉變為 “00”,即表示此物件處於輕量級鎖定狀態,這時執行緒堆疊與物件頭的狀態如圖13-4

如果上述更新操作失敗,則說明這個鎖物件被其他鎖佔用,此時輕量級變為重量級鎖,標誌位為“10”,後面等待的執行緒進入阻塞狀態。

解 鎖過程:也是由CAS進行操作的,如果物件的 Mark Word 仍然指向著執行緒的鎖記錄,那就用 CAS 操作把物件當前的 Mark Word 和執行緒中複製的 Displaced Mark Word 替換回來,如果替換成功,整個同步過程就完成了。如果替換失敗,說明有其他執行緒嘗試過獲取該鎖,那就要釋放鎖的同時,喚醒被掛起的執行緒。

輕 量級鎖能提升程式同步效能的依據是 “對於絕大部分的鎖,在整個同步週期內都是不存在競爭的”,這是一個經驗資料。如果沒有競爭,輕量級鎖使用 CAS 操作避免了使用互斥量的開銷,但如果存在鎖競爭,除了互斥量的開銷外,還額外發生了 CAS 操作,因此在有競爭的情況下,輕量級鎖會比傳統的重量級鎖更慢。

>偏向鎖

偏向鎖也 是 JDK 1.6 中引入的一項鎖優化,它的目的是消除資料在無競爭情況下的同步原語,進一步提高程式的執行效能。如果說輕量級鎖是在無競爭的情況下使用 CAS 操作去消除同步使用的互斥量,那偏向鎖就是在無競爭的情況下把整個同步都消除掉,連 CAS 操作都不做了。

實質就是設定一個變數,判斷這 個變數是否是當前執行緒,是就避免再次加鎖解鎖操作,從而避免了多次的CAS操作。壞處是如果一個執行緒持有偏向鎖,另外一個執行緒想爭用偏向物件,擁有者想釋 放這個偏向鎖,釋放會帶來額外的效能開銷,但是總體來說偏向鎖帶來的好處還是大於CAS的代價的。在具體問題具體分析的前提下,有時候使用引數 -XX:-UseBiasedLocking 來禁止偏向鎖優化反而可以提升效能。

二、lock的實現方案

與 synchronized不同的是lock是純java手寫的,與底層的JVM無關。在java.util.concurrent.locks包中有很多 Lock的實現類,常用的有ReenTrantLock、ReadWriteLock(實現類有ReenTrantReadWriteLock)

,其實現都依賴java.util.concurrent.AbstractQueuedSynchronizer類(簡稱AQS),實現思路都大同小異,因此我們以ReentrantLock作為講解切入點。

分 析之前我們先來花點時間看下AQS。AQS是我們後面將要提到的CountDownLatch/FutureTask/ReentrantLock /RenntrantReadWriteLock/Semaphore的基礎,因此AQS也是Lock和Excutor實現的基礎。它的基本思想就是一個 同步器,支援獲取鎖和釋放鎖兩個操作。

要支援上面鎖獲取、釋放鎖就必須滿足下面的條件:

1、 狀態位必須是原子操作的

2、 阻塞和喚醒執行緒

3、 一個有序的佇列,用於支援鎖的公平性

場景:可定時的、可輪詢的與可中斷的鎖獲取操作,公平佇列,或者非塊結構的鎖。

主要從以下幾個特點介紹:

1.可重入鎖

如果鎖具備可重入性,則稱作為可重入鎖。像synchronized和ReentrantLock都是可重入鎖,可重入性在我看來實際上表明瞭鎖的分配機制:基於執行緒的分配,而不是基於方法呼叫的分配。

2.可中斷鎖

可中斷鎖:顧名思義,就是可以相應中斷的鎖。

在Java中,synchronized就不是可中斷鎖,而Lock是可中斷鎖。

如果某一執行緒A正在執行鎖中的程式碼,另一執行緒B正在等待獲取該鎖,可能由於等待時間過長,執行緒B不想等待了,想先處理其他事情,我們可以讓它中斷自己或者在別的執行緒中中斷它,這種就是可中斷鎖。

3.公平鎖和非公平鎖

公 平鎖以請求鎖的順序來獲取鎖,非公平鎖則是無法保證按照請求的順序執行。synchronized就是非公平鎖,它無法保證等待的執行緒獲取鎖的順序。而對 於ReentrantLock和ReentrantReadWriteLock,它預設情況下是非公平鎖,但是可以設定為公平鎖。

引數為true時表示公平鎖,不傳或者false都是為非公平鎖。

ReentrantLock lock = new ReentrantLock(true);

4.讀寫鎖

讀寫鎖將對一個資源(比如檔案)的訪問分成了2個鎖,一個讀鎖和一個寫鎖。

正因為有了讀寫鎖,才使得多個執行緒之間的讀操作不會發生衝突。

ReadWriteLock就是讀寫鎖,它是一個介面,ReentrantReadWriteLock實現了這個介面。

可以通過readLock獲取讀鎖,通過writeLock獲取寫鎖。

三、總結

1.synchronized

優點:實現簡單,語義清晰,便於JVM堆疊跟蹤,加鎖解鎖過程由JVM自動控制,提供了多種優化方案,使用更廣泛

缺點:悲觀的排他鎖,不能進行高階功能

2.lock

優點:可定時的、可輪詢的與可中斷的鎖獲取操作,提供了讀寫鎖、公平鎖和非公平鎖

缺點:需手動釋放鎖unlock,不適合JVM進行堆疊跟蹤

3.相同點

都是可重入鎖

參考文章:http://www.cnblogs.com/longshiyVip/p/5213771.html

相關文章