六十六、同步訪問共享的可變數據:
?? ?? 在Java中很多時候都是通過synchronized關鍵字來實現共享對象之間的同步的。事實上,對象同步并不僅限于當多個線程操作同一可變對象時,仍然能夠保證該共享對象的狀態始終保持一致。與此同時,他還可以保證進入同步方法或者同步代碼塊的每個線程,都看到由同一個鎖保護的之前所有的修改效果。
?? ?? Java的語言規范保證了讀寫一個變量是原子的,除非這個變量的類型為long或double。換句話說,讀取一個非long或double類型的變量,可以保證返回的值是某個線程保存在該變量中的,即時多個線程在沒有同步的情況下并發地修改這個變量也是如此。然而需要特別指出的是,這樣的做法是非常危險的。即便這樣做不會帶來數據同步修改的問題,但是他會導致另外一個更為隱匿的錯誤發生。見如下代碼:
?? ?? 對于上面的代碼片段,有些人會認為在主函數sleep一秒后,工作者線程的循環狀態標志(stopRequested)就會被修改,從而致使工作者線程正常退出。然而事實卻并非如此,因為Java的規范中并沒有保證在非同步狀態下,一個線程修改的變量,在另一個線程中就會立即可見。事實上,這也是Java針對內存模型進行優化的一個技巧。為了把事情描述清楚,我們可以將上面代碼中run方法的代碼模擬為優化后的代碼,見如下修改后的run方法:
?? ?? 這種優化被稱為提升,正是HotSpot Server VM的工作。
?? ?? 要解決這個問題并不難,只需在讀取和寫入stopRequested的時候加入synchronized關鍵字即可,見如下代碼:
????? 在上面的修改代碼中,讀寫該變量的函數均被加以同步。
?? ?? 事實上,Java中還提供了另外一種方式用于處理該類問題,即volatile關鍵字。該單詞的直譯為“易變的”,引申到這里就是告訴cpu該變量是容易被改變的變量,不能每次都從當前線程的內存模型中獲取該變量的值,而是必須從主存中獲取,這種做法所帶來的唯一負面影響就是效率的折損,但是相比于synchronized關鍵字,其效率優勢還是非常明顯的。見如下代碼:
????? 和第一個代碼片段相比,這里只是在stopRequested域變量聲明之前加上volatile關鍵字,從而保證該變量為易變變量。然而需要說明的是,該關鍵字并不能完全取代synchronized同步方式,見如下代碼:
1 public class Test { 2 private static volatile int nextID = 0; 3 public static int generateNextID() { 4 return nextID++; 5 } 6 }
?? ?? generateNextID方法的用意為每次都給調用者生成不同的ID值,遺憾的是,最終結果并不是我們期望的那樣,當多個線程調用該方法時,極有可能出現重復的ID值。這是因為++運算符并不是原子操作,而是由兩個指令構成,首先是讀取該值,加一之后再重新賦值。由此可見,這兩個指令之間的時間窗口極有可能造成數據的不一致。如果要修復該問題,我們可以使用JDK(1.5 later)中java.util.concurrent.atomic包提供的AtomicLong類,使用該類性能要明顯好于synchronized的同步方式,見如下修復后的代碼:
???
?
六十七、避免過度同步:
?? ?? 過度同步所導致的最明顯問題就是性能下降,特別是在如今的多核時代,再有就是可能引發的死鎖和一系列不確定性的問題。當同步函數或同步代碼塊內調用了外來方法,如可被子類覆蓋的方法,或外部類的接口方法等。由于這些方法的行為存在一定的未知性,如果在同步塊內調用了類似的方法,將極有可能給當前的同步帶來未知的破壞性。見如下代碼:
?? ?? 下面的代碼片段是回調接口和測試調用:
????? 對于這個測試用例,他完全沒有問題,可以保證得到正確的輸出,即打印出0-99的數字。
?? ?? 現在我們換一個觀察者接口的實現方式,見如下代碼片段:
?? ?? 對于以上代碼,當執行s.removeObserver(this)的時候,將會拋出ConcurrentModificationException異常,因為在notifyElementAdded方法中正在遍歷該集合。對于該段代碼,我只能說我們是幸運的,錯誤被及時拋出并迅速定位,這是因為我們的調用是在同一個線程內完成的,而Java中synchronized關鍵字構成的鎖是可重入的,或者說是可遞歸的,即在同一個線程內可多次調用且不會被阻塞。如果恰恰相反,我們的沖突調用來自于多個線程,那么將會形成死鎖。在多線程的應用程序中,死鎖是一種比較難以重現和定位的錯誤。為了解決上述問題,我們需要做的一是將調用外部代碼的部分移出同步代碼塊,再有就是針對該遍歷,我們需要提前copy出來一份,并基于該對象進行遍歷,從而避免了上面的并發訪問沖突,如:
????? 減少不必要的代碼同步還可以大大提高程序的并發執行效率,一個非常明顯的例子就是StringBuffer,該類在JDK的早期版本中即以出現,是數據操作同步類,即時我們是以單線程方式調用該類的方法,也不得不承受塊同步帶來的額外開銷。Java在1.5中提供了非同步版本的StringBuilder類,這樣在單線程應用中可以消除因同步而帶來的額外開銷,對于多線程程序,可以繼續選擇StringBuffer,或者在自己認為需要同步的代碼部分加同步塊。
?? ?
六十八、executor和task優先于線程:
?? ?? 在Java 1.5 中提供了java.util.concurrent包,在這個包中包含了Executor Framework框架,這是一個很靈活的基于接口的任務執行工具。該框架提供了非常方便的調用方式和強大的功能,如:
?? ?? ExecutorService executor = Executors.newSingleThreadExecutor();? //創建一個單線程執行器對象。
?? ?? executor.execute(runnable);? //提交一個待執行的任務。
?? ?? executor.shutdown();? //使執行器優雅的終止。
?? ?? 事實上,Executors對象還提供了更多的工廠方法,如適用于小型服務器的Executors.newCachedThreadPool()工廠方法,該方法創建的執行器實現類對于小型服務器來說還是比較有優勢的,因為在其內部實現中并沒有提供任務隊列,而是直接將任務提交給當前可用的線程,如果此時沒有可用的線程了,則創建一個新線程來執行該任務。因此在任務數量較多的大型服務器上,由于該機制創建了大量的工作者線程,這將會導致系統的整體運行效率下降。對于該種情況,Executors提供了另外一個工廠方法Executors.newFixedThreadPool(),該方法創建的執行器實現類的內部提供了任務隊列,用于任務緩沖。
?? ?? 相比于java.util.Timer,該框架也提供了一個更為高效的執行器實現類,通過工廠方法Executors.ScheduledThreadPool()可以創建該類。它提供了更多的內部執行線程,這樣在執行耗時任務是,其定時精度要優于Timer類。
六十九、并發工具優先于wait和notify:
?? ?? java.util.concurrent中更高級的工具分成三類:Executor Framework、并發集合(Concurrent Collection)以及同步器(Synchronizer)。相比于java.util中提供的集合類,java.util.concurrent中提供的并發集合就有更好的并發性,其性能通常數倍于普通集合,如ConcurrentHashMap等。換句話說,除非有極其特殊的原因存在,否則在并發的情況下,一定要優先選擇ConcurrentHashMap,而不是Collections.syschronizedmap或者Hashtable。
????? java.util.concurrent包中還提供了阻塞隊列,該隊列極大的簡化了生產者線程和消費者線程模型的編碼工作。
? ? ? 對于同步器,concurrent包中給出了四種主要的同步器對象:CountDownLatch、Semaphore、CyclicBarrier和Exchanger。這里前兩種比較常用。在該條目中我們只是簡單介紹一個CountDownLatch的優勢,該類允許一個或者多個線程等待一個或者多個線程來做某些事情。CountDownLatch的唯一構造函數帶有一個int類型的參數 ,這個int參數是指允許所有在等待的線程被處理之前,必須在鎖存器上調用countDown方法的次數。
?? ?? 現在我們給出一個簡單應用場景,然后再給出用CountDownLatch實現該場景的實際代碼。場景描述如下:
? ? ? 假設想要構建一個簡單的框架,用來給一個動作的并發執行定時。這個框架中包含單個方法,這個方法帶有一個執行該動作的executor,一個并發級別(表示要并發執行該動作的次數),以及表示該動作的runnable。所有的工作線程自身都準備好,要在timer線程啟動時鐘之前運行該動作。當最后一個工作線程準備好運行該動作時,timer線程就開始執行,同時允許工作線程執行該動作。一旦最后一個工作線程執行完該動作,timer線程就立即停止計時。直接在wait和notify之上實現這個邏輯至少來說會很混亂,而在CountDownLatch之上實現則相當簡單。見如下示例代碼:
七十一、慎用延遲初始化:
?? ?? 延遲初始化作為一種性能優化的技巧,它要求類的域成員在第一次訪問時才執行必要的初始化動作,而不是在類構造的時候完成該域字段的初始化。和大多數優化一樣,對于延遲初始化,最好的建議"除非絕對必要,否則就不要這么做"。延遲初始化如同一把雙刃劍,它確實降低了實例對象創建的開銷,卻增加了訪問被延遲初始化的域的開銷,這一點在多線程訪問該域時表現的更為明顯。見如下代碼:
????? 從上面的代碼可以看出,在每次訪問該域字段時,均需要承擔同步的開銷。如果在真實的應用中,在多線程環境下,我們確實需要為一個實例化開銷很大的對象實行延遲初始化,又該如何做呢?該條目提供了3中技巧:
? ? ? 1. 對于靜態域字段,可以考慮使用延遲初始化Holder class模式:
?? ?? 當getField()方法第一次被調用時,它第一次讀取FieldHolder.field,導致FieldHolder類得到初始化。這種模式的魅力在于,getField方法沒有被同步,并且只執行一個域訪問,因此延遲初始化實際上并沒有增加任何訪問成本。現在的VM將在初始化該類的時候,同步域的訪問。一旦這個類被初始化,VM將修補代碼,以便后續對該域的訪問不會導致任何測試或者同步。
? ? ? 2. 對于實例域字段,可使用雙重檢查模式:
????? 注意在上面的代碼中,首先將域字段f聲明為volatile變量,其語義在之前的條目中已經給出解釋,這里將不再贅述。再者就是在進入同步塊之前,先針對該字段進行驗證,如果不是null,即已經初始化,就直接返回該域字段,從而避免了不必要的同步開銷。然而需要明確的是,在同步塊內部的判斷極其重要,因為在第一次判斷之后和進入同步代碼塊之前存在一個時間窗口,而這一窗口則很有可能造成不同步的錯誤發生,因此第二次驗證才是決定性的。
? ? ? 在該示例代碼中,使用局部變量result代替volatile的域字段,可以避免在后面的訪問中每次都從主存中獲取數據,從而提高函數的運行性能。事實上,這只是一種代碼優化的技巧而已。
?? ?? 針對該技巧,最后需要補充的是,在很多并發程序中,對某一狀態的測試,也可以使用該技巧。
?? ?? 3. 對于可以接受重復初始化實例域字段,可使用單重檢查模式:
更多文章、技術交流、商務合作、聯系博主
微信掃碼或搜索:z360901061

微信掃一掃加我為好友
QQ號聯系: 360901061
您的支持是博主寫作最大的動力,如果您喜歡我的文章,感覺我的文章對您有幫助,請用微信掃描下面二維碼支持博主2元、5元、10元、20元等您想捐的金額吧,狠狠點擊下面給點支持吧,站長非常感激您!手機微信長按不能支付解決辦法:請將微信支付二維碼保存到相冊,切換到微信,然后點擊微信右上角掃一掃功能,選擇支付二維碼完成支付。
【本文對您有幫助就好】元
