引言
不知道大家面試的過程有沒有遇到過吊炸天的面試官,一上來就說,你先手動實現一個先進先出的不可重入鎖。驚不驚喜?激不激動?大展身手的時刻到了,來,我們一起看看下面這個例子
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
- 如果count=0,阻塞,等待count 變成1
- 如果count=1,修改count=0,並且直接運行,整個過程沒有阻塞
調用一次unpark
- 如果count=0,修改count=1
- 如果count=1,保持count=1
多次連續調用unpark 效果等同於一次
所以整個過程即使你多次調用unpark,他的值依然只是等於1,並不會進行累加
源碼分析
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(也就是之前調用過unpark,並且後面沒有調用過park來消耗許可),立即返回,並且整個過程不阻塞,修改許可值爲0
- 如果許可值爲0,進行阻塞等待,直到以下三種情況發生會被喚醒
- 其他線程調用了unpark 方法指定喚醒該線程
- 其他線程調用該線程的interrupt方法指定中斷該線程
- 無理由喚醒該線程(就是耍流氓,下面會解析)
unpark
public static void unpark(Thread thread) {
if (thread != null)
//通過UNSAFE 來喚醒指定的線程
//注意我們需要保證該線程還是存活的
//如果該線程還沒啓動或者已經結束了,調用該方法是沒有作用的
UNSAFE.unpark(thread);
}
源碼非常簡單,直接通過UNSAFE 來喚醒指定的線程,但是要注意一個非常關鍵的細節,就是這裏指定了喚醒的線程,這個跟Object 中的notify 完全不一樣的特性,synchronized 的鎖是加在對象的監視鎖上的,線程會阻塞在對象上,在喚醒的時候沒辦法指定喚醒哪個線程,只能通知在這個對象監視鎖 上等待的線程去搶這個鎖,具體是誰搶到這把鎖是不可預測的,這也就決定了synchronized 是沒有辦法實現類似上面這個先進先出的公平鎖。
park 和unpark 的調用不分先後順序
先來個列子
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();
}
}
輸出結果:
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這個特性,使我們並不需要去關心線程的執行順序,大大的降低了死鎖的可能性。
支持超時
//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 比較
- 他們都可以實現線程之間的通訊
- park 和wait 都可以讓線程進入阻塞狀態
- park 和unpark 可以在代碼的任何地方使用
- wait 和notify,notifyAll 需要和synchronized 搭配使用,必須在獲取到監視鎖之後纔可以使用,例如
synchronized (lock){
lock.wait()
}
- wait 和notify 需要嚴格控制順序,如果wait 在notify 後面執行,則這個wait 會一直得不到通知
- park 和unpark 通過許可來進行通訊,無需保證順序
- park 支持超時等待,但是wait 不支持
- unpark 支持喚醒指定線程,但是notify 不支持
- wait 和park 都可以被中斷喚醒,wait 會獲得一箇中斷異常
思考題
LockSupport 本質上也是一個Object,那麼調用LockSupport的unpark 可以喚醒調用LockSupport.wait() 方法的線程嗎?請把你的答案寫在留言區
看完兩件事
如果你覺得這篇內容對你挺有啓發,我想邀請你幫我2個小忙:
- 點贊,讓更多的人也能看到這篇內容(收藏不點贊,都是耍流氓 -_-)
- 關注公衆號「面試bat」,不定期分享原創知識,原創不易,請多支持(裏面還提供刷題小程序哦)。