掌握之併發編程-3.鎖

掌握高併發、高可用架構

第二課 併發編程

從本課開始學習併發編程的內容。主要介紹併發編程的基礎知識、鎖、內存模型、線程池、各種併發容器的使用。

第三節 鎖

併發編程 併發基礎 AQS Synchronized Lock

這小節咱們來學習併發編程中鎖的知識。主要包括關鍵字synchronized、各種LockAQS的原理、以及各自的應用。

synchronized

可以修飾方法或者代碼塊

表示多個線程訪問該方法或者代碼塊時要進行排隊,串行的執行該方法或者代碼塊

執行效率低,但是它是併發編程容器的基礎

分類 具體分類 被鎖的對象 示例代碼 說明
方法 實例方法 類的實例對象 synchronized void methodA() {};<br />void methodB() {};<br />synchronized void methodC() {}; 線程調用了同步方法,<br />別的線程可以調用非同步方法,<br />對於其他同步方法,必須該方法在執行完成後才能調用<br />不影響靜態方法的調用(包括同步,非同步)
靜態方法 類對象 static synchronized void methodA() {};<br />static void methodB() {};<br />static synchronized void methodC() {}; 線程調用了同步方法,<br />別的線程可以調用非同步方法,<br />對於其他同步方法,必須該方法在執行完成後才能調用<br />不影響對象方法的調用(包括同步,非同步)
代碼塊 實例對象 類的實例對象 synchronized(this) {} 同上
class對象 類對象 synchronized(SynchronizedTest.class) {} 同上
任意實例對象 實例對象Object Object lock = new Object();<br />synchronized(lock) {} 隻影響鎖住的對象,而不影響類和類的實例對象
synchronized的實現機制

JAVA對象頭Monitor是實現synchronized的基礎。

  1. JAVA對象頭,對於Hotspot虛擬機的對象頭主要包含兩部分數據:Mark Word(標記字段)、Klass Pointer(類型指針)

    • Mark Word,用於存儲對象自身的運行時數據,如哈希碼(Hash Code)、GC分代年齡、鎖狀態標識、線程持有的鎖、偏向線程ID、偏向時間戳等
    • Klass Pointer,是對象指向它的類元數據的指針,虛擬機通過這個指針來確定這個對象是哪個類的實例
  2. Monitor,是一種同步機制,即同一時刻只允許一個線程進入Monitor的臨界區,從而達到互斥的效果。synchronized的對象鎖,其指針指向的是一個Monitor對象的起始地址。每個對象實例都有一個monitor。由C++實現,其數據結構如下。

    ```C++
    ObjectMonitor() {
    _count = 0;
    _owner = NULL;
    _waitSet = NULL;
    _waitSetLock = 0;
    _EntryList = NULL;
    }

    
    
    其中,**_owner**指向持有ObjectMonitor對象的線程。當多個線程同時訪問一段同步代碼時,會把線程存放在鎖的對象的**_EntryList**中。當某個線程獲得對象的Monitor時,就會把*_owner*的值設置爲當前線程,同時*_count*加1。如果線程調用**wait()**方法,就會釋放當前持有的Monitor,*_owner*置爲null,*_count*減1,並將該線程放入**_waitSet**中。當然,如果持有monitor的線程正常執行完畢,也會釋放monitor,*_owner*置爲null,*_count*減1。

對於加在代碼塊上的synchronized,其字節碼是:一次monitorenter、兩次monitorexit(含有一次編譯器自動生成的異常處理的monitorexit);

對於加在方法上的synchronized,其字節碼是:標識方法爲ACC_SYNCHRONIZED

新特性

synchronized是一個重量級鎖,相較Lock,比較笨重,不高效。在JDK1.6中,其實現過程引入了大量的優化,如自旋鎖自適應自旋鎖鎖消除鎖粗化偏向鎖輕量級鎖等技術來減少鎖的開銷。

  • 自旋鎖,是指當一個線程在獲取鎖的時候,如果鎖已經被其他線程獲取,那麼該線程將循環等待,然後不斷判斷鎖是否能夠成功獲取,直到獲取到鎖才退出循環。

    優點:自旋鎖不會使線程發生狀態切換,而是一直處於活動狀態,不會進入阻塞狀態,減少了不必要的上下文切換,執行速度快

    缺點:如果某個線程持有鎖的時間過長,就會導致其他等待獲取鎖的線程進入循環等待,消耗CPU。如果使用不當會導致CPU使用率極高;不公平的鎖會導致“線程飢餓”問題

  • 自適應自旋鎖,JDK1.6引入的更聰明的自旋鎖。就是說自旋的次數不是固定的,它是由前一次在同一個鎖上的自旋時間以及鎖的持有者的狀態決定的。JVM自適應的調整自旋次數,使能更有效的獲取到鎖,避免浪費資源

  • 鎖消除,當JVM檢測到不可能存在共享數據競爭,此時會對這些鎖進行鎖消除

  • 鎖粗化,在使用鎖時,需要讓同步代碼塊的作用範圍儘可能小。所謂鎖粗化,就是將多個連續的加鎖、解鎖操作連接在一起,擴展成一個範圍更大的鎖

  • 偏向鎖,是指同一段代碼一直被同一個線程訪問,那麼該線程會自動獲取到鎖,從而降低獲取鎖的代價

  • 輕量級鎖,當鎖是偏向鎖的時候,此時被另一個線程訪問,偏向鎖會升級爲輕量級鎖。其他線程會通過自旋的形式嘗試獲取鎖,不會阻塞

  • 重量級鎖,當鎖是輕量級鎖時,另一個線程雖然在自旋等待獲取鎖,但是自旋不會一直持續,當自旋一定次數後,還沒有獲取到鎖,就會進入阻塞,該鎖也會膨脹爲重量級鎖。重量級鎖會讓其他線程進入阻塞,性能降低。此時就成爲了原始的synchronized的實現
鎖的等級

依次是:無鎖狀態、偏向鎖狀態、輕量級鎖狀態、重量級鎖狀態

Lock

Lock是一個接口,有以下方法。

public interface Lock {
    void lock();
    boolean tryLock();
    boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
    void unlock();
    void lockInterruptibly();
    Condition newCondition();
}

這裏說下方法void lockInterruptibly()一個線程獲得鎖之後是不可以被interrupt()方法中斷的,是不能中斷正在執行中的線程的,只能中斷阻塞過程中的線程lockInterruptibly方法允許當線程等待獲取鎖的過程中由其他線程來中斷等待。

Lock 和 synchronized的區別與相同點

區別:

  • Lock是一個接口,synchronized是內置關鍵字;Lock只能在代碼塊中,而synchronized可以在方法上和代碼塊中
  • synchronized不管正常退出還是異常退出,都會自動釋放鎖資源,不會導致死鎖的發生;Lock需要手動加鎖和解鎖,所以如果解鎖操作未執行就會導致死鎖
  • Lock可以讓等待鎖的線程響應中斷,而synchronized不行,使用synchronized時,等待的線程會一直等待下去,不能夠響應中斷
  • 通過Lock可以知道有沒有獲取到鎖,而synchronized無法知道
  • Lock可以選擇公平鎖和非公平鎖,而synchronized只有非公平鎖
  • Lock是樂觀鎖,通過CAS(compare and swap 比較交換)來實現,底層是AbstractQueuedSynchronizer(AQS),而synchronized是悲觀鎖,是由JVM來控制的
  • Lock的鎖是針對lock對象,而synchronized的鎖定對象是類或者指定對象
  • Lock可以提高多個線程進行讀操作的效率

相同點:

  • 都是可重入鎖
ReentrantLock

可重入鎖,是Lock接口的唯一實現類。

可重入鎖:是指如果一個線程獲得了一個對象的鎖,那麼它不需要再獲取該對象的鎖而可以直接執行方法。也就是鎖的分配機制是基於線程來分配的,而不是基於方法調用的分配。

可中斷鎖:可以響應中斷的鎖。只有lockInterruptibly()方法的鎖是可中斷鎖,lock()還是不可中斷的

公平鎖:指儘量以請求鎖的順序來獲取鎖。比如有多個線程在等待一個鎖,當鎖被其他線程釋放時,最先請求鎖的線程會獲得該鎖

非公平鎖:無法保證鎖的獲取是按照請求鎖的順序進行的。可能導致某個或者一些線程永遠獲取不到鎖

對於ReentrantLock,默認是非公平鎖,但可指定爲公平鎖。

ReentrantLock lock = new ReentrantLock(true)

ReentrantLock中定義了兩個內部類,一個是NotFairSync,一個是FairSync。當構造器參數爲true時,表示創建公平鎖,參數爲false或者無參時,表示非公平鎖。

ReadWriteLock

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

一個用來獲取讀鎖,一個用來獲取寫鎖。

ReentrantReadWriteLock

可重入讀寫鎖,實現了ReadWriteLock接口。

多個線程同時進行讀操作時,會使多個線程交替進行,從而提高讀操作的效率。但是,如果有線程佔用讀鎖,此時其他要獲取寫鎖的話,就必須等待讀鎖釋放後纔可執行;如果有線程佔用寫鎖,此時其他線程不管是要獲取讀鎖或者寫鎖的話,都必須等待寫鎖釋放。

Lock的實現原理

ReentrantLockFairSyncNotFairSync都繼承了AbstractQueuedSynchronizer,並且真正lock()unlock()的實現過程都是在AQS中。

首先,AQS的數據結構是:一個表示鎖狀態的變量volatile int state,取值範圍是 0 無鎖、1 有鎖;一個用於存儲等待獲取鎖的線程的雙向鏈表transient volatile Node headtransient volatile Node tail

其次,加鎖流程NotFairSync.lock()是:

  1. 通過CAS去嘗試獲取鎖:判斷當前state是0的話表示無鎖,然後把當前線程設置爲獨佔執行線程,再修改state爲1表示有鎖
    掌握之併發編程-3.鎖

  2. 如果第一步獲取鎖失敗,那麼執行acquire(1)(這是AQS的方法)

掌握之併發編程-3.鎖

主要是三個方法:

  • tryAcquire,再次嘗試通過CAS獲取一次鎖(子類NotFairSync的方法)
    • 判斷state,0則可以獲取到鎖,非0則判斷執行線程是否是當前線程,如果是,則把重入次數加1,都不是則設置獲取鎖失敗

掌握之併發編程-3.鎖

  • addWaiter,把當前線程放入等待隊列的雙向鏈表Node中(通過無限循環-自旋,找到鏈表尾,接到尾部)
  • acquireQueued,通過自旋,判斷當前線程是否到達鏈表頭部,當到達頭部時,就嘗試獲取鎖。如果獲取到鎖就把其從頭部移除
    • 在自旋過程中,通過shouldParkAfterFailedAcquire判斷當前線程的狀態是CancelledSignal等,從而在parkAndCheckInterrupt中對線程進行剔除(Cancelled)、阻塞(Signal)操作

掌握之併發編程-3.鎖
下面借兩張圖(<https://yq.aliyun.com/articles/640868&gt;)

71e8b71038243dfaf21ebcf6f9fcc5fbaa659b08

獲取鎖的流程:

48c01d6093d60683cf2d842a26d7e407cfbcde7f

然後,解鎖的流程是調用NotFairSync.release(),主要是對重入數量的調整。每次釋放鎖都只會對數量減1,直到state爲0時表示鎖釋放完成。
掌握之併發編程-3.鎖

Lock和synchronized的選擇

根據CAS的特性,建議在低鎖衝突的情況下使用Lock

在JDK1.6後,官方對synchronized做了大量的優化(偏向鎖、自旋、輕量級鎖等),因此在非必要的情況下,建議都使用synchronized做同步操作

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