java多線程系列----------- 共享受限資源(一)

能有此文十分感謝《Java編程思想》一書及其作者Bruce Eckel!

        可以把單線程程序當作在問題求解域求解的單一實體,每次只能做一件事。因爲只有一個實體,所以永遠不用擔心諸如“兩個實體試圖同時使用一個資源”這樣的問題——比如,兩個人在同一個地方停車,兩個人同時走過一扇門。

        有了併發就可以同時做多件事情了,但是,兩個或多個線程彼此互相干涉的問題也就出現了。如果不防範這種衝突,就可能發生兩個線程同時試圖訪問同一個銀行賬戶,或向同一個打印機打印,改變同一個值等諸如此類問題。

一、不正確地訪問資源

        考慮下面的例子,其中一個任務產生偶數,而其他任務消費這些數字。這裏,消費者任務的唯一工作就是檢查偶數的有效性。

        首先,定義EvenChecker,即消費者任務,因爲它將在隨後的示例中被複用。爲了將EvenChecker與將要試驗的各種類型的生成器解耦,將創建一個名爲IntGenerator的抽象類,它包含EvenCherker必須瞭解的必不可少的方法:即一個next()方法,和一個可以執行撤銷的方法。

public abstract class IntGenerator {
  private volatile boolean canceled = false;
  public abstract int next();
  // Allow this to be canceled:
  public void cancel() { canceled = true; }
  public boolean isCanceled() { return canceled; }
} 
        IntGenerator有一個cancel()方法,可以修改boolean類型的canceled標誌的狀態;還有一個isCanceled()方法,可以查看該對象是否已經被撤銷。因爲canceled標誌是boolean類型的,所以它是原子性的,即諸如賦值和返回值這樣的簡單操作在發生時沒有中斷的可能,因此不會看到這個域處於在執行這些簡單操作的過程中的中間狀態。爲了保證可視性,canceled標誌還是volatile的。

        任何IntGenerator都可以用下面的EvenChecker類來測試:

import java.util.concurrent.*;

public class EvenChecker implements Runnable {
  private IntGenerator generator;
  private final int id;
  public EvenChecker(IntGenerator g, int ident) {
    generator = g;
    id = ident;
  }
  public void run() {
    while(!generator.isCanceled()) {
      int val = generator.next();
      if(val % 2 != 0) {
        System.out.println(val + " not even!");
        generator.cancel(); // Cancels all EvenCheckers
      }
    }
  }
  // Test any type of IntGenerator:
  public static void test(IntGenerator gp, int count) {
    System.out.println("Press Control-C to exit");
    ExecutorService exec = Executors.newCachedThreadPool();
    for(int i = 0; i < count; i++)
      exec.execute(new EvenChecker(gp, i));
    exec.shutdown();
  }
  // Default value for count:
  public static void test(IntGenerator gp) {
    test(gp, 10);
  }
}
        注意,在本例中可以被撤銷的類不是Runnable,而所有依賴於IntGenerator對象的EvenChecker任務將測試它,以查看它是否已經被撤銷,正如在run()中所見。通過這種方式,共享公共資源(IntGenerator)的任務可以觀察該資源的終止信號。這可以消除所謂競爭條件,即兩個或更多任務競爭響應某個條件,因此產生衝突或不一致結果的情況。必須仔細考慮並防範併發系統失敗的所有可能途徑,例如,一個任務不能依賴於另一個任務,因爲任務關閉的順序無法得到保證。

        test()方法通過啓動大量使用相同的IntGenerator的EvenChecker,設置並執行對任何類型的IntGenerator的測試。如果IntGenerator引發失敗,那麼test()將報告它並返回。

        EvenChecker任務總是讀取和測試從與其相關的IntGenerator返回的值。注意,如果generator.isCanceled()爲true,則run()將返回,這將告知EvenChecker.test()中的Executor該任務完成了。任何EvenChecker任務都可以在與其相關聯的IntGenerator上調用cancel(),這將導致所有其他使用IntGenerator的EvenChecker得體地關閉。在後面將看到java包含的用於線程終止的各種通用的機制。

        下面的示例有一個產生一系列偶數值的next()方法:

public class EvenGenerator extends IntGenerator {
  private int currentEvenValue = 0;
  public int next() {
    ++currentEvenValue; // Danger point here!
    ++currentEvenValue;
    return currentEvenValue;
  }
  public static void main(String[] args) {
    EvenChecker.test(new EvenGenerator());
  }
}
        一個任務有可能在另一個任務執行第一個對currentEvenValue的遞增操作之後,但是沒有執行第二個操作之前,調用next()方法(即代碼中被註釋爲“Danger point here!”的地方)。這將使這個值處於“不恰當”的狀態。爲了證明這是可能發生的,EvenChecker.test()創建了一組EvenChecker對象,以連續地讀取並輸出同一個EvenGenerator,並測試檢查每個數值是否都是偶數。如果不是,就會報告錯誤,而程序也將關閉。

        這個程序最終將失敗,因爲各個EvenChecker任務在EvenGenerator處於”不恰當的“狀態時,仍能夠訪問其中的信息。但是,根據使用的特定的操作系統和其他實現細節,直到EvenGenerator完成多次循環之前,這個問題都不會被探測到。如果希望更快地發現失敗,可以嘗試着將對yield()的調用放置到第一個和第二個遞增操作之間。這只是併發程序的部分問題——如果失敗的概率非常低,那麼即使存在缺陷,它們也可能看起來是正確的。

        有一點很重要,那就是要注意到遞增程序自身也需要多個步驟,並且在遞增過程中任務會被線程機制掛起——也就是說,在Java中遞增不是原子性的操作。因此如果不保護任務即使單一的遞增也不是安全的。

二、解決共享資源競爭

        前面的示例展示了使用線程時的一個基本問題:你永遠都不知道一個線程何時在運行

        想象一下,你坐在桌邊手拿叉子正要去叉盤子裏的最後一片食物,當你的叉子就要夠着它時,這片食物突然消失了,因爲你的線程被掛起了,而另一個餐者進入並喫掉了它。這正是在編寫併發程序時需要處理的問題。對於併發工作,需要某種方式來防止兩個任務訪問相同的資源,至少在關鍵階段不能出現這種情況。

        防止這種衝突的方法就是當資源被一個任務使用時,在其上加鎖。第一個訪問某項資源的任務必須鎖定這項資源,使其他任務在其被解鎖之前,就無法訪問它了,而在其被解鎖之時,另一個任務就可以鎖定並使用 它,以此類推。

        基本上所有的併發模式在解決線程衝突問題的時候,都是採用序列化訪問共享資源的方案。這意味在給定時刻只允許一個任務訪問共享資源。通常這是通過在代碼前面加上一條鎖語句來實現的,這就使得在一段時間內只有一個任務可以運行這段代碼。因爲鎖語句產生了一種互相排斥的效果,所以這種機制常常稱爲互斥量(mutex)

        Java以提供關鍵字synchronized的形式,爲防止資源衝突提供了內置支持。當任務要執行被synchronized關鍵字保護的代碼片段的時候,它將檢查鎖是否可用,然後獲取鎖,釋放鎖。

        共享資源一般是以對象形式存在的內存片段,但也可是文件、輸入/輸出端口、或者是打印機。要控制對共享資源的訪問,得先把它包裝進一個對象。然後把所有要訪問這個資源的方法標記爲synchronized。如果某個任務處於一個對標記爲synchronized的方法的調用中,那麼在這個線程從該方法返回之前,其他所有要調用類中任何標記爲synchronized方法的線程都會被阻塞。

線程控制逃逸規則

        線程控制逃逸規則可以幫助你判斷代碼中對某些資源的訪問是否是線程安全的。
        如果一個資源的創建,使用,銷燬都在同一個線程內完成,且永遠不會脫離該線程的控制,則該資源的使用就是線程安全的。
        資源可以是對象,數組,文件,數據庫連接,套接字等等。Java中你無需主動銷燬對象,所以“銷燬”指不再有引用指向對象。
        即使對象本身線程安全,但如果該對象中包含其他資源(文件,數據庫連接),整個應用也許就不再是線程安全的了。比如2個線程都創建了各自的數據庫連接,每個連接自身是線程安全的,但它們所連接到的同一個數據庫也許不是線程安全的。比如,2個線程執行如下代碼:
檢查記錄X是否存在,如果不存在,插入X如果兩個線程同時執行,而且碰巧檢查的是同一個記錄,那麼兩個線程最終可能都插入了記錄:
線程1檢查記錄X是否存在。檢查結果:不存在
線程2檢查記錄X是否存在。檢查結果:不存在
線程1插入記錄X
線程2插入記錄X
        同樣的問題也會發生在文件或其他共享資源上。因此,區分某個線程控制的對象是資源本身,還是僅僅到某個資源的引用很重要。

        在生成偶數的代碼中,應該將類的數據成員都聲明爲private,而且只能通過方法來訪問這些數據,所以可以把方法標記爲synchronized來防止資源衝突。下面是聲明synchronized方法的方式:

synchronized void f() { /*...*/}
synchronized void g() { /*...*/}<span style="font-family: Arial, Helvetica, sans-serif; background-color: rgb(255, 255, 255);">	</span>

        所有對象都自動含有單一的鎖(也稱爲監視器)。當在對象上調用其任意synchronized方法的時候,此對象都被加鎖,這時候對象上的其他synchronized方法只有等到前一個方法調用完畢並釋放了鎖之後才能被調用。對於前面的方法,如果某個任務對對象調用了f(),對於同一個對象而言,就只能等到f()調用結束並釋放了鎖之後,其他任務才能調用f()和g()。所以,對於某個特定對象來說,其所有synchronized方法共享同一個鎖,這可以被用來防止多個任務同時訪問被編碼爲對象內存。

        注意,在使用併發時,將域設置爲private是非常重要的,否則,synchronized關鍵字就不能防止其他任務直接訪問域,這樣就會產生衝突。

        一個任務可以多次獲得對象的鎖。如果一個方法在同一個對象上調用了第二個方法,後者又調用了同一對象上的另一個方法,就會發生這種情況。JVM負責跟蹤對象被加鎖的次數。如果一個對象被解鎖(即鎖被完全釋放),其計數變爲0.在任務第一次給對象加鎖的時候,計數變爲1.每當這個相同的任務在這個對象上獲得鎖時,計數都會遞增。顯然,只有首先獲得了鎖的任務才能允許繼續獲取多個鎖。每當任務離開一個synchronized方法,計數遞減,當計數爲零的時候,鎖被完全釋放,此時別的任務就可以使用此資源。

        針對每個類,也有一個鎖(作爲類的Class對象的一部分),所以synchronized static方法可以在類的範圍內防止對static數據的併發訪問。

        你應該什麼時候同步呢?可以運用Brian的同步規則:

        如果你正在寫一個變量,它可能接下來將被另一個線程讀取,或者正在讀取一個上一次已經被另一個線程寫過的變量,那麼你必須使用同步,並且,讀寫線程都必須用相同的監視器鎖同步。

        如果在類中有超過一個方法在處理臨界數據,那麼你必須同步所有相關的方法。如果只同步一個方法,那麼其他方法將會隨意地忽略這個對象鎖,並可以在無任何懲罰的情況下被調用。每個訪問臨界共享資源的方法都必須被同步,否則它們就不會正確地工作。

        同步控制EvenGenerator,通過在EvenGenerator.java中加入synchronized關鍵字,可以防止不希望的線程訪問:

public class
SynchronizedEvenGenerator extends IntGenerator {
  private int currentEvenValue = 0;
  public synchronized int next() {
    ++currentEvenValue;
    Thread.yield(); // Cause failure faster
    ++currentEvenValue;
    return currentEvenValue;
  }
  public static void main(String[] args) {
    EvenChecker.test(new SynchronizedEvenGenerator());
  }
}

        對Thread.yield()的調用被插入到了兩個遞增操作之間,以提高在currentEvenValue是奇數狀態時上下文切換的可能性。因爲互斥可以防止多個任務同時進入臨界區,所以這不會產生任何失敗。但是如果失敗將會發生,調用yield()是一種促使其發生的有效方式。

        第一個進入next()的任務將獲得鎖,任何其他試圖獲取鎖的任務都將從其開始嘗試之時被阻塞,直至第一個任務釋放鎖。通過這種方式,任何時刻只有一個任務可以通過有互斥量看護的代碼。

使用顯式的Lock對象

        Java SE5 的java.util.concurrent類庫還包含有定義在java.util.concurrent.locks中的顯式的互斥機制。Lock對象必須被顯式地創建、鎖定和釋放。因此,它與內建的鎖形式相比,代碼缺乏優雅性。但是,對於解決某些類型的問題來說,它更加靈活。下面用顯式的Lock重寫的是SynchronizedEvenGenerator.java:

import java.util.concurrent.locks.*;

public class MutexEvenGenerator extends IntGenerator {
  private int currentEvenValue = 0;
  private Lock lock = new ReentrantLock();
  public int next() {
    lock.lock();
    try {
      ++currentEvenValue;
      Thread.yield(); // Cause failure faster
      ++currentEvenValue;
      return currentEvenValue;
    } finally {
      lock.unlock();
    }
  }
  public static void main(String[] args) {
    EvenChecker.test(new MutexEvenGenerator());
  }
}
        MutexEvenGenerator添加了一個被互斥調用的鎖,並使用lock()和unlock()方法在next()內部創建了臨界資源。當你在使用Lock對象時,將這裏所示的慣用法內部化是很重要的,緊接着的對lock()的調用,你必須放置在finally子句中帶有unlock()的try-finally語句中。注意,return語句必須在try子句中出現,以確保unlock()不會過早發生,從而將數據暴露給了第二個任務。

        儘管try-finally所需的代碼比synchronized關鍵字要多,但是這也代表了顯式的Lock對象的優點之一。如果在使用synchronized關鍵字時,某些事物失敗了,那麼就會拋出一個異常。但是你沒有機會去做任何清理工作,以維護系統使其處於良好狀態。有了顯式的Lock對象,就可以使用finally子句將系統維護在正確的狀態了。

        大體上,當你使用synchronized關鍵字時,需要寫的代碼量更少,並且用戶錯誤出現的可能性也會降低,因此通常只有在解決特殊問題時,才使用顯式的Lock對象。例如,用synchronized關鍵字不能嘗試着獲取鎖且最終獲取鎖會失敗,或者嘗試着獲取鎖一段時間,然後放棄它,要實現這些,必須使用concurrent類庫:

import java.util.concurrent.*;
import java.util.concurrent.locks.*;

public class AttemptLocking {
  private ReentrantLock lock = new ReentrantLock();
  public void untimed() {
    boolean captured = lock.tryLock();
    try {
      System.out.println("tryLock(): " + captured);
    } finally {
      if(captured)
        lock.unlock();
    }
  }
  public void timed() {
    boolean captured = false;
    try {
      captured = lock.tryLock(2, TimeUnit.SECONDS);
    } catch(InterruptedException e) {
      throw new RuntimeException(e);
    }
    try {
      System.out.println("tryLock(2, TimeUnit.SECONDS): " +
        captured);
    } finally {
      if(captured)
        lock.unlock();
    }
  }
  public static void main(String[] args) {
    final AttemptLocking al = new AttemptLocking();
    al.untimed(); // True -- lock is available
    al.timed();   // True -- lock is available
    // Now create a separate task to grab the lock:
    new Thread() {
      { setDaemon(true); }
      public void run() {
        al.lock.lock();
        System.out.println("acquired");
      }
    }.start();
    Thread.yield(); // Give the 2nd task a chance
    al.untimed(); // False -- lock grabbed by task
    al.timed();   // False -- lock grabbed by task
  }
}
         ReentrantLock允許你嘗試着獲取但最終未獲取鎖,這樣如果其他人已經獲取了這個鎖,那你就可以決定離開去執行其他一些事情,而不是等待直至這個鎖被釋放,就像在untimed()方法中所看到的。在timed()中,做出了嘗試去獲取鎖,該嘗試可以在2秒之後失敗。在main()中,作爲匿名類而創建了一個單獨的Thread,它將獲取鎖,這使得untimed()和timed()方法對某些事物產生競爭。

        顯式的Lock對象在加鎖和釋放鎖方面,相對於內建的synchronized鎖來說,還賦予了你更細粒度的控制力。這對於實現專有同步結構是很有用的,例如用於遍歷鏈接列表中的節點的節節傳遞的加鎖機制(也稱爲鎖耦合),這種遍歷代碼必須在釋放當前節點的鎖之前捕獲下一個節點的鎖。

三、原子性與易變性

        在關於Java線程的討論中,一個常不正確的知識是“原子操作不需要進行同步控制”。原子操作是不能被線程調度機制中斷的操作;一旦操作開始,那麼它一定可以在可能發生的“上下文切換”之前(切換到其他線程執行)執行完畢。依賴於原子性是很棘手且很危險的,如果你是一個併發專家,或者你得到了來自這樣的專家的幫助,你才應該使用原子性來代替同步。如果你認爲自己可以應付這種玩火似的情況,那麼請接受下面的測試:

        如果你可以編寫用於現代微處理器的高性能JVM,那麼就有資格去考慮是否可以避免同步。

        瞭解原子性是很有用的,並且要知道原子性與其他高級技術一道,在java.util.concurrent類庫中已經實現了某些更加巧妙的構件。但是要堅決抵擋住完全依賴自己的能力去進行處理的這種慾望,請看看之前表述的Brian的同步規則。

        原子性可以應用於除long和double之外的所有基本類型之上的簡單操作。對於讀取和寫入除long和double之外的基本類型變量這樣的操作,可以保證它們會被當做不可分的操作來操作內存。但是JVM可以將64位(long和double變量)的讀取和寫入當作兩個分離的32位操作來執行,這就產生了在一個讀取和寫入操作中間發生上下文切換,從而導致不同的任務可以看到不正確結果的可能性(這有時被稱爲字撕裂,因爲你可能會看到部分被修改過的數值)。但是,當你定義long和double變量時,如果使用volatile關鍵字,就會獲得(簡單的賦值與返回操作的)原子性

        因此,原子操作可由線程機制來保證其不可中斷,專家級的程序員可以利用這一點來編寫無鎖的代碼,這些代碼不需要被同步。但是,即便是這樣,它也是一種過於簡化的機制。有時,甚至看起來應該是安全的原子操作,實際上也可能不安全。

        在多處理器系統上,相對單處理器系統而言,可視性問題遠比原子性問題多得多。一個任務做出的修改,即使在不中斷的意義上講是原子性的,對其他任務也可能是不可視的(例如,修改只是暫時性地存儲在本地處理器的緩存中),因此不同的任務對應用的狀態有不同的視圖。另一方面,同步機制強制在處理器系統中,一個任務做出的修改必須在應用中是可視的。如果沒有同步機制,那麼修改時可視將無法確定。

        volatile關鍵字還確保了應用中的可視性。如果你將一個域聲明爲volatile的,那麼只要對這個域產生了寫操作,那麼所有的讀操作就可以看到這個修改。即便使用了本地緩存,情況也確實如此,volatile域會立即被寫入主存中,而讀取操作就發生在主存中。

        理解原子性和易變性是不同的概念這一點很重要。在非volatile域上的原子操作不必刷新到主存中去,因此其他讀取該域的任務也不必看到這個新值。如果多個任務在同時訪問某個域,那麼這個域就應該是volatile的,否則,這個域就應該只能經由同步來訪問。同步也會導致向主存中刷新,因此如果一個域完全有synchronized方法或語句塊來防護,那麼就不必將其設置爲是volatile的。

        一個任務所作的任何寫入操作對這個任務來說都是可視的,因此如果它只是需要在這個任務內部可視,那麼你就不需要將其設置爲volatile的。

        當一個域的值依賴於它之前的值時(例如遞增一個計數器),volatile就無法工作了。如果某個域的值受到其他域的值的限制,那麼volatile也無法工作,例如Range類的lower和upper邊界就必須遵循lower<=upper的限制。

        使用volatile而不是synchronized的唯一安全的情況是類中只有一個可變的域。再次提醒,你的第一選擇應該是synchronized關鍵字,這是最安全的方式,而嘗試其他任何方式都是有風險的。

        在Java中遞增操作不是原子性的,如果你盲目性地使用原子性概念,那麼就會看到在下面程序中的getValue()符合上面的描述:

public class AtomicityTest implements Runnable {
  private int i = 0;
  public int getValue() { return i; }
  private synchronized void evenIncrement() { i++; i++; }
  public void run() {
    while(true)
      evenIncrement();
  }
  public static void main(String[] args) {
    ExecutorService exec = Executors.newCachedThreadPool();
    AtomicityTest at = new AtomicityTest();
    exec.execute(at);
    while(true) {
      int val = at.getValue();
      if(val % 2 != 0) {
        System.out.println(val);
        System.exit(0);
      }
    }
  }
}
        但是改程序將找到奇數並終止。儘管return i確實是原子性操作,但是缺少同步使得其數值可以在處於不穩定的中間狀態時被讀取。除此之外,由於i也不是volatile的,因此還存在可視性問題。getValue()和evenIncrement()必須是synchronized的。

        正如上面示例,考慮一些更簡單的事情:一個產生序列數字的類。每當nextSerialNumber()被調用時,它必須向調用者返回唯一的值:

public class SerialNumberGenerator {
  private static volatile int serialNumber = 0;
  public static int nextSerialNumber() {
    return serialNumber++; // Not thread-safe
  }
}
        正如前面注意到的,Java遞增操作不是原子性的,並且涉及一個讀操作和一個寫操作,所以即便是在這麼簡單的操作中,也爲產生線程問題留下了空間。易變性在這裏實際上不是什麼問題,真正的問題在於nextSerialNumber()在沒有同步的情況下對共享可變值進行了訪問。

        基本上,如果一個域可能會被多個任務同時訪問,或者這些任務中至少有一個是寫入任務,那麼你應該將這個域設置爲volatile的。如果你將一個域定義爲volatile,那麼它就告訴編譯器不要執行任何移除讀取和寫入操作的優化,這些操作的目的是用線程中的局部變量維護對這個域的精確同步。實際上,讀取和寫入都是針對內存的,而卻沒有被緩存。

import java.util.concurrent.*;

// Reuses storage so we don't run out of memory:
class CircularSet {
	private int[] array;
	private int len;
	private int index = 0;

	public CircularSet(int size) {
		array = new int[size];
		len = size;
		// Initialize to a value not produced
		// by the SerialNumberGenerator:
		for (int i = 0; i < size; i++)
			array[i] = -1;
	}

	public synchronized void add(int i) {
		array[index] = i;
		// Wrap index and write over old elements:
		index = ++index % len;
	}

	public synchronized boolean contains(int val) {
		for (int i = 0; i < len; i++)
			if (array[i] == val)
				return true;
		return false;
	}
}

public class SerialNumberChecker {
	private static final int SIZE = 10;
	private static CircularSet serials = new CircularSet(1000);
	private static ExecutorService exec = Executors.newCachedThreadPool();

	static class SerialChecker implements Runnable {
		public void run() {
			while (true) {
				int serial = SerialNumberGenerator.nextSerialNumber();
				if (serials.contains(serial)) {
					System.out.println("Duplicate: " + serial);
					System.exit(0);
				}
				serials.add(serial);
			}
		}
	}

	public static void main(String[] args) throws Exception {
		for (int i = 0; i < SIZE; i++)
			exec.execute(new SerialChecker());
		// Stop after n seconds if there's an argument:
		if (args.length > 0) {
			TimeUnit.SECONDS.sleep(new Integer(args[0]));
			System.out.println("No duplicates detected");
			System.exit(0);
		}
	}
}
        SerialNumberChecker包含一個靜態的CircularSet,它持有所產生的所有序列數;另外還包含一個內嵌的SerialChecker類,它可以確保序列數是唯一的。通過創建多個任務來競爭序列數,將會發現這些任務最終會得到重複的序列數,爲解決這個問題,在nextSerialNumber()前面添加synchronized關鍵字。

四、原子類

        Java SE 5引入了諸如AtomicInteger、AtomicLong、AtomicReference等特殊的原子性變量類,它們提供下面形式的原子性條件更新操作:

boolean compareAndSet(expectedValue,updateValue);
        這些類被調整爲可以使用在某些現代處理器上的可獲得的,並且是在機器級別上的原子性。對於常規編程,它們很少會派上用場,但是在涉及性能調優時,它們就大有用武之地了。下面用AtomicInteger來重寫AtomicityTest.java:
import java.util.concurrent.*;
import java.util.concurrent.atomic.*;
import java.util.*;

public class AtomicIntegerTest implements Runnable {
  private AtomicInteger i = new AtomicInteger(0);
  public int getValue() { return i.get(); }
  private void evenIncrement() { i.addAndGet(2); }
  public void run() {
    while(true)
      evenIncrement();
  }
  public static void main(String[] args) {
    new Timer().schedule(new TimerTask() {
      public void run() {
        System.err.println("Aborting");
        System.exit(0);
      }
    }, 5000); // Terminate after 5 seconds
    ExecutorService exec = Executors.newCachedThreadPool();
    AtomicIntegerTest ait = new AtomicIntegerTest();
    exec.execute(ait);
    while(true) {
      int val = ait.getValue();
      if(val % 2 != 0) {
        System.out.println(val);
        System.exit(0);
      }
    }
  }
}
        這裏通過使用AtomicInteger而消除了synchronized關鍵字。因爲這個程序不會失敗,所以添加了一個Timer,以便在五秒之後自動終止。

        下面是用Atomic重寫MutexEvenGenerator.java:

import java.util.concurrent.atomic.*;

public class AtomicEvenGenerator extends IntGenerator {
  private AtomicInteger currentEvenValue =
    new AtomicInteger(0);
  public int next() {
    return currentEvenValue.addAndGet(2);
  }
  public static void main(String[] args) {
    EvenChecker.test(new AtomicEvenGenerator());
  }
}
        所有其他形式的同步再次通過使用AtomicInteger得到了消除。

        應該強調的是,Atomic類被設計用來構建java.util.concurrent中的類,因此只有在特殊情況下使用它們,即便使用了也需要確保不存在其他可能出現的問題。通常依賴於鎖要更安全一些(要麼是synchronized關鍵字,要麼是顯式的Lock對象)。


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章