深刻併發鎖,解析Synchronized鎖升級

這篇文章分爲六個部分,不一樣特性的鎖分類,併發鎖的不一樣設計,Synchronized中的鎖升級,ReentrantLock和ReadWriteLock的應用,幫助你梳理 Java 併發鎖及相關的操做。java

1、鎖有哪些分類

通常咱們提到的鎖有如下這些:算法

  • 樂觀鎖/悲觀鎖
  • 公平鎖/非公平鎖
  • 可重入鎖
  • 獨享鎖/共享鎖
  • 互斥鎖/讀寫鎖
  • 分段鎖
  • 偏向鎖/輕量級鎖/重量級鎖
  • 自旋鎖

上面是不少鎖的名詞,這些分類並非全是指鎖的狀態,有的指鎖的特性,有的指鎖的設計,下面分別說明。數據庫

一、樂觀鎖 VS 悲觀鎖

樂觀鎖與悲觀鎖是一種廣義上的概念,體現了看待線程同步的不一樣角度,在Java和數據庫中都有此概念對應的實際應用。編程

(1)樂觀鎖

顧名思義,就是很樂觀,每次去拿數據的時候都認爲別人不會修改,因此不會上鎖,可是在更新的時候會判斷一下在此期間別人有沒有去更新這個數據,可使用版本號等機制。數組

樂觀鎖適用於多讀的應用類型,樂觀鎖在Java中是經過使用無鎖編程來實現,最常採用的是CAS算法,Java原子類中的遞增操做就經過CAS自旋實現的。緩存

CAS全稱 Compare And Swap(比較與交換),是一種無鎖算法。在不使用鎖(沒有線程被阻塞)的狀況下實現多線程之間的變量同步。java.util.concurrent包中的原子類就是經過CAS來實現了樂觀鎖。安全

簡單來講,CAS算法有3個三個操做數:數據結構

  • 須要讀寫的內存值 V。
  • 進行比較的值 A。
  • 要寫入的新值 B。

當且僅當預期值A和內存值V相同時,將內存值V修改成B,不然返回V。這是一種樂觀鎖的思路,它相信在它修改以前,沒有其它線程去修改它;而Synchronized是一種悲觀鎖,它認爲在它修改以前,必定會有其它線程去修改它,悲觀鎖效率很低。多線程

(2)悲觀鎖

老是假設最壞的狀況,每次去拿數據的時候都認爲別人會修改,因此每次在拿數據的時候都會上鎖,這樣別人想拿這個數據就會阻塞直到它拿到鎖。架構

傳統的MySQL關係型數據庫裏邊就用到了不少這種鎖機制,好比行鎖,表鎖等,讀鎖,寫鎖等,都是在作操做以前先上鎖。

  • 悲觀鎖適合寫操做多的場景,先加鎖能夠保證寫操做時數據正確。
  • 樂觀鎖適合讀操做多的場景,不加鎖的特色可以使其讀操做的性能大幅提高。

二、公平鎖 VS 非公平鎖

(1)公平鎖

就是很公平,在併發環境中,每一個線程在獲取鎖時會先查看此鎖維護的等待隊列,若是爲空,或者當前線程是等待隊列的第一個,就佔有鎖,不然就會加入到等待隊列中,之後會按照FIFO的規則從隊列中取到本身。

公平鎖的優勢是等待鎖的線程不會餓死。缺點是總體吞吐效率相對非公平鎖要低,等待隊列中除第一個線程之外的全部線程都會阻塞,CPU喚醒阻塞線程的開銷比非公平鎖大。

(2)非公平鎖

上來就直接嘗試佔有鎖,若是嘗試失敗,就再採用相似公平鎖那種方式。

非公平鎖的優勢是能夠減小喚起線程的開銷,總體的吞吐效率高,由於線程有概率不阻塞直接得到鎖,CPU沒必要喚醒全部線程。缺點是處於等待隊列中的線程可能會餓死,或者等好久纔會得到鎖。

(3)典型應用

java jdk併發包中的ReentrantLock能夠指定構造函數的boolean類型來建立公平鎖和非公平鎖(默認),好比:公平鎖可使用new ReentrantLock(true)實現。

三、獨享鎖 VS 共享鎖

(1)獨享鎖

是指該鎖一次只能被一個線程所持有。

(2)共享鎖

是指該鎖可被多個線程所持有。

對於Java ReentrantLock而言,其是獨享鎖。可是對於Lock的另外一個實現類ReadWriteLock,其讀鎖是共享鎖,其寫鎖是獨享鎖。

  • 讀鎖的共享鎖可保證併發讀是很是高效的,讀寫,寫讀 ,寫寫的過程是互斥的。
  • 獨享鎖與共享鎖也是經過AQS來實現的,經過實現不一樣的方法,來實現獨享或者共享。

