ReentrantLock中的lockInterruptibly、lock、tryLock

ReentrantLock中的lockInterruptibly、lock、tryLock

這篇文章的思路:

  • 首先嚐試着閱讀Java文檔(雙語對照)
  • 瞭解幾個關鍵類之間的關係
  • 通過源代碼來了解lock、lockInterruptibly、tryLock的流程
  • 最後總結三者的區別

Java文檔(雙語)

lock

  1. Acquires the lock unless the current thread is interrupted.

    獲得鎖,除非當前線程被中斷。

  2. Acquires the lock if it is not held by another thread and returns immediately, setting the lock hold count to one.

    如果鎖未被另一個線程持有,則獲取該鎖並立即返回,將鎖持有計數設置爲1。

  3. If the current thread already holds this lock then the hold count is incremented by one and the method returns immediately.

    如果當前線程已經持有此鎖,則保持計數將增加1,並且方法將立即返回。

  4. If the lock is held by another thread then the current thread becomes disabled for thread scheduling purposes and lies dormant until the lock has been acquired, at which time the lock hold count is set to one.

    如果鎖被另一個線程持有,則當前線程將因線程調度而禁用,並處於休眠狀態,直到獲得鎖爲止,此時鎖持有計數設置爲1

**PS:**注意高亮文字,下面要用。


lockInterruptibly

  1. Acquires the lock unless the current thread is interrupted.

    獲得鎖,除非當前線程被中斷。

  2. Acquires the lock if it is not held by another thread and returns immediately, setting the lock hold count to one.

    如果鎖未被另一個線程持有,則獲取該鎖並立即返回,將鎖持有計數設置爲1。

  3. If the current thread already holds this lock then the hold count is incremented by one and the method returns immediately.

    如果當前線程已經持有此鎖,則保持計數將增加1,並且方法將立即返回。

  4. If the lock is held by another thread then the current thread becomes disabled for thread scheduling purposes and lies dormant until one of two things happens:

    如果鎖被另一個線程持有,則當前線程將被禁用以進行線程調度,並處於休眠狀態,直到發生以下兩種情況之一:

    • The lock is acquired by the current thread; or

      鎖由當前線程獲取

    • Some other thread interrupts the current thread.

      其他線程中斷當前線程

  5. If the lock is acquired by the current thread then the lock hold count is set to one.

    如果鎖是由當前線程獲取的,則鎖保持計數設置爲1。

  6. If the current thread:

    如果當前線程:

    • has its interrupted status set on entry to this method; or

      在進入此方法時設置了中斷狀態

    • is interrupted while acquiring the lock,

      或在獲取鎖時被中斷

    then InterruptedException is thrown and the current thread’s interrupted status is cleared.

    那麼會拋出InterruptedException異常,並且會清除當前線程的中斷狀態。

    In this implementation, as this method is an explicit interruption point, preference is given to responding to the interrupt over normal or reentrant acquisition of the lock.

    在這個實現中,由於這個方法是一個顯式的中斷點,所以優先考慮響應中斷,而不是對鎖的正常獲取或可重入獲取。

tryLock

  1. Acquires the lock only if it is not held by another thread at the time of invocation.

    只有在調用時另一個線程未持有鎖時,才獲取鎖。

  2. Acquires the lock if it is not held by another thread and returns immediately with the value true, setting the lock hold count to one. Even when this lock has been set to use a fair ordering policy, a call to tryLock() will immediately acquire the lock if it is available, whether or not other threads are currently waiting for the lock. This “barging” behavior can be useful in certain circumstances, even though it breaks fairness. If you want to honor the fairness setting for this lock, then use tryLock(0, TimeUnit.SECONDS) which is almost equivalent (it also detects interruption).

    如果鎖未被另一個線程持有,則獲取該鎖,並立即返回值true,將鎖持有計數設置爲1。即使此鎖已設置爲使用公平排序策略,對tryLock()的調用也將立即獲取該鎖(如果該鎖可用),無論其他線程當前是否在等待該鎖。這種“討價還價”行爲在某些情況下是有用的,即使它破壞了公平。如果您想遵守這個鎖的公平性設置,那麼使用tryLock(0,TimeUnit.SECONDS),這幾乎是等效的(它還檢測到中斷)。

  3. If the current thread already holds this lock then the hold count is incremented by one and the method returns true.

    如果當前線程已經持有此鎖,則保持計數將增加1,並且方法返回true。

  4. If the lock is held by another thread then this method will return immediately with the value false.

    如果這個鎖被另一個線程持有,那麼這個方法會立即返回false。

核心類之間的關係

可能看到上邊官方文檔寫的有點晦澀。

說簡單點就是:

  • lock不會去相應中斷。
  • lockInterruptibly會相應中斷。

接下來我們看看源碼:

看源碼之前,先了解一下幾個關鍵類(syncReentrantLockFairSyncNonfairSync)的關係。

  • syncextendsAQS的抽象靜態內部類。

  • SyncNonfairSyncFairSync都是ReentrantLock的內部類。

  • NonfairSyncFairSync都繼承Sync

在這裏插入圖片描述

UML圖

閱讀源碼

知道了幾個關鍵類的關係之後,我們就可以看locklockInterruptibly的源碼了:

Lock

lock的代碼實現:

// lock方法調用的是Sync實現類的lock方法(因爲lock()方法在Sync中是抽象方法)
public void lock() {
    sync.lock();
}

由於Sync有兩個實現類,FairSyncNonfairSync兩個實現類(區別就是是否爲公平鎖)

由於需要照顧篇幅的緣故,在這裏就拿NonfairSync來說,她是非公平鎖,跟synchronized一樣。

NonfairSync中的lock()先利用CAS嘗試改變State的值,如果返回true,將當前線程設置爲獨佔線程,如果返回false,則調用acquire()方法

static final class NonfairSync extends Sync {
    
    final void lock() {
        // 1. 先介紹一下 compareAndSetState
        // 它利用CAS來改變state的值(state是在AQS中的,NonfairSync繼承Sync繼承AQS)
        // 簡述CAS:(如果當前內存值等於期望值,那就更新值並返回true;如果不等,返回false)
        // 也就是說,如果state在內存中是0,那麼就把他修改爲1,並return true。
        // 否則,直接返回false。
        // 2. ReentrantLock中的state表示線程重入鎖的次數。
        if (compareAndSetState(0, 1))
            // 將當前線程設置爲獨佔線程。
            setExclusiveOwnerThread(Thread.currentThread());
        else
            // 以獨佔模式獲取,忽略中斷。通過調用至少一次tryAcquire來實現,成功時返回。否則線程將排隊,可能會重複阻塞和解除阻塞,調用tryAcquire直到成功。
            acquire(1);
    }
    省略......
}
public abstract class AbstractQueuedSynchronizer{   
    // 第一個參數爲期望值
    // 第二個參數是如果期望值與內存中的值相等的話,需要設置的值。
    protected final boolean compareAndSetState(int expect, int update) {
        // See below for intrinsics setup to support this
        // 第一個參數是當前對象,第二個是state變量的內存偏移量,利用這兩個參數可以找到內存中的值。
        // unsafe是一個底層操作類,有大量的native方法。
        return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
    }
    
    public final void acquire(int arg) {
        // 這是一個短路操作
        // 先調用tryAcquire看同步狀態是否獲取成功
        // 若成功,返回true,那麼if中的整個表達式就是false,那麼acquire方法就執行結束,
        // 若失敗,返回false,接下來再執行addWaiter()、acquireQueued()方法
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }
   	省略.....
}

因篇幅問題,這裏只簡單介紹AQS中的addWaiter()acquireQueued()方法。

  • addWaiter()將當前線程構造爲一個Node結點,加入到AQS中的同步隊列裏。

  • acquireQueued()

    Acquires in exclusive uninterruptible mode for thread already in queue. Used by condition wait methods as well as acquire.

    在隊列中的線程會以不間斷、獨佔的模式去獲取鎖。

總結一下lock()的方法的流程:

  • 先用compareAndSetState(int expect, int value)去嘗試修改同步狀態(AQS.state)

    • 若修改成功(即獲取到鎖),調用setExclusiveOwnerThread(Thread t)設置當前線程擁有獨佔式的訪問全限,lock()方法執行結束。
    • 若修改失敗(未獲得鎖),調用acquire()方法
  • acquire()方法會先調用tryAcquire看同步狀態是否獲取成功。

    • 若成功(即爲拿到鎖),返回true。那麼if中的整個表達式就是false,那麼acquire方法就執行結束。
    • 若失敗(未拿到鎖),返回false,接下來再執行addWaiter()acquireQueued()方法。
  • addWaiter()將當前線程構造爲一個Node結點,加入到AQS中的同步隊列裏。

  • acquireQueued()內部利用for(;;;)死循環去嘗試獲得鎖,

    • 若獲得到鎖,那就把該Node從隊列中移除,並返回false。那麼acquire()調用就結束了
    • 若沒有獲得到鎖,那就把節點狀態改爲SIGNAL,然後調用LookSupport.park方法使得當前線程阻塞。
      • 當AQS隊列中前驅節點被釋放他纔會繼續執行。

到此Lock()詳解方法結束!(還不算是特別詳細,想再深入的可以讀讀AQS的源碼)


lockInterruptibly

lockInterruptibly的代碼實現:

// ReentrantLock中的lockInterruptibly()方法
// 調用的是AQS的acquireInterruptibly()方法(因爲Sync繼承AQS)
public void lockInterruptibly() throws InterruptedException {
    sync.acquireInterruptibly(1);
}
// AQS的節選代碼
public abstract class AbstractQueuedSynchronizer{

