java多線程:synchronized和lock比較淺析

轉載: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

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