不知道你們面試的過程有沒有遇到過吊炸天的面試官,一上來就說,你先手動實現一個先進先出的不可重入鎖。驚不驚喜?激不激動?大展身手的時刻到了,來,咱們一塊兒看看下面這個例子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小程序
因此整個過程即便你屢次調用unpark,他的值依然只是等於1,並不會進行累加bash
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)。調用這個方法會有如下狀況併發
public static void unpark(Thread thread) {
if (thread != null)
//經過UNSAFE 來喚醒指定的線程
//注意咱們須要保證該線程仍是存活的
//若是該線程還沒啓動或者已經結束了,調用該方法是沒有做用的
UNSAFE.unpark(thread);
}
複製代碼
源碼很是簡單,直接經過UNSAFE 來喚醒指定的線程,可是要注意一個很是關鍵的細節,就是這裏指定了喚醒的線程,這個跟Object 中的notify 徹底不同的特性,synchronized 的鎖是加在對象的監視鎖上的,線程會阻塞在對象上,在喚醒的時候沒辦法指定喚醒哪一個線程,只能通知在這個對象監視鎖 上等待的線程去搶這個鎖,具體是誰搶到這把鎖是不可預測的,這也就決定了synchronized 是沒有辦法實現相似上面這個先進先出的公平鎖。源碼分析
先來個列子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方法必定要放在循環體內
synchronized (lock){
lock.wait()
}
複製代碼
LockSupport 本質上也是一個Object,那麼調用LockSupport的unpark 能夠喚醒調用LockSupport.wait() 方法的線程嗎?請把你的答案寫在留言區
若是你以爲這篇內容對你挺有啓發,我想邀請你幫我2個小忙: