亚洲免费在线-亚洲免费在线播放-亚洲免费在线观看-亚洲免费在线观看视频-亚洲免费在线看-亚洲免费在线视频

Effective Java (并發)

系統 1757 0

六十六、同步訪問共享的可變數據:

?? ?? 在Java中很多時候都是通過synchronized關鍵字來實現共享對象之間的同步的。事實上,對象同步并不僅限于當多個線程操作同一可變對象時,仍然能夠保證該共享對象的狀態始終保持一致。與此同時,他還可以保證進入同步方法或者同步代碼塊的每個線程,都看到由同一個鎖保護的之前所有的修改效果。
?? ?? Java的語言規范保證了讀寫一個變量是原子的,除非這個變量的類型為long或double。換句話說,讀取一個非long或double類型的變量,可以保證返回的值是某個線程保存在該變量中的,即時多個線程在沒有同步的情況下并發地修改這個變量也是如此。然而需要特別指出的是,這樣的做法是非常危險的。即便這樣做不會帶來數據同步修改的問題,但是他會導致另外一個更為隱匿的錯誤發生。見如下代碼:

1 ???? public class StopThread {
2 ???????? private static boolean stopRequested = false ;
3 ???????? public static void main(String[] args) throw InterruptedException {
4 ???????????? Thread bgThread = new Thread( new Runnable() {
5 ???????????????? public void run() {
6 ???????????????????? int i = 0;
7 ???????????????????? while (!stopRequested)
8 ???????????????????????? i++;
9 ???????????????? }
10 ???????????? });
11 ???????????? bgThread.start();
12 ???????????? TimeUnit.SECONDS.sleep(1);
13 ???????????? stopRequested = true ;
14 ???????? }
15 ???? }
?

?? ?? 對于上面的代碼片段,有些人會認為在主函數sleep一秒后,工作者線程的循環狀態標志(stopRequested)就會被修改,從而致使工作者線程正常退出。然而事實卻并非如此,因為Java的規范中并沒有保證在非同步狀態下,一個線程修改的變量,在另一個線程中就會立即可見。事實上,這也是Java針對內存模型進行優化的一個技巧。為了把事情描述清楚,我們可以將上面代碼中run方法的代碼模擬為優化后的代碼,見如下修改后的run方法:

1 public void run() {
2 ???????? int i = 0;
3 ???????? if (!stopRequested) {
4 ???????????? while ( true )
5 ???????????????? i++;
6 ???????? }
7 ???? }
?

?? ?? 這種優化被稱為提升,正是HotSpot Server VM的工作。
?? ?? 要解決這個問題并不難,只需在讀取和寫入stopRequested的時候加入synchronized關鍵字即可,見如下代碼:

復制代碼
1 ???? public class StopThread {
2 ???????? private static boolean stopRequested = false ;
3 ???????? private static synchronized void requestStop() {
4 ???????????? stopRequested = true ;
5 ???????? }
6 ???????? private static synchronized boolean stopRequested() {
7 ???????????? return stopRequested;
8 ???????? }
9 ???????? public static void main(String[] args) throw InterruptedException {
10 ???????????? Thread bgThread = new Thread( new Runnable() {
11 ???????????????? public void run() {
12 ???????????????????? int i = 0;
13 ???????????????????? while (!stopRequested())
14 ???????????????????????? i++;
15 ???????????????? }
16 ???????????? });
17 ???????????? bgThread.start();
18 ???????????? TimeUnit.SECONDS.sleep(1);
19 ???????????? requestStop();
20 ???????? }
21 ???? }
?

????? 在上面的修改代碼中,讀寫該變量的函數均被加以同步。
?? ?? 事實上,Java中還提供了另外一種方式用于處理該類問題,即volatile關鍵字。該單詞的直譯為“易變的”,引申到這里就是告訴cpu該變量是容易被改變的變量,不能每次都從當前線程的內存模型中獲取該變量的值,而是必須從主存中獲取,這種做法所帶來的唯一負面影響就是效率的折損,但是相比于synchronized關鍵字,其效率優勢還是非常明顯的。見如下代碼:

1 ???? public class StopThread {
2 ???????? private static volatile boolean stopRequested = false ;
3 ???????? public static void main(String[] args) throw InterruptedException {
4 ???????????? Thread bgThread = new Thread( new Runnable() {
5 ???????????????? public void run() {
6 ???????????????????? int i = 0;
7 ???????????????????? while (!stopRequested)
8 ???????????????????????? i++;
9 ???????????????? }
10 ???????????? });
11 ???????????? bgThread.start();
12 ???????????? TimeUnit.SECONDS.sleep(1);
13 ???????????? stopRequested = true ;
14 ???????? }
15 ???? }
?

????? 和第一個代碼片段相比,這里只是在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的同步方式,見如下修復后的代碼:

復制代碼
1 ???? public class Test {
2 ???????? private static final AtomicLong nextID = new AtomicLong();
3 ???????? public static long generateNextID() {
4 ???????????? return nextID.getAndIncrement();
5 ???????? }
6 ???? }

??? ?
六十七、避免過度同步:

?? ?? 過度同步所導致的最明顯問題就是性能下降,特別是在如今的多核時代,再有就是可能引發的死鎖和一系列不確定性的問題。當同步函數或同步代碼塊內調用了外來方法,如可被子類覆蓋的方法,或外部類的接口方法等。由于這些方法的行為存在一定的未知性,如果在同步塊內調用了類似的方法,將極有可能給當前的同步帶來未知的破壞性。見如下代碼:

1 ???? public class ObservableSet<E> extends ForwardingSet<E> {
2 ???????? public ObservableSet(Set<E> set) {
3 ???????????? super (set);
4 ???????? }
5 ???????? private final List<SetObserver<E>> observers = new ArrayList<SetObserver<E>>();
6 ???????? public void addObserver(SetObserver<E> observer) {
7 ???????????? synchronized (observers) {
8 ???????????????? observers.add(observer);
9 ???????????? }
10 ???????? }
11 ???????? public boolean removeObserver(SetObserver<E> observer) {
12 ???????????? synchronized (observers) {
13 ???????????????? return observers.remover(observer);
14 ???????????? }
15 ???????? }
16 ???????? private void notifyElementAdded(E element) {
17 ???????????? synchronized (observers) {
18 ???????????????? for (SetObserver<E> observer : observers)
19 ???????????????????? observer.added( this ,element);
20 ???????????? }
21 ???????? }
22 ???????? @Override public boolean add(E element) {
23 ???????????? boolean added = super .add(element);
24 ???????????? if (added)
25 ???????????????? notifyElementAdded(element);
26 ???????????? return added;
27 ???????? }
28 ???????? @Override public boolean addAll(Collection<? extends E> c) {
29 ???????????? boolean result = false ;
30 ???????????? for (E element : c)
31 ???????????????? result |= add(element);
32 ???????????? return result;
33 ???????? }
34 ???? }
?

?? ?? 下面的代碼片段是回調接口和測試調用:

1 ???? public interface SetObserver<E> {
2 ???????? void added(ObservableSet<E> set,E element);
3 ???? }
4 ????
5 ???? public static void main(String[] args) {
6 ???????? ObservableSet<Integer> set = new ObservableSet<Integer>( new HashSet<Integer>());
7 ???????? set.addObserver( new SetObserver<Integer>() {
8 ???????????? public void added(ObservableSet<Integer> s, Integer e) {
9 ???????????????? System.out.println(e);
10 ???????????? }
11 ???????? });
12 ???????? for ( int i = 0; i < 100; i++)
13 ???????????? set.add(i);
14 ???? }
?

????? 對于這個測試用例,他完全沒有問題,可以保證得到正確的輸出,即打印出0-99的數字。
?? ?? 現在我們換一個觀察者接口的實現方式,見如下代碼片段:

1 ???? set.addObserver( new SetObserver<Integer>() {
2 ???????? public void added(ObservableSet<Integer> s,Integer e) {
3 ???????????? System.out.println(e);
4 ???????????? if (e == 23)
5 ???????????????? s.removeObserver( this );
6 ???????? }
7 ???? });
?

?? ?? 對于以上代碼,當執行s.removeObserver(this)的時候,將會拋出ConcurrentModificationException異常,因為在notifyElementAdded方法中正在遍歷該集合。對于該段代碼,我只能說我們是幸運的,錯誤被及時拋出并迅速定位,這是因為我們的調用是在同一個線程內完成的,而Java中synchronized關鍵字構成的鎖是可重入的,或者說是可遞歸的,即在同一個線程內可多次調用且不會被阻塞。如果恰恰相反,我們的沖突調用來自于多個線程,那么將會形成死鎖。在多線程的應用程序中,死鎖是一種比較難以重現和定位的錯誤。為了解決上述問題,我們需要做的一是將調用外部代碼的部分移出同步代碼塊,再有就是針對該遍歷,我們需要提前copy出來一份,并基于該對象進行遍歷,從而避免了上面的并發訪問沖突,如:

復制代碼
1 ???? private void notifyElementAdded(E element) {
2 ???????? List<SetObserver<E>> snapshot = null ;
3 ???????? synchronized (observers) {
4 ???????????? snapshot = new ArrayList<SetObserver<E>>(observers);
5 ???????? }
6 ???????? for (SetObserver<E> Observer : snapshot)
7 ???????????? Observer.added( this ,element);
8 ???? }
?

????? 減少不必要的代碼同步還可以大大提高程序的并發執行效率,一個非常明顯的例子就是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之上實現則相當簡單。見如下示例代碼:

1 ???? public static long time(Executor executor, int concurrency, final Runnable action) {
2 ???????? final CountDownLatch ready = new CountDownLatch(concurrency);
3 ???????? final CountDownLatch start = new CountDownLatch(1);
4 ???????? final CountDownLatch done = new CountDownLatch(concurrency);
5 ???????? for ( int i = 0; i < concurrency; i++) {
6 ???????????? executor.execute( new Runnable() {
7 ???????????????? public void run() {
8 ???????????????????? ready.countDown();
9 ???????????????????? try {
10 ???????????????????????? start.await();
11 ???????????????????????? action.run();
12 ???????????????????? } catch (InterruptedException e) {
13 ???????????????????????? Thread.currentThread().interrupt();
14 ???????????????????? } finally {
15 ???????????????????????? done.countDown();
16 ???????????????????? }
17 ???????????????? }
18 ???????????? });
19 ???????????? // 等待工作者線程準備可以執行,即所有的工作線程均調用ready.countDown()方法。
20 ???????????? ready.await();
21 ???????????? // 這里使用nanoTime,是因為其精確度高于System.currentTimeMills()。
22 ???????????? long startNanos = System.nanoTime();
23 ???????????? // 該語句執行后,工作者線程中的start.await()均將被喚醒。
24 ???????????? start.countDown();
25 ???????????? // 下面的等待,只有在所有的工作者線程均調用done.countDown()之后才會被喚醒。
26 ???????????? done.await();
27 ???????????? return System.nanoTime() - startNanos;
28 ???????? }
29 ???? }
?


七十一、慎用延遲初始化:

?? ?? 延遲初始化作為一種性能優化的技巧,它要求類的域成員在第一次訪問時才執行必要的初始化動作,而不是在類構造的時候完成該域字段的初始化。和大多數優化一樣,對于延遲初始化,最好的建議"除非絕對必要,否則就不要這么做"。延遲初始化如同一把雙刃劍,它確實降低了實例對象創建的開銷,卻增加了訪問被延遲初始化的域的開銷,這一點在多線程訪問該域時表現的更為明顯。見如下代碼:

1 ???? public class TestClass {
2 ???????? private final FieldType field;
3 ???????? synchronized FieldType getField() {
4 ???????????? if (field == null )
5 ???????????????? field = computeFieldValue();
6 ???????????? return field;
7 ???????? }
8 ???? }
?

????? 從上面的代碼可以看出,在每次訪問該域字段時,均需要承擔同步的開銷。如果在真實的應用中,在多線程環境下,我們確實需要為一個實例化開銷很大的對象實行延遲初始化,又該如何做呢?該條目提供了3中技巧:
? ? ? 1. 對于靜態域字段,可以考慮使用延遲初始化Holder class模式:

1 ???? public class TestClass {
2 ???????? private static class FieldHolder {
3 ???????????? static final FieldType field = computeFieldValue();
4 ???????? }
5 ???????? static FieldType getField() {
6 ???????????? return FieldHolder.field;
7 ???????? }
8 ???? }
?

?? ?? 當getField()方法第一次被調用時,它第一次讀取FieldHolder.field,導致FieldHolder類得到初始化。這種模式的魅力在于,getField方法沒有被同步,并且只執行一個域訪問,因此延遲初始化實際上并沒有增加任何訪問成本。現在的VM將在初始化該類的時候,同步域的訪問。一旦這個類被初始化,VM將修補代碼,以便后續對該域的訪問不會導致任何測試或者同步。
? ? ? 2. 對于實例域字段,可使用雙重檢查模式:

1 ???? public class TestClass {
2 ???????? private volatile FieldType f;
3 ???????? FieldType getField() {
4 ???????????? FieldType result = f;
5 ???????????? if (result == null ) {
6 ???????????????? synchronized ( this ) {
7 ???????????????????? result = f;
8 ???????????????????? if (result == null )
9 ???????????????????????? f = result = computeFieldValue();
10 ???????????????? }
11 ???????????? }
12 ???????????? return result;
13 ???????? }
14 ???? }
?

????? 注意在上面的代碼中,首先將域字段f聲明為volatile變量,其語義在之前的條目中已經給出解釋,這里將不再贅述。再者就是在進入同步塊之前,先針對該字段進行驗證,如果不是null,即已經初始化,就直接返回該域字段,從而避免了不必要的同步開銷。然而需要明確的是,在同步塊內部的判斷極其重要,因為在第一次判斷之后和進入同步代碼塊之前存在一個時間窗口,而這一窗口則很有可能造成不同步的錯誤發生,因此第二次驗證才是決定性的。
? ? ? 在該示例代碼中,使用局部變量result代替volatile的域字段,可以避免在后面的訪問中每次都從主存中獲取數據,從而提高函數的運行性能。事實上,這只是一種代碼優化的技巧而已。
?? ?? 針對該技巧,最后需要補充的是,在很多并發程序中,對某一狀態的測試,也可以使用該技巧。
?? ?? 3. 對于可以接受重復初始化實例域字段,可使用單重檢查模式:

1 ???? public class TestClass {
2 ???????? private volatile FieldType f;
3 ???????? FieldType getField() {
4 ???????????? FieldType result = f;
5 ???????????? if (result == null )
6 ???????????????? f = result = computeFieldValue();
7 ???????????? return result;
8 ???????? }
9 ???? }?

Effective Java (并發)


更多文章、技術交流、商務合作、聯系博主

微信掃碼或搜索:z360901061

微信掃一掃加我為好友

QQ號聯系: 360901061

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

【本文對您有幫助就好】

您的支持是博主寫作最大的動力,如果您喜歡我的文章,感覺我的文章對您有幫助,請用微信掃描上面二維碼支持博主2元、5元、10元、自定義金額等您想捐的金額吧,站長會非常 感謝您的哦!!!

發表我的評論
最新評論 總共0條評論
主站蜘蛛池模板: 免费爱爱 | 999热这里只有精品 999人在线精品播放视频 | 99视频在线免费 | 国产亚洲精品一品区99热 | www.香蕉视频在线观看 | 欧美久久久久久 | 国精品在亚洲_欧美 | 手机看片日韩欧美 | 成人aa在线观看视频 | 在线观看片成人免费视频 | 日韩在线观看视频网站 | 欧美成人免费在线观看 | 日本中文字幕网站 | 任你干精品视频 | 中文字幕视频免费在线观看 | 小视频国产| 国产成人精品免费视频大全五级 | 免费中日高清无专码有限公司 | 国产成人精品男人的天堂网站 | 亚洲欧美日韩在线不卡中文 | 99热99在线| 久久精品国产精品2020 | 狠狠色噜噜狠狠狠狠97影音先锋 | 久久lu| 色婷婷六月桃花综合影院 | 久久99蜜桃精品久久久久小说 | 午夜免费福利视频 | 精品精品国产高清a毛片牛牛 | 激情四播 | 激情99| 日本日日黄 | 久久久久国产精品免费网站 | 一级一级毛片看看 | 豆国产96在线 | 亚洲 | 日本中文字幕视频在线看 | 成人欧美日韩高清不卡 | 深夜成人影院 | 草久久久久 | 免费观看成人www精品视频在线 | 中文字幕日韩精品麻豆系列 | 精品久久久久久婷婷 |