Java 可重入鎖的那些事(一)

本文主要包含的內容:可重入鎖(ReedtrantLock)、公平鎖、非公平鎖、可重入性、同步隊列、CAS等概念的理解

顯式鎖🔒

上一篇文章提到的synchronized關鍵字為隱式鎖,會自動獲取和自動釋放的鎖,而相對的顯式鎖則需要在編程時指明何時獲取鎖,何時釋放鎖。

通常,鎖提供對共享資源的獨佔訪問:一次只能有一個執行緒可以獲取鎖,並且對共享資源的所有訪問都需要先獲取鎖;而有一些鎖可能允許並發訪問共享資源。

本文主要講解可重入鎖(ReentrantLock),該鎖為獨佔共享資源鎖,即獨佔鎖。

1.可重入鎖(ReentrantLock)

可重入鎖指的是同一個執行緒可無限次地進入同一把鎖的不同程式碼,又因該鎖通過執行緒獨佔共享資源的方式確保並發安全,又稱為獨佔鎖

舉個例子:同一個類中的synchronize關鍵字修飾了不同的方法。synchronize是內置的隱式的可重入鎖,例子中的兩個方法使用的是同一把鎖,只要能執行testB()也就說明執行緒拿到了鎖,所以執行testA()方法就不用被阻塞等待獲取鎖了;如果不是同一把鎖或非可重入鎖,就會在執行testA()時被阻塞等待。

public class Demo {

    public synchronized void testA(){
        System.out.println("執行測試A");
    }

    public synchronized void testB(){
        System.out.println("執行測試B");
        testA();
    }

}

1.1.可重入鎖的類圖關係

ReentrantLock實現了Lock介面和Serializable介面(都沒畫出來),它有三個內部類(SyncNonfairSyncFairSync),Sync是一個抽象類,它繼承 AbstractQueuedSynchronizer 抽象同步隊列,同時有兩個實現類(NonfairSyncFairSync),其中父類AQS是個模板類提供了許多以鎖相關的操作,子類分別是兩種不同的獲取鎖實現(非公平鎖和公平鎖)。AQS 又繼承了AbstractOwnableSynchronizer類,AOS用於保存鎖被獨佔的執行緒對象。

image

ReentrantLock 類的構造方法有如下兩種,很顯然,在對象實例化時將決定同步器Sync是公平還是非公平。

// ReentrantLock類

private final Sync sync;
// 默認非公平
public ReentrantLock() {
    sync = new NonfairSync();
}

public ReentrantLock(boolean fair) {
    sync = fair ? new FairSync() : new NonfairSync();
}

先關注ReentrantLock類的方法lock() 和 unlock()。從源碼可以發現ReentrantLock類的方法是交給內部類Sync 類來實現,而lock()方法在Sync類中是個抽象方法,具體實現在子類FairSync和NonfairSync類。其實ReentrantLock類中的其他方法也是交給Sync類去處理的,所以想要理解ReentrantLock類的重點是理解Sync類。

注意一個點:Sync類中lock()抽象方法不是Lock介面的抽象方法,它們是通過調用(如下👇)程式碼產生關聯的。

// java.util.concurrent.locks.ReentrantLock類

public void lock() {
    sync.lock();
}
public void unlock() {
    sync.release(1);
}

結論一:

  • ReentrantLock 可重入鎖獲取鎖有兩種實現:公平和非公平;注意:從類圖關係我們可以知道,公平和非公平內部類只有兩個方法,都是與獲取鎖有關,公平與否僅針對獲取鎖而言,也即是lock()方法。PS:tryAcquire(int)最終會被lock()調用。

  • ReentrantLock的理解重點源碼應該關注內部同步器Sync類和Sync的父類抽象同步隊列AbstractQueuedSynchronizer。

1.2.怎麼使用ReentrantLock

使用案例:並發安全訪問共享資源

