ITEM 81: 使用同步工具而不是 WAIT / NOTIFY

ITEM 81: PREFER CONCURRENCY UTILITIES TO WAIT AND NOTIFY
??這本書的第一版專門介紹了如何正確使用 wait 和 notify [Bloch01寸五,item 50]。它的建議仍然有效浊洞,并在本項目結束時加以總結,但這個建議遠沒有以前那么重要。這是因為使用等待和通知的理由要少得多晚缩。自 Java 5 以來轻纪,該平臺提供了更高級別的并發(fā)實用程序晕城,可以完成以前必須在 wait 和 notify 之上手工編寫的那些事情烹植。考慮到正確使用等待和通知的困難愕贡,您應該使用更高級的并發(fā)實用程序草雕。
java.util.concurrent 中的高級實用工具分為三類:Executor,item 80已簡要介紹固以;concurrent collections 墩虹;和 synchronizers。本項目將簡要介紹 concurrent collections 和 synchronizers 憨琳。
??Concurrent collections 是標準集合接口(如列表诫钓、隊列和映射) 的高性能并發(fā)實現(xiàn)。為了提供高并發(fā)性篙螟,這些實現(xiàn)在內部管理它們自己的同步(item 79)菌湃。因此,不可能從并發(fā)集合中排除并發(fā)活動遍略;鎖定它只會降低程序的速度惧所。
??因為不能在并發(fā)集合上排除并發(fā)活動骤坐,所以也不能在它們上原子地組合方法調用。因此下愈,并發(fā)集合接口配備了依賴于狀態(tài)的修改操作纽绍,這些操作將幾個原語組合成單個原子操作。這些操作被證明對并發(fā)集合非常有用势似,因此在 Java 8 中使用默認方法將它們添加到相應的集合接口中(item 21)拌夏。
??例如,Map 的 putIfAbsent(key, value) 方法在鍵不存在的情況下為鍵插入映射履因,并返回與鍵關聯(lián)的前一個值障簿,如果鍵不存在,則返回 null搓逾。這使得實現(xiàn)線程安全的規(guī)范化映射變得很容易卷谈。這個方法模擬了 String.intern 的行為。

// Concurrent canonicalizing map atop ConcurrentMap - not optimal
private static final ConcurrentMap<String, String> map = new ConcurrentHashMap<>();
public static String intern(String s) {
    String previousValue = map.putIfAbsent(s, s); 
    return previousValue == null ? s : previousValue;
}

??事實上霞篡,你可以做得更好世蔗。ConcurrentHashMap 針對檢索操作進行了優(yōu)化,比如 get朗兵。因此污淋,首先調用 get 是值得的,只有在 get 指出有必要時才調用 putIfAbsent:

// Concurrent canonicalizing map atop ConcurrentMap - faster!
public static String intern(String s) {
    String result = map.get(s); 
    if (result == null) {
        result = map.putIfAbsent(s, s); 
        if (result == null)
            result = s; 
        }
    return result; 
}

