ReentrantLock原始碼

Jame!發表於2021-07-29

ReentrantLock原始碼

  • JUC 指java.util.concurrent包下,一系列關於併發的類,JUC就是包名的首字母

  • CAS 比較並交換,可以看另一篇文章

  • AQS 指主要利用CAS來實現的輕量級多執行緒同步機制,並且不會在CPU上出現上下文切換和排程的情況

自定義鎖

如何在自己實現一個鎖?

可以定義一個屬性來判斷當前是否有其執行緒在執行,如果正在執行那麼其他執行緒需要等待

如何實現? 例如有兩個執行緒T1和T2,都執行同一段程式碼

自定義兩個方法

public void lock();
public void unlock();
public void addI(){
    i++;
}
將上面的addI方法更改為下面的
public void addI(){
    lock();
    i++;
    unlock();
}   

這裡忽略程式出錯導致死鎖的情況,正常解鎖需要放在finally程式碼塊中

當T1進入程式碼,將鎖的改為被持有的狀態

/**
*  0為未持有
*  1為被持有
*/
private volatile int i=0;

public void lock(){
    //CAS修改成功返回true
    while(CAS(i,1)){
        return
    }
}

public void unlock(){
   i=0;
}

上面的虛擬碼當T1進入lock方法後,因為是第一個進入的,鎖的狀態還是0,通過cas可以改為1,修改成功返回true,進入迴圈return到addI方法,執行i++操作,然後進入unLock方法,將狀態改為0,方法結束

假設當T1進入方法將狀態改為1,那麼T2進入會一直迴圈CAS修改,執行緒一直在自旋不會走下面的程式碼,直到鎖的狀態改為0,才會繼續業務程式碼

那麼我們就實現了一個簡單的鎖,但是這個鎖有什麼缺點呢? 沒有獲取到鎖的執行緒會一直自旋,消耗系統資源,這個是我們不想看到的

在java中還有一個類LockSupport,其中有一個park方法

public static void park() {
    UNSAFE.park(false, 0L);
}

public native void park(boolean var1, long var2);

裡面繼續呼叫UNSAFE類,這個類裡的方法是使用C/C++實現,park方法的作用是將當前執行緒立即休眠,讓出CPU,直到被喚醒,還有一個喚醒的方法

public static void unpark(Thread thread) {
    if (thread != null)
        UNSAFE.unpark(thread);
}

public native void unpark(Object var1);

這個同樣也是其他語言實現,傳入需要被喚醒的執行緒,那麼我們上面的程式碼可以改造為

/**
*  0為未持有
*  1為被持有
*/
private volatile int i=0;

//存放等待獲取鎖的執行緒
private Thread t;

public void lock(){
    //CAS修改成功返回true
    if(CAS(i,1)){
        return
    }
    //將沒有獲取到鎖的執行緒存放
    t=Thread.currentThread()
    //如果沒有獲取到鎖則進行休眠
    LockSupport.park();
    
}

public void unlock(){
   i=0;
   if(t!=null){ 
       LockSupport.unpark(t);
   }
}

我們修改完後即使沒有獲取到鎖的執行緒也不會佔用CPU的資源,但是如果出現2個以上的執行緒同時進行操作,那麼會出現丟失執行緒的情況,可以再進行優化,將等待的執行緒存放到佇列中,就不再演示了,而ReentrantLock就是主要使用CAS,park,自旋來實現的,接下來看ReentrantLock的原始碼

ReentrantLock

當初始化一個ReentrantLock使用預設構造時建立的是一個非公平鎖

public ReentrantLock() {
    sync = new NonfairSync();
}

如果想建立一個公平鎖則使用有參構造

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

這篇文章先來看公平鎖的實現

public void service() {
    //建立一個公平鎖
    ReentrantLock reentrantLock = new ReentrantLock(true);
    reentrantLock.lock();
    try {
        System.out.println("==這裡有一堆的業務===");
    } catch (Exception e) {
        e.printStackTrace();
    } finally {
        reentrantLock.unlock();
    }
}

加鎖

沒有競爭情況

public void lock() {
    sync.lock();
}
呼叫的sync是一個ReentrantLock的內部抽象類
abstract static class Sync extends AbstractQueuedSynchronizer{
    ......
}

