BAT面試官:你先手動用LockSupport實現一個先進先出的不可重入鎖?吊炸天

引言

不知道你們面試的過程有沒有遇到過吊炸天的面試官,一上來就說,你先手動實現一個先進先出的不可重入鎖。驚不驚喜?激不激動?大展身手的時刻到了,來,咱們一塊兒看看下面這個例子java

public class FIFOMutex {

    private final AtomicBoolean locked = new AtomicBoolean(false);
    private final Queue<Thread> waiters
        = new ConcurrentLinkedQueue<Thread>();

    public void lock() {
        boolean wasInterrupted = false;
        Thread current = Thread.currentThread();
        waiters.add(current);

        // 只有本身在隊首才能夠得到鎖,不然阻塞本身
        //cas 操做失敗的話說明這裏有併發,別人已經捷足先登了,那麼也要阻塞本身的
        //有了waiters.peek() != current判斷若是本身隊首了,爲何不直接獲取到鎖還要cas 操做呢?
        //主要是由於接下來那個remove 操做把本身移除掉了額,可是他尚未真正釋放鎖,鎖的釋放在unlock方法中釋放的
        while (waiters.peek() != current ||
            !locked.compareAndSet(false, true)) {
            //這裏就是使用LockSupport 來阻塞當前線程
            LockSupport.park(this);
            //這裏的意思就是忽略線程中斷,只是記錄下曾經被中斷過
            //你們注意這裏的java 中的中斷僅僅是一個狀態,要不要退出程序或者拋異常須要程序員來控制的
            if (Thread.interrupted()) {
                wasInterrupted = true;
            }
        }
        // 移出隊列,注意這裏移出後,後面的線程就處於隊首了,可是仍是不能獲取到鎖的,locked 的值仍是true,
        // 上面while 循環的中的cas 操做仍是會失敗進入阻塞的
        waiters.remove();
        //若是被中斷過,那麼設置中斷狀態
        if (wasInterrupted) {
            current.interrupt();
        }

    }

    public void unlock() {
        locked.set(false);
        //喚醒位於隊首的線程
        LockSupport.unpark(waiters.peek());
    }

}
複製代碼

上面這個例子其實就是jdk中LockSupport 提供的一個例子。LockSupport 是提供線程的同步原語的很是底層的一個類,若是必定要深挖的話,他的實現又是借用了Unsafe這個類來實現的,Unsafe 類中的方法都是native 的,真正的實現是C++的代碼程序員

經過上面這個例子,分別調用瞭如下兩個方法面試

public static void park(Object blocker) 
    public static void unpark(Thread thread) 
複製代碼

LockSupport的等待和喚醒是基於許可的,這個許可在C++ 的代碼中用一個變量count來保存,它只有兩個可能的值,一個是0,一個是1。初始值爲0小程序

調用一次park

  1. 若是count=0,阻塞,等待count 變成1
  2. 若是count=1,修改count=0,而且直接運行,整個過程沒有阻塞

調用一次unpark

  1. 若是count=0,修改count=1
  2. 若是count=1,保持count=1

屢次連續調用unpark 效果等同於一次

因此整個過程即便你屢次調用unpark,他的值依然只是等於1,並不會進行累加bash

源碼分析

park

public static void park(Object blocker) {
        Thread t = Thread.currentThread();
        //設置當前線程阻塞在blocker,主要是爲了方便之後dump 線程出來排查問題,接下來會講
        setBlocker(t, blocker);
        //調用UNSAFE來阻塞當前線程,具體行爲看下面解釋
        UNSAFE.park(false, 0L);
        //被喚醒以後來到這裏
        setBlocker(t, null);
    }
複製代碼

這裏解釋下UNSAFE.park(false, 0L)。調用這個方法會有如下狀況併發

  1. 若是許可值爲1(也就是以前調用過unpark,而且後面沒有調用過park來消耗許可),當即返回,而且整個過程不阻塞,修改許可值爲0
  2. 若是許可值爲0,進行阻塞等待,直到如下三種狀況發生會被喚醒
    1. 其餘線程調用了unpark 方法指定喚醒該線程
    2. 其餘線程調用該線程的interrupt方法指定中斷該線程
    3. 無理由喚醒該線程(就是耍流氓,下面會解析)

unpark

public static void unpark(Thread thread) {
        if (thread != null)
        //經過UNSAFE 來喚醒指定的線程
        //注意咱們須要保證該線程仍是存活的
        //若是該線程還沒啓動或者已經結束了,調用該方法是沒有做用的
            UNSAFE.unpark(thread);
    }
複製代碼

源碼很是簡單,直接經過UNSAFE 來喚醒指定的線程,可是要注意一個很是關鍵的細節,就是這裏指定了喚醒的線程,這個跟Object 中的notify 徹底不同的特性,synchronized 的鎖是加在對象的監視鎖上的,線程會阻塞在對象上,在喚醒的時候沒辦法指定喚醒哪一個線程,只能通知在這個對象監視鎖 上等待的線程去搶這個鎖,具體是誰搶到這把鎖是不可預測的,這也就決定了synchronized 是沒有辦法實現相似上面這個先進先出的公平鎖。源碼分析

park 和unpark 的調用不分前後順序

先來個列子ui

public class LockSupportTest {

    private static final Logger logger = LoggerFactory.getLogger(LockSupportTest.class);