??除了提供優(yōu)秀的并發(fā)性余掖,ConcurrentHashMap 非炒绫快。在我的機器上盐欺,上面的 intern 方法比 String.intern 快6倍以上(但是請記住赁豆,String.intern 必須使用一些策略來防止在長期生存的應用程序中發(fā)生內存泄漏)。并發(fā)集合使得同步集合在很大程度上過時冗美。例如魔种,優(yōu)先使用ConcurrentHashMap 而不是 Collections.synchronizedMap。簡單地用并發(fā)映射替換同步映射可以顯著提高并發(fā)應用程序的性能粉洼。
??一些集合接口通過阻塞操作進行了擴展节预,阻塞操作等待(或阻塞)直到成功執(zhí)行它們。例如属韧,BlockingQueue 擴展了 Queue 并添加了幾個方法安拟,包括 take,它從隊列中刪除并返回頭元素宵喂,如果隊列為空則等待糠赦。這允許阻塞隊列用于工作隊列(也稱為生產者-消費者隊列),一個或多個生產者線程對工作項進行排隊,以及一個或多個消費者線程在項目可用時從工作項取出隊列并處理它們愉棱。如您所料唆铐,大多數(shù)ExecutorService 實現(xiàn),包括 ThreadPoolExecutor奔滑,都使用 BlockingQueue (item 80)艾岂。
??同步器是允許線程彼此等待的對象,允許它們協(xié)調自己的活動朋其。最常用的同步器是 CountDownLatch 和 Semaphore王浴。比較不常用的是 CyclicBarrier 和 Exchanger。最強大的同步器是 Phaser 梅猿。
??CountDownLatch 是一次性使用的柵欄氓辣,允許一個或多個線程等待一個或多個其他線程做某事。CountDownLatch 的唯一構造函數(shù)接受一個int 類型袱蚓,它表示在允許所有等待線程繼續(xù)執(zhí)行之前钞啸,必須在 countDown 上調用倒計時方法的次數(shù)。
??在這個簡單的原語之上構建有用的東西非常容易喇潘。例如体斩,假設您想要構建一個簡單的框架來為操作的并發(fā)執(zhí)行計時。這個框架由一個單獨的方法組成颖低,該方法采用一個執(zhí)行器來執(zhí)行操作絮吵,一個表示要并發(fā)執(zhí)行的操作數(shù)量的并發(fā)級別,以及一個表示操作的 runnable忱屑。在計時器線程啟動時鐘之前蹬敲,所有的工作線程都已準備好運行操作。當最后一個工作線程準備運行動作時莺戒,定時器線程“啟動發(fā)令槍”伴嗡,允許工作線程執(zhí)行動作。當最后一個工作線程完成操作時从铲,計時器線程停止時鐘瘪校。實現(xiàn)這個邏輯直接在等待和通知上面至少可以說是混亂的,但在 CountDownLatch 上它是驚人的簡單:

// Simple framework for timing concurrent execution
public static long time(Executor executor, int concurrency, Runnable action) throws InterruptedException {
    CountDownLatch ready = new CountDownLatch(concurrency); 
    CountDownLatch start = new CountDownLatch(1); 
    CountDownLatch done = new CountDownLatch(concurrency);
    for (int i = 0; i < concurrency; i++) { 
        executor.execute(() -> {
            ready.countDown(); // Tell timer we're ready 
            try {
                start.await(); // Wait till peers are ready
                action.run();
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();  
            } finally {
                done.countDown(); // Tell timer we're done 
            }
        });
    }
    ready.await(); // Wait for all workers to be ready 
    long startNanos = System.nanoTime(); 
    start.countDown(); // And they're off! 
    done.await(); // Wait for all workers to finish 
    return System.nanoTime() - startNanos;
}

??注意食店,該方法使用了三個倒計時鎖存渣淤。第一個是 ready 它由工作線程用來告訴計時器線程何時準備就緒赏寇。然后工作線程等待第二個鎖存吉嫩,即start。當最后一個工作線程調用 ready.countDown, 定時器線程記錄啟動時間并調用 start.countDown, 允許所有工作線程繼續(xù)進行嗅定。然后定時器線程等待第三個鎖存自娩,done,直到最后一個工作線程完成操作并調用 done.countDown。一旦發(fā)生這種情況忙迁,計時器線程就會喚醒并記錄結束時間脐彩。
??還有一些細節(jié)值得注意。傳遞給 time 方法的執(zhí)行程序必須允許創(chuàng)建至少與給定并發(fā)級別相同的線程姊扔,否則測試將永遠不會完成惠奸。這就是所謂的線程饑餓死鎖[Goetz06, 8.1.1]。如果一個工作線程捕獲了一個 InterruptedException恰梢,它使用 Thread.currentThread().interrupt() 重新指定中斷佛南,并從它的 run 方法返回。這允許執(zhí)行程序按照它認為合適的方式處理中斷嵌言。請注意 System.nanoTime 用于為活動計時嗅回。對于間隔計時,始終使用 System.nanoTime 而不是 System.currentTimeMillis摧茴。 System.nanoTime 時間更加精確绵载,而且不受系統(tǒng)實時時鐘調整的影響。最后苛白,請注意娃豹,這個示例中的代碼不會產生準確的計時,除非 action 執(zhí)行了相當多的工作丸氛,比如一秒或更長時間培愁。精確的微基準測試是出了名的困難,最好借助諸如 jmh [jmh] 這樣的專門框架來完成缓窜。
??這一項只涉及到并發(fā)實用程序可以做的事情的皮毛定续。例如,前面例子中的三個倒計時鎖存可以用一個 cyclicbarrier 或 Phaser 實例代替禾锤。得到的代碼會更簡潔一些私股,但可能更難以理解。
??雖然您應該優(yōu)先使用并發(fā)實用程序來等待和通知恩掷,但您可能必須維護使用等待和通知的遺留代碼倡鲸。wait 方法用于讓線程等待某個條件。它必須在同步區(qū)域內調用黄娘,該同步區(qū)域鎖定調用它的對象峭状。下面是使用 wait 方法的標準習語:

// The standard idiom for using the wait method
synchronized (obj) {
  while (<condition does not hold>)
    obj.wait(); // (Releases lock, and reacquires on wakeup)
    ... // Perform action appropriate to condition 
}

??始終使用等待循環(huán)習語來調用等待方法;永遠不要在循環(huán)之外調用它逼争。循環(huán)用于在等待之前和之后測試條件优床。
??在等待之前測試條件,如果條件已經保持誓焦,則跳過等待以確钡ǔǎ活動。如果條件已經存在,并且在線程等待之前已經調用了notify(或notifyAll)方法移层,則不能保證線程將從等待中醒來仍翰。
??等待后測試條件,如果條件不存在观话,則再次等待是確保安全的必要條件予借。如果線程在條件不存在時繼續(xù)操作,它可以銷毀鎖保護的不變式频蛔。當條件不存在時蕾羊,線程可能被喚醒有幾個原因:
??? 另一個線程可能已經獲得了鎖,并改變了保護狀態(tài)之間的時間線程調用通知和等待線程被喚醒帽驯。
??? 當條件不存在時龟再,另一個線程可能意外或惡意地調用了notify。類通過等待公共可訪問的對象將自己暴露給這種惡作劇尼变。公共可訪問對象的同步方法中的任何等待都容易受到這個問題的影響利凑。
??? 通知線程在喚醒等待線程時可能過于“慷慨”。例如嫌术,通知線程可能會調用 notifyAll哀澈,即使只有一些等待線程滿足了它們的條件。
???在沒有通知的情況下度气,等待的線程(很少)會被喚醒割按。這被稱為虛假喚醒 [POSIX, 11.4.3.6.1;Java9-api]。
??一個相關的問題是使用 notify 還是 notifyAll 來喚醒等待的線程磷籍。(回想一下适荣,notify 會喚醒一個等待線程(假設存在這樣一個線程),而 notifyAll會喚醒所有等待線程院领。) 有時有人說弛矛,應該始終使用 notifyAll。這是一個合理而保守的建議比然。它總是會產生正確的結果丈氓,因為它確保您將喚醒需要被喚醒的線程。您也可以喚醒其他一些線程强法,但這不會影響程序的正確性万俗。這些線程將檢查它們正在等待的條件,如果發(fā)現(xiàn)該條件為假饮怯,將繼續(xù)等待闰歪。
??作為一種優(yōu)化,如果等待集中的所有線程都在等待相同的條件硕淑,并且每次只有一個線程可以從條件變?yōu)檎嬷蝎@益课竣,那么可以選擇調用 notify 而不是 notifyAll。
??即使?jié)M足了這些前提條件置媳,也可能有理由使用 notifyAll 代替 notify于樟。就像在循環(huán)中放置等待調用可以防止公共可訪問對象上的意外或惡意通知一樣,使用 notifyAll 代替 notify 可以防止不相關線程的意外或惡意等待拇囊。否則迂曲,這樣的等待可能會“吞下”一個重要通知,讓預定收件人無限期地等待寥袭。
??總之路捧,與 java.util.concurrent 提供的高級語言相比,直接使用 wait 和 notify 就像是在“并發(fā)匯編語言”中編程传黄。在新代碼中很少有理由使用wait 和 notify 杰扫。如果您維護使用 wait 和 notify 的代碼,請確保它始終使用標準習語從 while 循環(huán)中調用 wait膘掰。通常應該優(yōu)先使用 notifyAll 方法章姓,而不是 notify。如果使用 notify识埋,則必須非常小心凡伊,以確保活性窒舟。

?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末系忙,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子惠豺,更是在濱河造成了極大的恐慌银还,老刑警劉巖,帶你破解...
    沈念sama閱讀 222,183評論 6 516
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件洁墙,死亡現(xiàn)場離奇詭異见剩,居然都是意外死亡,警方通過查閱死者的電腦和手機扫俺,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,850評論 3 399
  • 文/潘曉璐 我一進店門苍苞,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人狼纬,你說我怎么就攤上這事羹呵。” “怎么了疗琉?”我有些...
    開封第一講書人閱讀 168,766評論 0 361
  • 文/不壞的土叔 我叫張陵冈欢,是天一觀的道長。 經常有香客問我盈简,道長凑耻,這世上最難降的妖魔是什么太示? 我笑而不...
    開封第一講書人閱讀 59,854評論 1 299
  • 正文 為了忘掉前任,我火速辦了婚禮香浩,結果婚禮上类缤,老公的妹妹穿的比我還像新娘。我一直安慰自己邻吭,他們只是感情好餐弱,可當我...
    茶點故事閱讀 68,871評論 6 398
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著囱晴,像睡著了一般膏蚓。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上畸写,一...
    開封第一講書人閱讀 52,457評論 1 311
  • 那天驮瞧,我揣著相機與錄音,去河邊找鬼枯芬。 笑死剧董,一個胖子當著我的面吹牛,可吹牛的內容都是我干的破停。 我是一名探鬼主播翅楼,決...
    沈念sama閱讀 40,999評論 3 422
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼真慢!你這毒婦竟也來了毅臊?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 39,914評論 0 277
  • 序言:老撾萬榮一對情侶失蹤黑界,失蹤者是張志新(化名)和其女友劉穎管嬉,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體朗鸠,經...
    沈念sama閱讀 46,465評論 1 319
  • 正文 獨居荒郊野嶺守林人離奇死亡蚯撩,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 38,543評論 3 342
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了烛占。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片胎挎。...
    茶點故事閱讀 40,675評論 1 353
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖忆家,靈堂內的尸體忽然破棺而出犹菇,到底是詐尸還是另有隱情,我是刑警寧澤芽卿,帶...
    沈念sama閱讀 36,354評論 5 351
  • 正文 年R本政府宣布揭芍,位于F島的核電站,受9級特大地震影響卸例,放射性物質發(fā)生泄漏称杨。R本人自食惡果不足惜肌毅,卻給世界環(huán)境...
    茶點故事閱讀 42,029評論 3 335
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望姑原。 院中可真熱鬧悬而,春花似錦、人聲如沸页衙。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,514評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽店乐。三九已至,卻和暖如春呻袭,著一層夾襖步出監(jiān)牢的瞬間眨八,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,616評論 1 274
  • 我被黑心中介騙來泰國打工左电, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留廉侧,地道東北人。 一個月前我還...
    沈念sama閱讀 49,091評論 3 378
  • 正文 我出身青樓篓足,卻偏偏與公主長得像段誊,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子栈拖,可洞房花燭夜當晚...
    茶點故事閱讀 45,685評論 2 360