天天看點

誰是代碼界3%的王者?- 第五題Lock的簡單解讀

 一、背景

阿裡技術的公衆發了一篇文章

《誰是代碼界3%的王者?》

提到“在Java代碼界,有些陷阱外表看起來是個青銅實際上是王者,據說97%工程師會被“秒殺””

給出了5道題,非常考驗基礎。

本文簡單解讀第五題。

二、代碼

給出的示例代碼:

public class LockTest {
    private final static Lock lock = new ReentrantLock();
    public static void main(String[] args) {
        try {
            lock.tryLock();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
}      
誰是代碼界3%的王者?- 第五題Lock的簡單解讀

給出的問題:

下列哪種說法是錯誤的:

A: lock是非公平鎖

B: finally代碼塊不會抛出異常

C: tryLock擷取鎖失敗則直接往下執行

答案應該是B(不過問題和源碼并不是很比對)。

三、解讀

我們改造一些上面的源代碼,新增一個子線程,讓子線程先擷取鎖,我們看會發生什麼:

import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class LockTest {
    private final static Lock lock = new ReentrantLock();
    public static void main(String[] args) throws InterruptedException {
        
        Thread thread = new Thread(() -> {
            if (lock.tryLock()) {
                System.out.println("子線程擷取鎖"+"成功");
                try {
                    TimeUnit.SECONDS.sleep(6);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    System.out.println("子線程釋放鎖");
                    lock.unlock();
                }
            }
        });
        thread.start();
        
        TimeUnit.SECONDS.sleep(1);
        try {
            // 有可能沒擷取到鎖
            boolean tryLock = lock.tryLock();
            System.out.println("主線程擷取鎖"+(tryLock?"成功":"失敗"));
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            // 有可能沒擷取到鎖調用了釋放
            System.out.println("主線程釋放鎖");
            lock.unlock();
        }
    }
}      
誰是代碼界3%的王者?- 第五題Lock的簡單解讀

運作結果:

子線程擷取鎖成功

主線程擷取鎖失敗

主線程釋放鎖

Exception in thread "main" java.lang.IllegalMonitorStateException

    at java.util.concurrent.locks.ReentrantLock$Sync.tryRelease(ReentrantLock.java:151)

    at java.util.concurrent.locks.AbstractQueuedSynchronizer.release(AbstractQueuedSynchronizer.java:1261)

    at java.util.concurrent.locks.ReentrantLock.unlock(ReentrantLock.java:457)

    at com.chujianyun.common.style.LockTest.main(LockTest.java:41)

子線程釋放鎖

接下來我們分析原因。

先看A選項: lock是非公平鎖,我們看到lock的類型為ReentrantLock。

還是老規矩,源代碼代碼:

/**
     * Creates an instance of {@code ReentrantLock}.
     * This is equivalent to using {@code ReentrantLock(false)}.
     */
    public ReentrantLock() {
        sync = new NonfairSync();
    }      
誰是代碼界3%的王者?- 第五題Lock的簡單解讀

很明顯預設的空參數構造方法建立的的确是非公平鎖。

另外我們發現它還提供了構造方法支援指定是否為公平鎖。

/**
     * Creates an instance of {@code ReentrantLock} with the
     * given fairness policy.
     *
     * @param fair {@code true} if this lock should use a fair ordering policy
     */
    public ReentrantLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
    }      
誰是代碼界3%的王者?- 第五題Lock的簡單解讀

公平鎖和非公平鎖的主要差別:

誰是代碼界3%的王者?- 第五題Lock的簡單解讀
誰是代碼界3%的王者?- 第五題Lock的簡單解讀

更詳細内容參見

《不可不說的Java“鎖”事》

再看C選項tryLock擷取鎖失敗則直接往下執行。

繼續看源碼

java.util.concurrent.locks.ReentrantLock#tryLock()

/**
     * Acquires the lock only if it is not held by another thread at the time
     * of invocation.
     *
     * <p>Acquires the lock if it is not held by another thread and
     * returns immediately with the value {@code true}, setting the
     * lock hold count to one. Even when this lock has been set to use a
     * fair ordering policy, a call to {@code tryLock()} <em>will</em>
     * immediately acquire the lock if it is available, whether or not
     * other threads are currently waiting for the lock.
     * This &quot;barging&quot; 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
     * {@link #tryLock(long, TimeUnit) tryLock(0, TimeUnit.SECONDS) }
     * which is almost equivalent (it also detects interruption).
     *
     * <p>If the current thread already holds this lock then the hold
     * count is incremented by one and the method returns {@code true}.
     *
     * <p>If the lock is held by another thread then this method will return
     * immediately with the value {@code false}.
     *
     * @return {@code true} if the lock was free and was acquired by the
     *         current thread, or the lock was already held by the current
     *         thread; and {@code false} otherwise
     */
    public boolean tryLock() {
        return sync.nonfairTryAcquire(1);
    }      
誰是代碼界3%的王者?- 第五題Lock的簡單解讀

從源碼裡可以看出失敗會傳回false,而且沒有抛異常。

另外看Lock接口的注釋可以發現,原示例中的用法不規範:

java.util.concurrent.locks.Lock#tryLock()

誰是代碼界3%的王者?- 第五題Lock的簡單解讀

上面的專門給出了鎖的典型用法:

Lock lock = ...;
if (lock.tryLock()) {
 try {
   // manipulate protected state
 } finally {
   lock.unlock();
 }
} else {
   // perform alternative actions
 }      
誰是代碼界3%的王者?- 第五題Lock的簡單解讀

從代碼我們可以看出,我們通過lock.tryLock的傳回值判斷加鎖是否成功,來分别執行成功和失敗的邏輯。

并且加鎖成功的邏輯中解鎖的代碼要放到finally代碼塊中保證異常仍然會釋放鎖。

另外從改造後的例子可以看出,lock.tryLock擷取鎖失敗會繼續執行。

我們再看B選項: finally代碼塊不會抛出異常

根據我們改造的源碼可以看到,finally代碼塊可能抛出IllegalMonitorStateException。

同樣的我們看源碼:

java.util.concurrent.locks.ReentrantLock#unlock

/**
     * Attempts to release this lock.
     *
     * <p>If the current thread is the holder of this lock then the hold
     * count is decremented.  If the hold count is now zero then the lock
     * is released.  If the current thread is not the holder of this
     * lock then {@link IllegalMonitorStateException} is thrown.
     *
     * @throws IllegalMonitorStateException if the current thread does not
     *         hold this lock
     */
    public void unlock() {
        sync.release(1);
    }      
誰是代碼界3%的王者?- 第五題Lock的簡單解讀

這裡寫的很清楚,如果目前線程并不持有此鎖時,調用unlock會報IllegalMonitorStateException。

四、拓展

4.1 正确用法

我們去java.util.concurrent.locks.ReentrantLock的源碼頂部看注釋發現,官方給出了一個标準用法範例:

class X {
   private final ReentrantLock lock = new ReentrantLock();
  // ...
   public void m() {
     lock.lock();  // 直到條件滿足,否則一直阻塞
     try {
       // ... 函數體
     } finally {
       lock.unlock()
    }
   }
 }}      
誰是代碼界3%的王者?- 第五題Lock的簡單解讀

為什麼不把lock.lock()放在try裡呢??

因為如果放到try内,且前面有其他代碼出現異常,并沒有擷取到鎖,則finnaly解鎖會抛異常。

4.2 lock.lock()擷取失敗後面會不會執行?

java.util.concurrent.locks.ReentrantLock#lock

/**
     * Acquires the lock.
     *
     * <p>Acquires the lock if it is not held by another thread and returns
     * immediately, setting the lock hold count to one.
     *
     * <p>If the current thread already holds the lock then the hold
     * count is incremented by one and the method returns immediately.
     *
     * <p>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.
     */
    public void lock() {
        sync.lock();
    }      
誰是代碼界3%的王者?- 第五題Lock的簡單解讀

通過注釋就可以看到如果鎖被其他線程所持有,則目前線程被阻塞直到擷取到鎖,此時鎖的持有數量也會被設定為1。

五、總結

可以說JDK源碼是我們學習JDK類的一個最關鍵且最好的素材。

我們要學習研究某個類或者方法的時候,超級!超級!超級!建議先進源碼去看注釋。

我們學習的時候,開發的時候有困惑的地方盡量都要先去看源碼,不要等面試的時候再去”突擊“!!

創作不易,如果覺得本文對你有幫助,歡迎點贊,歡迎關注我,如果有補充歡迎評論交流,我将努力創作更多更好的文章。