public class LockDemo {
    public static void main(String[] args) {
        // 簡單模擬20人搶優惠
        for(int i=0;i<20;i++){
            new Thread(new ThreadDemo()).start();
        }
    }

}
// 前十位可以獲取優惠,憑號碼兌換優惠
class ThreadDemo implements Runnable{
    private static Integer num = 10;
    private static final ReentrantLock reentrantLock = new ReentrantLock();
    @Override
    public void run() {
        try {
            Thread.sleep(10);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        // 獲取鎖
        reentrantLock.lock();
        try {
            if(num<=0){
                System.out.println("已被搶完,下次再來");
                return;
            }
            System.out.println(Thread.currentThread().getName()+"用戶搶到的號碼:"+num--);
        }finally {
            // 釋放鎖
            reentrantLock.unlock();
        }

    }
}

執行結果:

Thread-18用戶搶到的號碼:10

Thread-14用戶搶到的號碼:9

Thread-15用戶搶到的號碼:8

Thread-4用戶搶到的號碼:7

Thread-1用戶搶到的號碼:6

Thread-19用戶搶到的號碼:5

Thread-11用戶搶到的號碼:4

Thread-17用戶搶到的號碼:3

Thread-16用戶搶到的號碼:2

Thread-13用戶搶到的號碼:1

已被搶完,下次再來

已被搶完,下次再來

……

常用的一些方法

方法名稱 描述
void lock() 獲取鎖
boolean tryLock() 嘗試獲取鎖,調用該方法不會阻塞,會立即返回獲取結果,獲取到則返回true,獲取不到則返回false
boolean tryLock(long timeout, TimeUnit unit) 嘗試在阻塞的指定時間內獲取鎖
void lockInterruptibly() 獲取鎖,除非當前執行緒是interrupted,即發生中斷時,結束鎖的獲取
void unlock() 釋放鎖
boolean isHeldByCurrentThread() 查詢此鎖是否由當前執行緒持有
boolean isLocked() 查詢此鎖是否由任何執行緒持有

2.一些概念的理解

2.1.鎖和同步隊列的關係

前面講述過:ReentrantLock類的方法都是交給內部類Sync類來實現的。

Sync和它的子類都實現了,為什麼還要ReentrantLock類來套這麼一層呢?這關係到鎖的使用和實現的問題。

  • 鎖是面向開發者,隱藏細節讓鎖的開發變得更簡潔;