    public static void main(String[] args) throws Exception {
        LockSupportTest test = new LockSupportTest();
        Thread park = new Thread(() -> {
            logger.info(Thread.currentThread().getName() + ":park線程先休眠一下,等待其餘線程對這個線程執行一次unpark");
            try {
                Thread.sleep(4000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            logger.info(Thread.currentThread().getName() + ":調用park");
            LockSupport.park(test);
            logger.info(Thread.currentThread().getName() + ": 被喚醒");
        });

        Thread unpark = new Thread(() -> {
            logger.info(Thread.currentThread().getName() + ":調用unpark喚醒線程" + park.getName());
            LockSupport.unpark(park);
            logger.info(Thread.currentThread().getName() + ": 執行完畢");
        });
        park.start();
        Thread.sleep(2000);
        unpark.start();
    }
}
複製代碼

輸出結果:this

18:52:42.065 Thread-0:park線程先休眠一下,等待其餘線程對這個線程執行一次unpark
18:52:44.064 Thread-1:調用unpark喚醒線程Thread-0
18:52:44.064 Thread-1: 執行完畢
18:52:46.079 Thread-0:調用park
18:52:46.079 Thread-0:被喚醒

複製代碼

從結果中能夠看到,即便先調用unpark,後調用park,線程也能夠立刻返回,而且整個過程是不阻塞的。這個跟Object對象的wait()和notify()有很大的區別,Object 中的wait() 和notify()順序錯亂的話,會致使線程一直阻塞在wait()上得不到喚醒。正是LockSupport這個特性,使咱們並不須要去關心線程的執行順序,大大的下降了死鎖的可能性。spa

支持超時

//nanos 單位是納秒,表示最多等待nanos 納秒,
//好比我最多等你1000納秒,若是你還沒到,就再也不等你了,其餘狀況跟park 同樣
public static void parkNanos(Object blocker, long nanos) //deadline 是一個絕對時間,單位是毫秒,表示等待這個時間點就再也不等 //(好比等到今天早上9點半,若是你還沒到,我就再也不等你了) ,其餘狀況跟park 同樣 public static void parkUntil(Object blocker, long deadline) 複製代碼

正是有了這個方法,因此咱們平時用的ReentrantLock 等各類lock 才能夠支持超時等待,底層其實就是借用了這兩個方法來實現的。這個也是synchronized 沒有辦法實現的特性

支持查詢線程在哪一個對象上阻塞

public static Object getBlocker(Thread t) {
        if (t == null)
            throw new NullPointerException();
        return UNSAFE.getObjectVolatile(t, parkBlockerOffset);
    }
複製代碼

之前沒看源碼的時候有個疑問,線程都已經阻塞了,爲何還能夠查看指定線程的阻塞在相關的對象上呢?不該該是調用的話也是沒有任何反應的的嗎?直到看了源碼,才知道它其實不是用該線程去直接獲取線程的屬性,而是經過UNSAFE.getObjectVolatile(t, parkBlockerOffset) 來獲取的。這個方法的意思就是獲取內存區域指定偏移量的對象

最佳實踐

阻塞語句LockSupport.park() 須要在循環體,例如本文一開始的例子

while (waiters.peek() != current ||
            !locked.compareAndSet(false, true)) {
            //在循環體內
            LockSupport.park(this);
            //喚醒後來到這裏
            //忽略其餘無關代碼
        }
複製代碼

若是不在循環體內會有什麼問題呢?假如變成如下代碼片斷

if (waiters.peek() != current ||
            !locked.compareAndSet(false, true)) {
            //在循環體內
            LockSupport.park(this);
            //喚醒後來到這裏
            //忽略其餘無關代碼
        }
複製代碼

這裏涉及一個線程無理由喚醒的概念,也就是說阻塞的線程並無其餘線程調用unpark() 方法的時候就被喚醒

假如前後來了兩個線程A和B,這時候A先到鎖,這個時候B阻塞。可是在A還沒釋放鎖的時候,同時B被無理由喚醒了,若是是if,那麼 線程B就直接往下執行獲取到了鎖,這個時候同時A和B均可以訪問臨界資源,這樣是不合法的,若是是while 循環的話,會判斷B不是 在隊首或者CAS 失敗的會繼續調用park 進入阻塞。因此你們記得park方法必定要放在循環體內

LockSupport中的 park ,unpark 和Object 中的wait,notify 比較

  1. 他們均可以實現線程之間的通信
  2. park 和wait 均可以讓線程進入阻塞狀態
  3. park 和unpark 能夠在代碼的任何地方使用
  4. wait 和notify,notifyAll 須要和synchronized 搭配使用,必須在獲取到監視鎖以後纔可使用,例如
synchronized (lock){
 lock.wait()
}
複製代碼
  1. wait 和notify 須要嚴格控制順序,若是wait 在notify 後面執行,則這個wait 會一直得不到通知
  2. park 和unpark 經過許可來進行通信,無需保證順序
  3. park 支持超時等待,可是wait 不支持
  4. unpark 支持喚醒指定線程,可是notify 不支持
  5. wait 和park 均可以被中斷喚醒,wait 會得到一箇中斷異常

思考題

LockSupport 本質上也是一個Object,那麼調用LockSupport的unpark 能夠喚醒調用LockSupport.wait() 方法的線程嗎?請把你的答案寫在留言區

看完兩件事

若是你以爲這篇內容對你挺有啓發,我想邀請你幫我2個小忙:

  1. 點贊,讓更多的人也能看到這篇內容(收藏不點贊,都是耍流氓 -_-)
  2. 關注公衆號「面試bat」,不按期分享原創知識,原創不易,請多支持(裏面還提供刷題小程序哦)。

相關文章
相關標籤/搜索