    public final void acquireInterruptibly(int arg) throws InterruptedException {
        // 測試當前線程是否已中斷,如果是則拋出異常
        if (Thread.interrupted())
            throw new InterruptedException();
        // 如果當前線程沒有中斷,嘗試去獲取同步狀態,
        // 如果獲取成功(拿到鎖),方法調用結束
        // 如果獲取失敗(沒有拿到鎖),方法調用doAcquireInterruptibly()
        if (!tryAcquire(arg))
            doAcquireInterruptibly(arg);
	}
    
    // 該方法和上邊的acquireQueued()類似
    private void doAcquireInterruptibly(int arg)
        throws InterruptedException {
        // 利用addWaiter()方法爲當前線程創建一個結點,並添加到AQS的隊列中
        final Node node = addWaiter(Node.EXCLUSIVE);
        boolean failed = true;
        try {
            // 死循環
            for (;;) {
                // 獲取到當前線程結點的前驅節點
                final Node p = node.predecessor();
                // 如果p是頭部節點的話,嘗試獲取鎖
                if (p == head && tryAcquire(arg)) {
                    // p獲取鎖成功之後,把當前線程結點設置爲頭部節點。然後把p結點移除。
                    setHead(node);
                    // 把p結點設置爲null,利用垃圾回收
                    p.next = null; // help GC
                    failed = false;
                    return;
                }
                // shouldParkAfterFailedAcquire()方法
                // 利用CAS將當前線程結點狀態改爲SIGNAL,表示當前線程堵塞。
                // 1.若修改狀態成功,則會執行parkAndCheckInterrupt()使得當前線程堵塞。此時會
                // 拋出異常
                // 2.若修改狀態失敗,因短路,會繼續循環(此時線程沒有堵塞)。
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    throw new InterruptedException();
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }
    省略....
}

tryLock

// 只獲取一次鎖,成功則返回true,失敗返回false
public boolean tryLock() {
    return sync.nonfairTryAcquire(1);
}
abstract static class Sync extends AbstractQueuedSynchronizer {
    final boolean nonfairTryAcquire(int acquires) {
        final Thread current = Thread.currentThread();
        int c = getState();
        // 沒有獲取過鎖
        if (c == 0) {
            // 嘗試獲取鎖
            if (compareAndSetState(0, acquires)) {
                // 將當前線程設置爲獨佔線程。
                setExclusiveOwnerThread(current);
                return true;
            }
        }
        // 判斷當前線程是否就是setExclusiveOwnerThread()上次設置的線程
        // 判斷是否爲可重入鎖
        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;
    }
}

三者的區別

lockInterruptibly()測試:

public static void main(String[] args) {
    ReentrantLock lock = new ReentrantLock();
    Thread thread = new Thread(() -> {
        System.out.println("準備進入同步區");
        try {
            lock.lockInterruptibly();
            System.out.println("執行同步代碼");
        } catch (InterruptedException e) {
            System.out.println("線程被中斷!");
        }
    });
    // 讓main線程獲取鎖,讓thread線程獲取鎖失敗。
    lock.lock();
    // 啓動線程
    thread.start();
    // 中斷線程
    thread.interrupt();
}

輸出結果:

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-zr3omoTo-1584182737102)(D:\Users\Desktop\文本\A-面試準備\ReentrantLock中的lockInterruptibly和lock.assets\image-20200314174849867.png)]

lockInterruptibly()方法優先相應中斷

如果在上邊的例子中debug,會發現thread線程在acquireInterruptibly()拋出異常處。

在這裏插入圖片描述


lock測試:

public static void main(String[] args) throws InterruptedException {
    ReentrantLock lock = new ReentrantLock();
    Thread thread = new Thread(() -> {
        System.out.println("準備進入同步區");
        lock.lock();
        System.out.println("執行同步代碼");
    });
    lock.lock();
    thread.start();
    thread.interrupt();
}

輸出結果:

在這裏插入圖片描述

lock()方法不相應中斷(會一直卡着)

tryLock測試:

public static void main(String[] args) throws InterruptedException {
    ReentrantLock lock = new ReentrantLock();
    Thread thread = new Thread(() -> {
        int i = 0;
        System.out.println("準備進入同步區");
        while(!lock.tryLock()){
            System.out.println("等待" + i++ + "次");
        }
        System.out.println(lock.isLocked());
        System.out.println("執行同步代碼");
        lock.unlock();
    });
    lock.lock();
    thread.start();
    Thread.sleep(1);
    lock.unlock();
}

結果:

準備進入同步區
等待0次
等待1.......//省略
    
等待52次
等待53true
執行同步代碼

總結:

  • lockInterruptibly()方法調用後一直阻塞到獲得鎖,但優先響應中斷信號。
  • lock()方法調用後一直阻塞直到獲取鎖。
  • tryLock()嘗試是否能獲得鎖,如果不能獲得立即返回。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章