併發工具優先於wait和notify。
自從Java 1.5 發行版本開始,Java平臺就提供了更高階的併發工具,他們可以完成以前必須在wait和notify上手寫程式碼來完成的各項工作。既然正確的使用wait和notify比較困難,就應該用更高階的併發工具來代替。
java.util.concurrent中更高階的工具分成三類:Executor Framework、併發集合(Concurrent Collection)以及同步器(Synchronizer)。
併發集合為標準的集合介面(如List、Queue和Map)提供了高效能的併發實現。為了提供高併發性,這些實現在內部自己管理同步。因此,併發集合中不可能排除併發活動;將它鎖定沒有什麼作用,只會使程式的速度變慢。
這意味著客戶無法原子的對併發集合進行方法呼叫。因此有些集合介面已經通過依賴狀態的修改操作進行了擴充套件,它將幾個基於操作合併到了單個原子操作中。例如,ConcurrentMap擴充套件了Map介面,並添加了幾個方法,包括putIfAbsent(key, value),當鍵沒有對映時會替她插入一個對映,並返回與鍵關聯的前一個值,如果沒有這樣的值,則返回null。
ConcurrentHashMap除了提供卓越的併發性之外,速度也非常快。除非不得已,否則應該優先使用ConcurrentHashMap,而不是使用Collections.synchronizedMap或者Hashtable。只要用併發Map替換老式的同步Map,就可以極大地提升併發應用程式的效能。更一般的,應該優先使用併發集合,而不是使用外部同步的集合。
有些集合介面已經通過阻塞操作進行了擴充套件,他們會一直等待(或者阻塞)到可以成功執行為止。例如,BlockingQueue擴充套件了Queue介面,並添加了包括take在內的幾個方法,它從佇列中刪除並返回了頭元素,如果佇列為空,就等待。這樣就允許將阻塞佇列用於工作佇列,也稱作生產者-消費者佇列,一個或者多個生產者執行緒在工作佇列中新增工作專案,並且當工作專案可用時,一個或者多個消費者執行緒,則從工作佇列中取出佇列並處理工作專案。不出所料,大多數ExecutorService實現(包括ThreadPoolExecutor)都使用BlockingQueue。
同步器是一些使執行緒能夠等待另一個執行緒的物件,允許他們協調動作。最常用的同步器是CountDownLatch和Semaphore。較不常用的是CyclicBarrier和Exchanger。
倒計數鎖存器(CountDownLatch)是一次性的障礙,允許一個或者多個執行緒等待一個或者多個其他執行緒來做某些事情。CountDownLatch的唯一構造器帶有一個int型別的引數,這個int引數是指允許所有在等待的執行緒被處理之前,必須在鎖存器上呼叫countDown方法的次數。
要在這個簡單的基本型別之上構建一些有用的東西,做起來是相當的容易。例如,假設想要構建一個簡單地框架,用來給一個工作的併發執行定時。這個框架中包含單個方法,這個方法帶有一個執行該動作的executor,一個併發級別(表示要併發執行該動作的次數),以及表示該動作的runnable。所有的工作執行緒(worker thread)自身都準備好,要在timer執行緒啟動時鐘之前執行該動作(為了實現準確地定時,這是必須的)。當最後一個工作執行緒準備好執行該動作時,timer執行緒就“發起頭炮”,同時允許工作執行緒執行該動作。一旦最後一個工作執行緒執行完成該動作,timer執行緒就立即停止計時。
還有一些細節值得注意。傳遞給timer方法的executor必須允許建立至少與指定併發級別一樣多的執行緒,否則就永遠不會結束。這就是執行緒飢餓死鎖。如果工作執行緒捕捉到InterruptedException,就會利用習慣用法Thread.currentThread().interrupt()重新斷言中斷,並從他的run方法中返回。這樣就允許executor在必要的時候處理中斷,事實上也理當如此。最後,對於間歇式的定時,始終應該優先使用System.nanoTime,而不是使用System.currentTimeMills。System.nanoTIme更加準確也更加精確,它不受系統時鐘的調整所影響。
雖然你時鐘應該優先使用併發工具,而不是使用wait和notify,但可能必須維護使用了wait和notify的遺留程式碼。wait方法被用來使執行緒等待某個條件,它必須在同步區域內部被呼叫,這個同步區域將物件鎖定在了呼叫wait方法的物件上。
始終應該使用wait迴圈模式來呼叫wait方法;永遠不要在迴圈之外呼叫wait方法。循環會在等待之前和之後測試條件。
在等待之前測試條件,當條件已經成立時就跳過等待,這對於確保活性是必要的。如果條件已經成立,並且線上程等待之前,notify(或者notifyAll)方法已經被呼叫,則無法保證該執行緒將會從等待中甦醒過來。
在等待之後測試條件,如果 條件不成立的話繼續等待,這對於確保安全性是必要的。當條件不成立的時候,如果執行緒繼續執行,則可能會破壞被鎖保護的約束關係。當條件不成立時,有下面一些理由可使一個執行緒甦醒過來:
- 另一個執行緒可能已經得到了鎖,並且從一個執行緒呼叫notify那一刻起,到等待執行緒甦醒過來的這段時間中,得到鎖的執行緒已經改變了受保護的狀態。
- 條件並不成立,但是另一個執行緒可能意外的或惡意的呼叫了notify。在公有可訪問的物件上等待,這些類實際上把自己暴露在了這種危險的境地中。公有可訪問物件的同步方法中包含的wait都會出現這樣的問題。
- 通知執行緒在喚醒等待執行緒時可能會過度“大方”。例如,即使只有某一些等待執行緒的條件已經被滿足,但是通知執行緒可能仍然呼叫notifyAll。
- 在沒有通知的情況下,等待執行緒也可能(但很少)會甦醒過來。這被稱為“偽喚醒”。
一個相關的話題是,為了喚醒正在等待的執行緒,你應該使用notfiy還是notifyAll。一種常見的說法是,你總是應該使用notifyAll。這是合理而保守的建議。它總會產生正確的結果,因為它可以保證你將會喚醒所有需要被喚醒的執行緒你可能也會喚醒其他一些執行緒,但是這不會影響程式的正確性,這些執行緒醒來之後,會檢查他們正在等待的條件如果發現條件並不滿足,就會繼續等待。
從優化的角度來看,如果處於等待狀態的所有執行緒都在等待同一個條件,而每次只有一個執行緒可以從這個條件中被喚醒,那麼你應該選擇呼叫notify,而不是notifyAll。
即使這些條件都是真的,也許還是有理由使用notifyAll而不是notify。就好像把wait呼叫放在一個迴圈中,以避免在公有可訪問物件上的意外或惡意的通知一樣,與此類似,使用notifyAll代替notify可以避免來自不想關執行緒的意外或惡意的等待。否則,這樣的等待會“吞掉”一個關鍵的通知,使真正的接收執行緒無限的等待下去。
簡而言之,直接使用wait和notify就像用“併發組合語言”進行程式設計一樣,而java.util.concurrent則提供了更高階的語言。沒有理由在新程式碼中使用wait和notify,即使有、也是極少的。如果你在維護使用wait和notify的程式碼,務必確保始終是利用標準的模式從while迴圈內部呼叫wait。一般情況下,你應該優先使用notifyAll,而不是使用notify。如果使用notify,請一定要小心,以確保程式的活性。