它的公平鎖的實現方法,是FairSync類中的,也是一個內部類,在ReentrantLock中,繼承了Sync類,實現lock方法

static final class FairSync extends Sync {
    final void lock() {
        acquire(1);
    }
}
public final void acquire(int arg) {
    if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}

點進tryAcquire方法

protected final boolean tryAcquire(int acquires) {
    //獲取當前執行的執行緒
    final Thread current = Thread.currentThread();
    //得到鎖的狀態
    int c = getState();
    //如果鎖狀態為0說明當前鎖沒有被其他執行緒持有
    if (c == 0) {
        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;
}

繼續點進hasQueuedPredecessors方法,該方法定義在AbstractQueuedSynchronizer抽象類中的

public final boolean hasQueuedPredecessors() {
    // The correctness of this depends on head being initialized
    // before tail and on head.next being accurate if the current
    // thread is first in queue.
    Node t = tail; // Read fields in reverse initialization order
    Node h = head;
    Node s;
    return h != t &&
        ((s = h.next) == null || s.thread != Thread.currentThread());
}

其中tail和head這兩個變數是在AbstractQueuedSynchronizer抽象類中定義的,用來存放等待執行緒頭和尾部

因為當前執行緒執行前鎖的狀態是未被持有的,所以還沒有初始化過佇列,那麼等待佇列的頭和尾部都為null,return的第一個判斷h!=t為false,後面的&&運算子,所以直接返回

那麼回到tryAcquire方法,hasQueuedPredecessors返回false,而前面有一個取反!符號,則繼續執行compareAndSetState(0, acquires)方法,通過cas改變當前鎖的狀態為1,然後執行setExclusiveOwnerThread方法,該方法就是簡單的賦值

protected final void setExclusiveOwnerThread(Thread thread) {
    //當前持有鎖的執行緒
    exclusiveOwnerThread = thread;
}

繼續返回到acquire方法,為true,取反false,使用了&&阻斷符,則不會執行後面的acquireQueued方法,直接結束lock()方法,執行自定義的業務程式碼

tryAcquire方法什麼時候走到 else if (current == getExclusiveOwnerThread()) 判斷呢

ReentrantLock的特性之一就是體現在這裡-重入鎖

啥叫重入鎖?簡單講就是在加鎖後又加鎖

public void addI(){
    ReentrantLock rLock =new ReentrantLock(true);
    rLock.lock();
    
    //執行業務==
    
    rLock.lock();
    
    //執行業務==
    
    //解鎖最後加鎖的
    rLock.unlock();
    //解鎖最先加鎖的
    rLock.unlock();
}

當執行緒和該鎖已經持有的執行緒相同時則會進入這個判斷,將鎖的狀態加1,賦值給state,下面的判斷state小於0可能是判斷溢位的問題,即數值超出int型別最大容量則為負數,一般這種情況很少見吧

存在競爭情況

那麼上面是沒有其他執行緒競爭的情況,如果在T1加鎖後,T2,T3..來嘗試獲取鎖改怎麼辦呢?->進等待佇列

這個還是tryAcquire方法的程式碼,拿下來方便檢視

protected final boolean tryAcquire(int acquires) {
   //獲取當前執行的執行緒
   final Thread current = Thread.currentThread();
   //得到鎖的狀態
   int c = getState();
   //如果鎖狀態為0說明當前鎖沒有被其他執行緒持有
   if (c == 0) {
       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;
}

如果在T1進行完加鎖後T2來嘗試獲取鎖,因為state狀態不為0,而當前執行緒和鎖持有的執行緒又不同,則直接返回false

那麼返回acquire方法中

public final void acquire(int arg) {
    if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}

Node.EXCLUSIVE 返回一個Node節點

取反為true,則執行acquireQueued方法,而acquireQueued方法中有執行了addWaiter方法,先來看addWaiter方法

private Node addWaiter(Node mode) {
    Node node = new Node(Thread.currentThread(), mode);
    Node pred = tail;
    if (pred != null) {
        node.prev = pred;	
        if (compareAndSetTail(pred, node)) {
            pred.next = node;
            return node;
        }
    }
    enq(node);
    return node;
}

使用連結串列的形式來儲存阻塞排隊的執行緒,來看node的內部結構

主要的三個屬性

//存放上一個節點
volatile Node prev;
//存放下一個節點
volatile Node next;
//存放當前等待的執行緒
volatile Thread thread;
Node(Thread thread, Node mode) {     // Used by addWaiter
    this.nextWaiter = mode;
    this.thread = thread;
}

當進入這個方法後,首先將AbstractQueuedSynchronizer類中的尾部節點賦值給一個臨時變數,判斷尾部是否為空,假設現線上程為T2,佇列還沒有被初始化,尾部為空,則進入enq方法,繼續點進

private Node enq(final Node node) {
    for (;;) {
        Node t = tail;
        if (t == null) { 
            //CAS設定頭節點
            if (compareAndSetHead(new Node()))
                tail = head;
        } else {
            node.prev = t;
            //CAS設定尾巴節點
            if (compareAndSetTail(t, node)) {
                t.next = node;
                return t;
            }
        }
    }
}

還是將AbstractQueuedSynchronizer類中尾部節點賦值給臨時變數t 然後判斷t是否為空,因為佇列還沒有初始化,所以尾巴節點為空,則使用cas來設定 AbstractQueuedSynchronizer類中的頭節點,之後將設定的頭節點賦值給尾部

當執行完節點的關係如下

這時候有個疑問,怎麼沒有設定傳入的Node節點呢?而是設定新new出來的Node,和引數傳入的Node節點沒有一點關係?

注意看上面的程式碼for(;;) 死迴圈,當下次迴圈的時候t已經不為空了,因為上次迴圈給加了一個空節點,然後將傳入的Node節點的上一個賦值為t,然後通過CAS獲取AbstractQueuedSynchronizer類中的尾部節點,如果尾部節點還是為t,則更改為傳入的node物件,如果CAS失敗,即在CAS設定前被其他執行緒對AbstractQueuedSynchronizer類中的尾部節點進行了修改,則進行下一次for迴圈,直至設定成功,當操作完成後,節點結構如下圖

之後程式碼返回到acquireQueued(addWaiter(Node.EXCLUSIVE), arg))方法

final boolean acquireQueued(final Node node, int arg) {
    boolean failed = true;
    try {
        boolean interrupted = false;
        for (;;) {
            final Node p = node.predecessor();
            if (p == head && tryAcquire(arg)) {
                setHead(node);
                p.next = null; // help GC
                failed = false;
                return interrupted;
            }
            if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

還是一個for死迴圈,首先獲取上一個節點和AbstractQueuedSynchronizer類中的頭節點進行判斷,如果相同則呼叫tryAcquire()方法嘗試獲取鎖,因為在初始化佇列過程中可能獲取鎖執行的執行緒已經執行完了,並且釋放了鎖,所以這裡嘗試一下獲取鎖,假設沒有獲取到鎖,則不會進入if (p == head && tryAcquire(arg)) {}程式碼塊,繼續下面的判斷,進入shouldParkAfterFailedAcquire()方法,從名稱可以看到[在獲取鎖失敗後應該睡眠]

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    int ws = pred.waitStatus;
    if (ws == Node.SIGNAL)
        return true;
    if (ws > 0) {
        do {
            node.prev = pred = pred.prev;
        } while (pred.waitStatus > 0);
        pred.next = node;
    } else {
        compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
    }
    return false;
}

判斷上一個node節點的狀態,將上一個節點的Node.SIGNAL狀態的值為-1,而上面的程式碼中並沒有對waitStatus的值進行更改,預設初始化為0,則進入最後的else程式碼塊,通過CAS將waitStatus的值改為-1,方法返回false結束,回到acquireQueued方法中,繼續進行for迴圈,假設還是沒有獲取到鎖,則再次進入shouldParkAfterFailedAcquire方法中,因為上次for迴圈將waitStatus的值改為了-1,則這次進入了if (ws == Node.SIGNAL)的程式碼塊,返回true,返回到 if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt())判斷中,因為shouldParkAfterFailedAcquire方法返回了true,則繼續執行parkAndCheckInterrupt方法

private final boolean parkAndCheckInterrupt() {
    LockSupport.park(this);
    return Thread.interrupted();
}

當執行完parkAndCheckInterrupt方法後,T2執行緒就在這裡進行休眠

為什麼不開始就把waitStatus設定為-1呢?還要多自旋一次,有一個原因是儘量不使用park,能嘗試獲取到鎖最好

那麼假設現在又來一個執行緒T3

public final void acquire(int arg) {
    //嘗試獲取鎖肯定不會成功,則進入acquireQueued,addWaiter方法
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}
private Node addWaiter(Node mode) {
    Node node = new Node(Thread.currentThread(), mode);
    //這時tail已經是t2節點了
    Node pred = tail;
    //不為空進入
    if (pred != null) {
        //將當前節點上一個節點設定為t2
        node.prev = pred;
        //通過CAS來設定AQS類中的尾節點
        if (compareAndSetTail(pred, node)) {
            //然後設定T2的下一個節點
            pred.next = node;
            return node;
        }
    }
    enq(node);
    return node;
}

完成操作後節點關係如下

之後繼續執行acquireQueued方法

final boolean acquireQueued(final Node node, int arg) {
    boolean failed = true;
    try {
        boolean interrupted = false;
        for (;;) {
            //獲取上一個節點:T2
            final Node p = node.predecessor();
            //T2不是頭節點,則不進入下面的程式碼塊
            if (p == head && tryAcquire(arg)) {
                setHead(node);
                p.next = null; // help GC
                failed = false;
                return interrupted;
            }
           	//之後呼叫shouldParkAfterFailedAcquire方法
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    //同樣的程式碼,第一次獲取T3的前一個節點T2,判斷T2的ws值為0,
    //CAS修改後返回,外層迴圈再次進入這時T2的ws值為-1,返回true,方法結束
    int ws = pred.waitStatus;
    if (ws == Node.SIGNAL)
        return true;
    if (ws > 0) {
        do {
            node.prev = pred = pred.prev;
        } while (pred.waitStatus > 0);
        pred.next = node;
    } else {
        compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
    }
    return false;
}

解鎖

假設現在T1執行unlock方法,T2,T3在佇列中

public void unlock() {
    sync.release(1);
}
public final boolean release(int arg) {
    if (tryRelease(arg)) {
        Node h = head;
        if (h != null && h.waitStatus != 0)
            unparkSuccessor(h);
        return true;
    }
    return false;
}

進入tryRelease方法

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;
}

首先將狀態數值-1,判斷如果當前執行緒和持有鎖的執行緒不是同一個則丟擲異常,即解鎖的執行緒和加鎖的不是同一個執行緒

判斷如果c==0,也就是沒有重入鎖的情況,將free改為true,然後進入setExclusiveOwnerThread方法

protected final void setExclusiveOwnerThread(Thread thread) {
    exclusiveOwnerThread = thread;
}

protected final void setState(int newState) {
    state = newState;
}

方法返回,沒有重入鎖的情況,則free為true,獲取AQS類中的頭節點,假設不為空,ws=-1,則進入unparkSuccessor(h)方法

private void unparkSuccessor(Node node) {
    int ws = node.waitStatus;
    if (ws < 0)
        compareAndSetWaitStatus(node, ws, 0);
    Node s = node.next;
    if (s == null || s.waitStatus > 0) {
        s = null;
        for (Node t = tail; t != null && t != node; t = t.prev)
            if (t.waitStatus <= 0)
                s = t;
    }
    if (s != null)
        LockSupport.unpark(s.thread);
}

首先獲取頭結點的狀態,小於0進入程式碼塊,將頭結點的鎖狀態改為0,獲取下一個節點,那麼s就是t2,而t2的ws也是-1,所以直接進入最下面的程式碼塊,if(s!=null),unpark(t2)執行緒

那麼回到t2執行緒休眠的地方

private final boolean parkAndCheckInterrupt() {
    LockSupport.park(this);
    //在這裡醒來
    return Thread.interrupted();
}

下面的是判斷執行緒是否被中斷過,native方法,無法看到實現了,那麼假設沒有被中斷過則返回false,那麼返回上一個方法

final boolean acquireQueued(final Node node, int arg) {
    boolean failed = true;
    try {
        boolean interrupted = false;
        for (;;) {
            final Node p = node.predecessor();
            if (p == head && tryAcquire(arg)) {
                setHead(node);
                p.next = null; // help GC
                failed = false;
                return interrupted;
            }
            //返回程式碼後繼續執行這裡
            if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

因為parkAndCheckInterrupt方法返回false,所以進不去程式碼塊,那麼繼續執行for,當執行if (p == head && tryAcquire(arg))時p==head成立,而呼叫tryAcquire方法嘗試獲取鎖成功,因為t1已經釋放了,那麼進入下面的程式碼塊

if (p == head && tryAcquire(arg)) {
    setHead(node);
    //設定t2上一個節點,也就是空節點的下一個節點設定為null
    p.next = null; // help GC p節點沒有任何引用指向了,幫助垃圾回收
    failed = false;
    return interrupted;
}

private void setHead(Node node) {
    //將t2節點設定為頭部
    head = node;
    //然後將t2節點的thread設定為null
    node.thread = null;
    //節點的上一個節點設定為null
    node.prev = null;
}

經過上面的操作後節點關係如下

如果這個節點在頭說明它正在執行程式碼,而不是排隊,即使初始化時T1沒有進佇列,但是給它新增了一個空node,來代替它正在執行

例如有T2,T3在排隊,T1執行緒unpark後T2執行緒執行,上面的程式碼也能說明T2會先把當前節點的執行緒,上下節點都設定為null,而T2執行緒去執行程式碼去了,已經在執行過程中了

看別的部落格有一段解釋:比如你去買車票,你如果是第一個這個時候售票員已經在給你服務了,你不算排隊,你後面的才算排隊

注意一點:佇列頭始終為空Node

如何保證公平

情況1

T1執行完unpark後,釋放完鎖,還沒來的及喚醒佇列中的T2,這時T3執行緒來嘗試獲取到鎖

public final boolean hasQueuedPredecessors() {
    Node t = tail; 
    Node h = head;
    Node s;
    return h != t && 
        ((s = h.next) == null || s.thread != Thread.currentThread());
}

這種情況佇列中肯定有節點排隊,如果沒有節點直接獲取到鎖也是公平的,那麼有節點排隊h就不等於t,true,&&運算子繼續判斷,h的next節點也不為null,返回false

s.thread != Thread.currentThread() 如果當前來嘗試獲取鎖的物件不是在排隊的第一個(也就是頭結點的下一個節點,頭結點正在執行,不算在排隊的佇列中)也就是其他執行緒插隊的情況,則返回true,結果就是(true&&(false||true)) 整體返回true,外層程式碼取反為false,不會嘗試CAS獲取鎖,則T3去排隊

情況2

T2嘗試獲取鎖時發現T1持有鎖,於是去初始化佇列,在初始化過程中T1執行完釋放鎖,T2執行初始化佇列程式碼時間片用完,這時T3來嘗試獲取鎖

private Node enq(final Node node) {
    for (;;) {
        Node t = tail;
        if (t == null) { 
            if (compareAndSetHead(new Node()))<------假設T2初始化佇列執行到這裡CPU時間片用完
                tail = head;
        } else {
            node.prev = t;
            if (compareAndSetTail(t, node)) {
                t.next = node;
                return t;
            }
        }
    }
}

此時節點關係如下

那麼回到hasQueuedPredecessors方法,看最後的return

return h != t && 
    ((s = h.next) == null || s.thread != Thread.currentThread());

h頭節點為一個空node,而t為節點為null,不等於true繼續判斷,h頭結點下一個為null,整體返回true,外層程式碼取反為false,則去排隊

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

遺留問題

1 初始化佇列以及後面的入隊為什麼要設定空的頭節點

2 在parkAndCheckInterrupt()方法中最後呼叫的Thread.interrupted();一系列方法最後不改變任何東西,不明白它這個的作用,也有說是為了複用lockInterruptibly()方法,但是感覺有點牽強

太笨了看不明白,希望不吝賜教,也可以加qq群一起探討:737698533

相關文章