(3)AQS

抽象隊列同步器(AbstractQueuedSynchronizer,簡稱AQS)是用來構建鎖或者其餘同步組件的基礎框架,它使用一個整型的volatile變量(命名爲state)來維護同步狀態,經過內置的FIFO隊列來完成資源獲取線程的排隊工做。

concurrent包的實現結構如上圖所示,AQS、非阻塞數據結構和原子變量類等基礎類都是基於volatile變量的讀/寫和CAS實現,而像Lock、同步器、阻塞隊列、Executor和併發容器等高層類又是基於基礎類實現。

四、互斥鎖 VS 讀寫鎖

相交進程之間的關係主要有兩種,同步與互斥。所謂互斥,是指散佈在不一樣進程之間的若干程序片段,當某個進程運行其中一個程序片斷時,其它進程就不能運行它們之中的任一程序片斷,只能等到該進程運行完這個程序片斷後才能夠運行。所謂同步,是指散佈在不一樣進程之間的若干程序片段,它們的運行必須嚴格按照規定的某種前後次序來運行,這種前後次序依賴於要完成的特定的任務。

顯然,同步是一種更爲複雜的互斥,而互斥是一種特殊的同步。
也就是說互斥是兩個線程之間不能夠同時運行,他們會相互排斥,必須等待一個線程運行完畢,另外一個才能運行,而同步也是不能同時運行,但他是必需要安照某種次序來運行相應的線程(也是一種互斥)!

總結:互斥:是指某一資源同時只容許一個訪問者對其進行訪問,具備惟一性和排它性。但互斥沒法限制訪問者對資源的訪問順序,即訪問是無序的。

同步:是指在互斥的基礎上(大多數狀況),經過其它機制實現訪問者對資源的有序訪問。在大多數狀況下,同步已經實現了互斥,特別是全部寫入資源的狀況一定是互斥的。少數狀況是指能夠容許多個訪問者同時訪問資源。

(1)互斥鎖

在訪問共享資源以前對進行加鎖操做,在訪問完成以後進行解鎖操做。 加鎖後,任何其餘試圖再次加鎖的線程會被阻塞,直到當前進程解鎖。

若是解鎖時有一個以上的線程阻塞,那麼全部該鎖上的線程都被編程就緒狀態, 第一個變爲就緒狀態的線程又執行加鎖操做,那麼其餘的線程又會進入等待。 在這種方式下,只有一個線程可以訪問被互斥鎖保護的資源

(2)讀寫鎖

這個時候讀寫鎖就應運而生了,讀寫鎖是一種通用技術,並非Java特有的。

讀寫鎖特色:

  • 多個讀者能夠同時進行讀
  • 寫者必須互斥(只容許一個寫者寫,也不能讀者寫者同時進行)
  • 寫者優先於讀者(一旦有寫者,則後續讀者必須等待,喚醒時優先考慮寫者)

互斥鎖特色:

  • 一次只能一個線程擁有互斥鎖,其餘線程只有等待

(3)Linux的讀寫鎖

Linux內核也支持讀寫鎖。

互斥鎖
pthread_mutex_init()
pthread_mutex_lock()
pthread_mutex_unlock()
讀寫鎖
 
pthread_rwlock_init()
pthread_rwlock_rdlock()
pthread_rwlock_wrlock()
pthread_rwlock_unlock()
條件變量
 
pthread_cond_init()
pthread_cond_wait()
pthread_cond_signal()

五、自旋鎖

自旋鎖(spinlock):是指當一個線程在獲取鎖的時候,若是鎖已經被其它線程獲取,那麼該線程將循環等待,而後不斷的判斷鎖是否可以被成功獲取,直到獲取到鎖纔會退出循環。

在Java中,自旋鎖是指嘗試獲取鎖的線程不會當即阻塞,而是採用循環的方式去嘗試獲取鎖,這樣的好處是減小線程上下文切換的消耗,缺點是循環會消耗CPU。
典型的自旋鎖實現的例子,能夠參考自旋鎖的實現

它是爲實現保護共享資源而提出一種鎖機制。其實,自旋鎖與互斥鎖比較相似,它們都是爲了解決對某項資源的互斥使用。不管是互斥鎖,仍是自旋鎖,在任什麼時候刻,最多隻能有一個保持者,也就說,在任什麼時候刻最多隻能有一個執行單元得到鎖。可是二者在調度機制上略有不一樣。對於互斥鎖,若是資源已經被佔用,資源申請者只能進入睡眠狀態。