  • 抽象同步隊列是面向鎖的實現,屏蔽了同步狀態的管理、執行緒的排隊、等待與喚醒等底層操作,簡化了自定義同步器和鎖的實現。

說白了,ReentrantLock(鎖)類為了簡化開發者的使用,具體實現交由其內部類自定義的同步器Sync去處理,而AQS則以模板的方式提供一系列有關鎖的操作及部分可被子類Sync重寫的模板方法。

2.2.公平鎖與非公平鎖概述

公平與非公平指的是獲取鎖的機制不同。

公平鎖強調先來後到,表示執行緒獲取鎖的順序是按照執行緒請求鎖的時間早晚來決定,即同步隊列記錄執行緒先後順序,隊列的特性FIFO(先進先出);

非公平鎖只要CAS設置同步狀態成功,當前執行緒就會獲取到鎖,沒獲取成功的依然放在同步隊列中按FIFO原則等待,等待下一次的CAS操作。

從源碼上可以知道它們的主要區別是多一個判斷:!hasQueuedPredecessors()

該判斷表示:加入了同步隊列中當前節點是否有前驅節點,即在同步隊列中有沒有比當前執行緒更早的執行緒在隊列中等待了,而非公平鎖是沒有這個判斷的

// java.util.concurrent.locks.ReentrantLock.NonfairSync
// 非公平
protected final boolean tryAcquire(int acquires) {
    return nonfairTryAcquire(acquires);

}
// java.util.concurrent.locks.ReentrantLock.Sync
final boolean nonfairTryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    if (c == 0) {
        if (compareAndSetState(0, acquires)) {
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    else if (current == getExclusiveOwnerThread()) {
        int nextc = c + acquires;
        if (nextc < 0) // overflow
            throw new Error("Maximum lock count exceeded");
        setState(nextc);
        return true;
    }
    return false;
}

// java.util.concurrent.locks.ReentrantLock.FairSync
// 公平:比非公平多了一步判斷 !hasQueuedPredecessors()
protected final boolean tryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    if (c == 0) {
        // 主要區別:!hasQueuedPredecessors()
        if (!hasQueuedPredecessors() &&
            compareAndSetState(0, acquires)) {
            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;
}

附上獲取鎖時公平鎖和非公平鎖的源碼區別圖

image

結論二:

公平鎖和非公平鎖的主要區別是:!hasQueuedPredecessors(),表示同步隊列中當前節點是否有前驅節點,即在同步隊列中有沒有比當前執行緒更早的執行緒在隊列中等待了,而非公平鎖沒有這個判斷

2.3.實現鎖的可重入特性

前面在公平鎖與非公平鎖概述這點中,附上了對比兩者的關鍵源碼,其中可重入的源碼是一樣的👇

 ......
 else if (current == getExclusiveOwnerThread()) {
    int nextc = c + acquires;
    if (nextc < 0)
        throw new Error("Maximum lock count exceeded");
    setState(nextc);
    return true;
}

判斷當前執行緒和當前擁有獨佔訪問許可權的執行緒對比,是同一個執行緒則可以重新進入同一把鎖。處理邏輯是:對同步狀態state加上acquires=1,然後返回true,返回true即獲取鎖成功。

AbstractOwnableSynchronizer類用於保存鎖被獨佔的執行緒對象,AOS類只有以下兩個方法:

  • Thread getExclusiveOwnerThread()為獲取當前擁有獨佔訪問許可權的執行緒,

  • void setExclusiveOwnerThread(Thread)為設置當前擁有獨佔訪問許可權的執行緒。

所以每次在獲取鎖成功後會做這麼一步:setExclusiveOwnerThread(current)👇

if (compareAndSetState(0, acquires)) {
    setExclusiveOwnerThread(current);
    return true;
}

ReentrantLock的內部類Sync繼承AQS實現模板方法tryRelease(int) 實現鎖的釋放規則,源碼如下👇方法參數releases=1。

先判斷該執行緒是否為當前擁有獨佔訪問許可權的執行緒,再判斷同步狀態,如果狀態不為0,則鎖還沒釋放完,不執行 setExclusiveOwnerThread(null) 即不釋放獨佔訪問許可權的執行緒。因為發生鎖的重入時,同步狀態state>1,所以鎖釋放時同步狀態需要一層層出來,直到同步狀態為0時,才會置空擁有獨佔訪問權的執行緒。因此AQS的state狀態表示鎖的持有次數。

protected final boolean tryRelease(int releases) {
    int c = getState() - releases;
    if (Thread.currentThread() != getExclusiveOwnerThread())
        throw new IllegalMonitorStateException();
    boolean free = false;
    if (c == 0) {
        free = true;
        setExclusiveOwnerThread(null);
    }
    setState(c);
    return free;
}

結論三:公平和非公平的可重入性都一樣,並且同步狀態state的作用如下

  • 同步狀態state<0 表示throw new Error("Maximum lock count exceeded");

  • 同步狀態state=0 表示鎖沒有被佔用

  • 同步狀態state=1 表示鎖被佔用了

  • 同步狀態state>1 表示鎖發生了重新進入

即同步狀態state等於鎖持有的次數。

2.4.CAS概述

CAS的全稱是Compare And Swap,意思是比較並交換,是一種特殊的處理器指令。

以方法compareAndSetState(int expect,int update)為例:

處理邏輯是:期望參數expect值跟記憶體中當前狀態值比較,等於則原子性的修改state值為update參數值。

獲取鎖操作:compareAndSetState(0, 1),當同步狀態state=0時,則修改同步狀態state=1

compareAndSetState() 方法調用了Unsafe 類下的本地方法compareAndSwapInt(),該方法由JVM實現CAS一組彙編指令,指令的執行必須是連續的不可被中斷的,不會造成所謂的數據不一致問題,但只能保證一個共享變數的原子性操作

同步隊列中還有很多CAS相關方法,比如:

compareAndSetWaitStatus(Node,int,int):等待狀態的原子性修改

compareAndSetHead(Node):設置頭節點的原子性操作

compareAndSetTail(Node, Node):從尾部插入新節點的原子性操作

compareAndSetNext(Node,Node,Node):設置下一個節點的原子性操作

除了同步隊列中提供的CAS方法,在Java並發開發包中,還提供了一系列的CAS操作,我們可以使用其中的功能讓並發編程變得更高效和更簡潔。

java.util.concurrent.atomic一個小型工具包,支援單個變數上的無鎖執行緒安全編程。

比如:num++ 或num–,自增和自減這些操作是非原子性操作的,無法確保執行緒安全,為了提高性能不考慮使用鎖(synchronized、Lock),可以使用AtomicInteger類的方法來完成自增、自減,其本質是CAS原子性操作。

AtomicInteger num = new AtomicInteger(10);
// 自增
System.out.println(num.getAndIncrement());
// 自減
System.out.println(num.getAndDecrement());

注意:只是在自增和自減的過程是原子性操作。

如下程式碼👇下面整塊程式碼是非執行緒安全的,只是num.getAndDecrement()自減時是原子性操作,也即是並發場景下num.get()無法確保獲取到最新值。

private static AtomicInteger num = new AtomicInteger(10);
......
if(num.get()<=0){
    System.out.println("已被搶完,下次再來");
    return;
}
System.out.println("號碼:"+num.getAndDecrement());

支援哪些數據類型呢?

    基本數據類型

  • AtomicBoolean:原子更新布爾值類型

  • AtomicInteger:原子更新整數類型

  • AtomicLong:原子更新長整型

  • 數組類型

  • AtomicIntegerArray:原子更新整型數組裡的元素

  • AtomicLongArray:原子更新長整型數組裡的元素

  • AtomicReferenceArray:原子更新引用類型數組裡的元素

  • 引用類型

  • AtomicReference:原子更新引用類型

  • AtomicMarkableReference:原子更新帶有標記位的引用類型。可以原子更新一個布爾類型的標記位和引用類型。構造方法是AtomicMarkableReference(V initialRef,boolean initialMark)

  • AtomicStampedReference:原子更新帶有版本號的引用類型。該類將整數值與引用關聯起來,可用於原子的更新數據和數據的版本號,可以解決使用CAS進行原子更新時可能出現的ABA問題。

  • 更新類型中的欄位

  • AtomicIntegerFieldUpdater:原子更新整型的欄位的更新器

  • AtomicLongFieldUpdater:原子更新長整型欄位的更新器

  • AtomicReferenceFieldUpdater:原子更新引用類型里的欄位

3.抽象同步隊列AQS

AbstractQueuedSynchronizer 抽象同步隊列,它是個模板類提供了許多以鎖相關的操作,常說的AQS指的就是它。AQS繼承了AbstractOwnableSynchronizer類,AOS用於保存執行緒對象,保存什麼執行緒對象呢?保存鎖被獨佔的執行緒對象

抽象同步隊列AQS除了實現序列化標記介面,並沒有實現任何的同步介面,該類提供了許多同步狀態獲取和釋放的方法給自定義同步器使用,如ReentrantLock的內部類Sync。抽象同步隊列支援獨佔式或共享式的的獲取同步狀態,方便實現不同類型的自定義同步器。一般方法名帶有Shared的為共享式,比如,嘗試以共享式的獲取鎖的方法int tryAcquireShared(int),而獨佔式獲取鎖方法為boolean tryAcquire(int)

AQS是抽象同步隊列,其重點就是同步隊列如何操作同步隊列

3.1同步隊列

雙向同步隊列,採用尾插法新增節點,從頭部的下一個節點獲取操作節點,節點自旋獲取同步鎖,實現FIFO(先進先出)原則。

image

理解節點中的屬性值作用

  • prev:前驅節點;即當前節點的前一個節點,之所以叫前驅節點,是因為前一個節點在使用完鎖之後會解除後一個節點的阻塞狀態;

  • next:後繼節點;即當前節點的後一個節點,之所以叫後繼節點,是因為「後繼有人」了,表示有「下一代」節點承接這個獨有的鎖🔒;

  • nextWaiter:表示指向下一個Node.CONDITION狀態的節點(本文不講述Condition隊列,在此可以忽略它);

  • thread:節點對象中保存的執行緒對象,節點都是配角,執行緒才是主角;

  • waitStatus:當前節點在隊列中的等待狀態

因篇幅原因,關於抽象同步隊列AQS、鎖的獲取過程、鎖的釋放過程、自旋鎖、執行緒阻塞與釋放、執行緒中斷與阻塞關係等內容將在下一篇文章展開講解。

👇圖是新增節點的過程

image

image

Java中的執行緒安全與執行緒同步

Java執行緒狀態(生命周期)–一篇入魂

自己編寫平滑加權輪詢演算法,實現反向代理集群服務的平滑分配

Java實現平滑加權輪詢演算法–降權和提權

Java實現負載均衡演算法–輪詢和加權輪詢

Java全棧學習路線、學習資源和面試題一條龍

更多優質文章,請關注WX公眾號:Java全棧佈道師

image

Tags: