大佬問我: notify()是隨機喚醒線程么?
我的內(nèi)心戲: 這不是顯而易見么! 肯定是啊! jdk關(guān)于notify()注釋都寫的很清楚!
不過這么簡單的問題?
機智如我, 決定再次裝小小白, 回答: 不是!
大佬: 很好, 小伙子你真的讓我刮目相看了!!
我:
大佬: 說說為什么?
我: ………………
牢不可破的知識點被大佬一問, 瞬間感覺哪里有點問題!
于是, 咸魚君開啟了求證模式.
(大佬問我不懂的也就算了, 問這種“共識”的, 我一定舉出例子駁倒他!)
代碼求證
身為碼農(nóng), 我決定寫代碼先驗證下!
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.TimeUnit;
public class NotifyTest{
//等待列表, 用來記錄等待的順序
private static List<String> waitList = new LinkedList<>();
//喚醒列表, 用來喚醒的順序
private static List<String> notifyList = new LinkedList<>();
private static Object lock = new Object();
public static void main(String[] args) throws InterruptedException{
//創(chuàng)建50個線程
for(int i=0;i<50;i++){
String threadName = Integer.toString(i);
new Thread(() -> {
synchronized (lock) {
String cthreadName = Thread.currentThread().getName();
System.out.println("線程 ["+cthreadName+"] 正在等待.");
waitList.add(cthreadName);
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("線程 ["+cthreadName+"] 被喚醒了.");
notifyList.add(cthreadName);
}
},threadName).start();
TimeUnit.MILLISECONDS.sleep(50);
}
TimeUnit.SECONDS.sleep(1);
for(int i=0;i<50;i++){
synchronized (lock) {
lock.notify();
TimeUnit.MILLISECONDS.sleep(10);
}
}
TimeUnit.SECONDS.sleep(1);
System.out.println("wait順序:"+waitList.toString());
System.out.println("喚醒順序:"+notifyList.toString());
}
}
代碼很簡單, 創(chuàng)建了50個線程, 對其wait()和notify(), 同時使用waitList和notifyList來記錄各自的順序!
跑一下代碼
沒任何懸念, 結(jié)果不就是證明了notify()是隨機喚醒線程的么?!!
我信心爆棚, 喊著大佬來看(雖然沒啥炫耀的, 但是能圓下“指導(dǎo)”大佬的夢想!)
大佬看了下代碼, 然后看著我, 微微一笑
我內(nèi)心一慌: 難道有問題?
只見大佬默默的拿起我的鼠標(biāo), 剪切,粘貼
了一行代碼
for(int i=0;i<50;i++){
synchronized (lock) {
lock.notify();
//大佬把這行代碼移出了synchronized{}
//TimeUnit.MILLISECONDS.sleep(10);
}
TimeUnit.MILLISECONDS.sleep(10);
}
TimeUnit.SECONDS.sleep(1);
大佬再次微微一笑
: 你再跑跑看!
看到大佬的自信從容, 我越來越慌,
趕緊運行下
看到這不可置信
的結(jié)果, 我徹底慌了
什么?!! 這到底這么回事? 改動了一行代碼, notify()居然有序了?!!!
看著結(jié)果, 我沉思, 連大佬走了都沒注意.
究竟哪里出了問題? 難道notify()真是有序喚醒的?
于是,有了接下來的文章!
有疑問的小伙伴不妨看下去!(大佬可以退散了)
代碼問題分析
我們先分析下求證的代碼.
只是移動了下sleep()語句, 結(jié)果居然天差地別?!
其實問題就出在sleep()上, 準(zhǔn)確的是sleep()在synchronized里面還是外面.
當(dāng)我們執(zhí)行notify之后,由于sleep在symchronized內(nèi)部, 因此沒有釋放鎖!
(其實這點 大佬問我: notify()會立刻釋放鎖么?提起過)
lock.wait后 被通知到的線程隔缀,就會進入waitSet隊列;
之后我們循環(huán)時
for(int i=0;i<50;i++){
synchronized (lock) {
lock.notify();
TimeUnit.MILLISECONDS.sleep(10);
}
TimeUnit.SECONDS.sleep(1);
}
lock.notify();去喚醒等待線程, 我們假設(shè)喚醒了線程A;
但是因為后面還要執(zhí)行TimeUnit.MILLISECONDS.sleep(10)所以lock鎖并沒有被釋放!
當(dāng)TimeUnit.MILLISECONDS.sleep(10)執(zhí)行完畢后, lock被釋放,
此時被喚醒的線程A想獲取lock,
但是我們的for循環(huán)中synchronized (lock)也想繼續(xù)獲取lock,
于是兩者發(fā)生了鎖競爭.
由于synchronized實際上不是公平鎖,其鎖競爭的機制具有隨機性
這就導(dǎo)致了最終, 我們看到的結(jié)果好像是隨機的!
當(dāng)我們把TimeUnit.MILLISECONDS.sleep(10);移出synchronized同步塊后
for(int i=0;i<50;i++){
synchronized (lock) {
lock.notify();
}
TimeUnit.MILLISECONDS.sleep(10);
}
TimeUnit.SECONDS.sleep(1);
lock鎖立即被釋放了,
并且緊跟的 TimeUnit.SECONDS.sleep(1)確保被喚醒的線程能夠獲得lock鎖立刻執(zhí)行,
所以, 我們看到的結(jié)果才是正確的!
理論求證
想通了代碼, 得到了“notify是順序喚醒
”的結(jié)果后,
不禁疑惑,
既然“notify是順序喚醒”的, 那為什么廣為流傳的, 深入人心的確實“notify()是隨機喚醒線程
”,
JDK開發(fā)大佬不可能犯這樣的錯吧?!
帶著這樣的疑惑, 咸魚君選擇了看JDK源碼來求證!
這里以常用的JDK1.8源碼為例
找到“notify()”源碼, 看到了這段源碼注釋
翻譯一下, 大致意思就是:
notify在源碼的注釋中說到notify選擇喚醒的線程是任意的例书,但是依賴于具體實現(xiàn)的jvm.
看完后, 咸魚君頓時茅塞頓開!
我們都知道, JVM有很多實現(xiàn), 比較流行的就是hotspot!
帶著質(zhì)疑, 我們不妨接下去看看jdk1.8, hotspot中對于notify()究竟是如何實現(xiàn)的
synchronized的wait和notify是位于ObjectMonitor.cpp中
notify過程調(diào)用的是DequeueWaiter方法:
這里實際上是將_WaitSet中的第一個元素進行出隊操作,
這也說明了notify是個順序操作, 具有公平性.
看完源碼, 我們不難得出結(jié)論,
原來hotspot對notofy()的實現(xiàn)并不是我們以為的隨機喚醒, 而是“先進先出”的順序喚醒!
此刻, 我對大佬欽佩不已!
同時明白了兩個道理:
1. 廣為流傳的知識不一定是正確的, 有條件一定多看源碼, 敢于質(zhì)疑求證
2. 一定要注意版本, 沒有知識是一成不變的!
歡迎關(guān)注我
技術(shù)公眾號 “CTO技術(shù)”