可是自旋鎖不會引發調用者睡眠,若是自旋鎖已經被別的執行單元保持,調用者就一直循環在那裏看是否該自旋鎖的保持者已經釋放了鎖,」自旋」一詞就是所以而得名。

(1)Java如何實現自旋鎖?

下面是個簡單的例子:

public class SpinLock {
    private AtomicReference<Thread> cas = new AtomicReference<Thread>();
    public void lock() {
        Thread current = Thread.currentThread();
        // 利用CAS
        while (!cas.compareAndSet(null, current)) {
            // DO nothing
        }
    }
    public void unlock() {
        Thread current = Thread.currentThread();
        cas.compareAndSet(current, null);
    }
}

lock()方法利用的CAS,當第一個線程A獲取鎖的時候,可以成功獲取到,不會進入while循環,若是此時線程A沒有釋放鎖,另外一個線程B又來獲取鎖,此時因爲不知足CAS,因此就會進入while循環,不斷判斷是否知足CAS,直到A線程調用unlock方法釋放了該鎖。

(2)自旋鎖存在的問題

  1. 若是某個線程持有鎖的時間過長,就會致使其它等待獲取鎖的線程進入循環等待,消耗CPU。使用不當會形成CPU使用率極高。
  2. 上面Java實現的自旋鎖不是公平的,即沒法知足等待時間最長的線程優先獲取鎖。不公平的鎖就會存在「線程飢餓」問題。

(3)自旋鎖的優勢

  1. 自旋鎖不會使線程狀態發生切換,一直處於用戶態,即線程一直都是active的;不會使線程進入阻塞狀態,減小了沒必要要的上下文切換,執行速度快
  2. 非自旋鎖在獲取不到鎖的時候會進入阻塞狀態,從而進入內核態,當獲取到鎖的時候須要從內核態恢復,須要線程上下文切換。 (線程被阻塞後便進入內核(Linux)調度狀態,這個會致使系統在用戶態與內核態之間來回切換,嚴重影響鎖的性能)

2、併發鎖的不一樣設計方式

根據所鎖的設計方式和應用,有分段鎖,讀寫鎖等。

一、分段鎖技術,併發鎖的一種設計方案

分段鎖實際上是一種鎖的設計,並非具體的一種鎖,對於ConcurrentHashMap而言,其併發的實現就是經過分段鎖的形式來實現高效的併發操做。

以ConcurrentHashMap來講一下分段鎖的含義以及設計思想,ConcurrentHashMap中的分段鎖稱爲Segment,它即相似於HashMap(JDK7與JDK8中HashMap的實現)的結構,即內部擁有一個Entry數組,數組中的每一個元素又是一個鏈表;同時又是一個ReentrantLock(Segment繼承了ReentrantLock)。

當須要put元素的時候,並非對整個hashmap進行加鎖,而是先經過hashcode來知道他要放在那一個分段中,而後對這個分段進行加鎖,因此當多線程put的時候,只要不是放在一個分段中,就實現了真正的並行的插入。

可是,在統計size的時候,可就是獲取hashmap全局信息的時候,就須要獲取全部的分段鎖才能統計。

分段鎖的設計目的是細化鎖的粒度,當操做不須要更新整個數組的時候,就僅僅針對數組中的一項進行加鎖操做。

二、鎖消除和鎖膨脹(粗化)

鎖消除,如無必要,不要使用鎖。Java 虛擬機也能夠根據逃逸分析判斷出加鎖的代碼是否線程安全,若是確認線程安全虛擬機會進行鎖消除提升效率。

鎖粗化。若是一段代碼須要使用多個鎖,建議使用一把範圍更大的鎖來提升執行效率。Java 虛擬機也會進行優化,若是發現同一個對象鎖有一系列的加鎖解鎖操做,虛擬機會進行鎖粗化來下降鎖的耗時。

三、輪詢鎖與定時鎖

輪詢鎖是經過線程不斷嘗試獲取鎖來實現的,能夠避免發生死鎖,能夠更好地處理錯誤場景。Java 中能夠經過調用鎖的 tryLock 方法來進行輪詢。tryLock 方法還提供了一種支持定時的實現,能夠經過參數指定獲取鎖的等待時間。若是能夠當即獲取鎖那就當即返回,不然等待一段時間後返回。

四、讀寫鎖

讀寫鎖 ReadWriteLock 能夠優雅地實現對資源的訪問控制,具體實現爲 ReentrantReadWriteLock。讀寫鎖提供了讀鎖和寫鎖兩把鎖,在讀數據時使用讀鎖,在寫數據時使用寫鎖。

讀寫鎖容許有多個讀操做同時進行,但只容許有一個寫操做執行。若是寫鎖沒有加鎖,則讀鎖不會阻塞,不然須要等待寫入完成。

ReadWriteLock lock = new ReentrantReadWriteLock();
Lock readLock = lock.readLock();
Lock writeLock = lock.writeLock();

3、synchronized中的鎖

synchronized 代碼塊是由一對兒 monitorenter/monitorexit 指令實現的,Monitor 對象是同步的基本實現單元。

在 Java 6 以前,Monitor 的實現徹底是依靠操做系統內部的互斥鎖,由於須要進行用戶態到內核態的切換,因此同步操做是一個無差異的重量級操做。
現代的(Oracle)JDK 中,JVM 對此進行了大刀闊斧地改進,提供了三種不一樣的 Monitor 實現,也就是常說的三種不一樣的鎖:偏斜鎖(Biased Locking)、輕量級鎖和重量級鎖,大大改進了其性能。

一、synchronized中鎖的狀態

鎖的狀態是經過對象監視器在對象頭中的字段來代表的。
四種狀態會隨着競爭的狀況逐漸升級,並且是不可逆的過程,即不可降級。
這四種狀態都不是Java語言中的鎖,而是Jvm爲了提升鎖的獲取與釋放效率而作的優化(使用synchronized時)。

  • 無鎖狀態
  • 偏向鎖狀態
  • 輕量級鎖狀態
  • 重量級鎖狀態

二、偏向鎖、輕量級鎖、重量級鎖

這三種鎖是指鎖的狀態,而且是針對Synchronized。在Java 5經過引入鎖升級的機制來實現高效Synchronized。這三種鎖的狀態是經過對象監視器在對象頭中的字段來代表的。

偏向鎖是指一段同步代碼一直被一個線程所訪問,那麼該線程會自動獲取鎖。下降獲取鎖的代價。
輕量級鎖是指當鎖是偏向鎖的時候,被另外一個線程所訪問,偏向鎖就會升級爲輕量級鎖,其餘線程會經過自旋的形式嘗試獲取鎖,不會阻塞,提升性能。

重量級鎖是指當鎖爲輕量級鎖的時候,另外一個線程雖然是自旋,但自旋不會一直持續下去,當自旋必定次數的時候,尚未獲取到鎖,就會進入阻塞,該鎖膨脹爲重量級鎖。重量級鎖會讓其餘申請的線程進入阻塞,性能下降。

三、synchronized的鎖升級

所謂鎖的升級、降級,就是 JVM 優化 synchronized 運行的機制,當 JVM 檢測到不一樣的競爭情況時,會自動切換到適合的鎖實現,這種切換就是鎖的升級、降級。

當沒有競爭出現時,默認會使用偏斜鎖。JVM 會利用 CAS 操做(compare and swap),在對象頭上的 Mark Word 部分設置線程 ID,以表示這個對象偏向於當前線程,因此並不涉及真正的互斥鎖。這樣作的假設是基於在不少應用場景中,大部分對象生命週期中最多會被一個線程鎖定,使用偏斜鎖能夠下降無競爭開銷。

若是有另外的線程試圖鎖定某個已經被偏斜過的對象,JVM 就須要撤銷(revoke)偏斜鎖,並切換到輕量級鎖實現。輕量級鎖依賴 CAS 操做 Mark Word 來試圖獲取鎖,若是重試成功,就使用普通的輕量級鎖;不然,進一步升級爲重量級鎖。

4、看下ReentrantLock

ReentrantLock,一個可重入的互斥鎖,它具備與使用synchronized方法和語句所訪問的隱式監視器鎖相同的一些基本行爲和語義,但功能更強大。

一、基本用法

public class LockTest {
 
    private Lock lock = new ReentrantLock();
    public void testMethod() {
        lock.lock();
        for (int i = 0; i < 5; i++) {
            System.out.println("ThreadName=" + Thread.currentThread().getName()
                    + (" " + (i + 1)));
        }
        lock.unlock();
    }
 
}

二、Condition應用

synchronized與wait()和nitofy()/notifyAll()方法相結合能夠實現等待/通知模型,ReentrantLock一樣能夠,可是須要藉助Condition,且Condition有更好的靈活性,具體體如今:

  • 一個Lock裏面能夠建立多個Condition實例,實現多路通知
  • notify()方法進行通知時,被通知的線程時Java虛擬機隨機選擇的,可是ReentrantLock結合Condition能夠實現有選擇性地通知,這是很是重要的

三、Condition類和Object類

  • Condition類的awiat方法和Object類的wait方法等效
  • Condition類的signal方法和Object類的notify方法等效
  • Condition類的signalAll方法和Object類的notifyAll方法等效

5、再看下ReadWriteLock

在併發場景中用於解決線程安全的問題,咱們幾乎會高頻率的使用到獨佔式鎖,一般使用java提供的關鍵字synchronized(關於synchronized能夠看這篇文章)或者concurrents包中實現了Lock接口的ReentrantLock。

它們都是獨佔式獲取鎖,也就是在同一時刻只有一個線程可以獲取鎖。而在一些業務場景中,大部分只是讀數據,寫數據不多,若是僅僅是讀數據的話並不會影響數據正確性(出現髒讀),而若是在這種業務場景下,依然使用獨佔鎖的話,很顯然這將是出現性能瓶頸的地方。

針對這種讀多寫少的狀況,java還提供了另一個實現Lock接口的ReentrantReadWriteLock(讀寫鎖)。讀寫所容許同一時刻被多個讀線程訪問,可是在寫線程訪問時,全部的讀線程和其餘的寫線程都會被阻塞。

一、ReadWriteLock接口

ReadWriteLock,顧明思義,讀寫鎖在讀的時候,上讀鎖,在寫的時候,上寫鎖,這樣就很巧妙的解決synchronized的一個性能問題:讀與讀之間互斥。

ReadWriteLock也是一個接口,原型以下:

public interface ReadWriteLock {
    Lock readLock();
    Lock writeLock();
}

該接口只有兩個方法,讀鎖和寫鎖。

也就是說,咱們在寫文件的時候,能夠將讀和寫分開,分紅2個鎖來分配給線程,從而能夠作到讀和讀互不影響,讀和寫互斥,寫和寫互斥,提升讀寫文件的效率。

二、ReentrantReadWriteLock應用

下面的實例參考《Java併發編程的藝術》,使用讀寫鎖實現一個緩存。

import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

public class Cache {
    static Map<String,Object> map = new HashMap<String, Object>();
    static ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
    static Lock readLock = readWriteLock.readLock();
    static Lock writeLock = readWriteLock.writeLock();
    
    public static final Object getByKey(String key){
        readLock.lock();
        try{
            return map.get(key);
        }finally{
            readLock.unlock();
        }
    }
    
    public static final Object getMap(){
        readLock.lock();
        try{
            return map;
        }finally{
            readLock.unlock();
        }
    }
    
    public static final Object put(String key,Object value){
        writeLock.lock();
        try{
            return map.put(key, value);
        }finally{
            writeLock.unlock();
        }
    }
    
    public static final Object remove(String key){
        writeLock.lock();
        try{
            return map.remove(key);
        }finally{
            writeLock.unlock();
        }
    }
    
    public static final void clear(){
        writeLock.lock();
        try{
            map.clear();
        }finally{
            writeLock.unlock();
        }
    }
    
    public static void main(String[] args) {
        List<Thread> threadList = new ArrayList<Thread>();
        for(int i =0;i<6;i++){
            Thread thread = new PutThread();
            threadList.add(thread);
        }
        for(Thread thread : threadList){
            thread.start();
        }
        put("ji","ji");
        System.out.println(getMap());
    }
    
    private static class PutThread extends Thread{
        public void run(){
            put(Thread.currentThread().getName(),Thread.currentThread().getName());
        }
    }
}

三、讀寫鎖的鎖降級

讀寫鎖支持鎖降級,遵循按照獲取寫鎖,獲取讀鎖再釋放寫鎖的次序,寫鎖可以降級成爲讀鎖,不支持鎖升級,關於鎖降級下面的示例代碼摘自ReentrantWriteReadLock源碼中:

void processCachedData() {
        rwl.readLock().lock();
        if (!cacheValid) {
            // Must release read lock before acquiring write lock
            rwl.readLock().unlock();
            rwl.writeLock().lock();
            try {
                // Recheck state because another thread might have
                // acquired write lock and changed state before we did.
                if (!cacheValid) {
                    data = ...
            cacheValid = true;
          }
          // Downgrade by acquiring read lock before releasing write lock
          rwl.readLock().lock();
        } finally {
          rwl.writeLock().unlock(); // Unlock write, still hold read
        }
      }
 
      try {
        use(data);
      } finally {
        rwl.readLock().unlock();
      }
    }
}
關注公衆號:架構進化論,得到第一手的技術資訊和原創文章
相關文章
相關標籤/搜索