Java打造線程安全類的7個技巧

翻譯自7 Techniques for Thread-Safe Classeshtml

幾乎每一個Java應用程序都使用線程。像Tomcat這樣的Web服務器在單獨的工做線程中處理每一個請求,甚至使用java.util.concurrent.ForkJoinPool來提升性能。java

所以,以線程安全的方式編寫類是很是有必要的,能夠經過如下技術實現該目標。緩存

無狀態

當多個線程訪問同一個類的靜態變量時,必須協調對此變量的訪問,以避免出現同步問題。安全

其中最簡單的方法是避免對類的靜態變量訪問。類中的靜態方法僅使用局部變量和方法輸入參數。從java.lang.Math類中截取一部分代碼爲例:服務器

public static int subtractExact(int x, int y) {
    int r = x - y;
    if (((x ^ y) & (x ^ r)) < 0) {
        throw new ArithmeticException("integer overflow");
    }
    return r;
}
複製代碼

無共享狀態

若是沒法避免狀態的存在,那請不要共享狀態。狀態應該只由一個線程擁有。該技巧的一個示例是SWT或Swing圖形用戶界面框架的事件處理線程數據結構

您能夠經過繼承Thread類並添加實例變量來實現線程局部實例變量。在如下示例中,每一個啓動的工做線程中都會有本身的變量pool和workQueue。多線程

package java.util.concurrent;
public class ForkJoinWorkerThread extends Thread {
    final ForkJoinPool pool;                
    final ForkJoinPool.WorkQueue workQueue; 
}
複製代碼

實現線程局部變量的另外一種方法是使用類java.lang.ThreadLocal來建立線程局部的字段。如下是使用java.lang.ThreadLocal的實例變量的示例:併發

public class CallbackState {
public static final ThreadLocal<CallbackStatePerThread> callbackStatePerThread = 
    new ThreadLocal<CallbackStatePerThread>()
   {
      @Override
        protected CallbackStatePerThread initialValue() { 
       return getOrCreateCallbackStatePerThread();
      }
   };
}
複製代碼

將實例變量的類型包裝在java.lang.ThreadLocal中。能夠經過方法initialValue()爲java.lang.ThreadLocal提供初始值。框架

如下展現瞭如何使用該實例變量:ide

CallbackStatePerThread callbackStatePerThread = CallbackState.callbackStatePerThread.get();
複製代碼

經過調用方法get(),將獲得只與當前線程關聯的對象。

由於在應用程序服務器中,許多線程池用於處理請求,因此java.lang.ThreadLocal會致使此環境中的內存消耗太高。所以,不建議將java.lang.ThreadLocal用於由應用程序服務器的請求處理線程執行的類。

消息傳遞

若是不使用上述技巧共享狀態,則須要使用線程通訊的方法。即爲在線程之間傳遞消息。可使用java.util.concurrent包中的併發隊列實現消息傳遞。或者使用像Akka這樣的框架,這是一個actor風格併發的框架。如下示例顯示如何使用Akka發送消息:

target.tell(message, getSelf());
複製代碼

接收消息:

@Override
public Receive createReceive() {
     return receiveBuilder()
        .match(String.class, s -> System.out.println(s.toLowerCase()))
        .build();
}
複製代碼

不可變狀態

爲了不發送線程在另外一個線程讀取消息時更改消息的問題,消息應該是不可變的。所以,Akka框架具備全部消息必須是不可變的約定

實現不可變類時,應將其字段聲明爲final。這不只能夠確保編譯器能檢查出字段是不可變的,並且即便出現錯誤也能夠正確初始化。如下是final實例變量的示例:

public class ExampleFinalField {
    private final int finalField;
    public ExampleFinalField(int value) {
        this.finalField = value;
    }
}
複製代碼

使用java.util.concurrent中的數據結構

消息傳遞使用併發隊列進行線程之間的通訊。併發隊列是java.util.concurrent包中提供的數據結構之一。java.util.concurrent包提供併發的map,queue,dequeue,set和list。這些數據結構通過高度優化和線程安全測試。

synchronized字段

若是上述技術都沒有使用,還可使用同步鎖。經過在同步塊處添加鎖,確保一次只有一個線程能夠執行此部分。

synchronized(lock)
{
    i++;
}
複製代碼

請注意,當使用多個嵌套同步塊時,會有死鎖的風險。當兩個線程試圖獲取對方線程持有的鎖和對象時,就會發生死鎖。

volatile字段

正常狀況下,非易失性字段能夠緩存在寄存器或高速緩存中。經過將變量聲明爲volatile,能夠通知JVM和編譯器始終返回最新的寫入值。這不只適用於變量自己,並且適用於寫入volatile字段的線程所寫的全部值。volatile實例變量的示例:

public class ExampleVolatileField {
    private volatile int  volatileField;
}
複製代碼

更多技巧

  • 原子更新:一種技術,能夠在其中調用CPU提供的比較和設置等原子指令
  • java.util.concurrent.locks.ReentrantLock:一種鎖實現,提供比synchronized塊更多的靈活性
  • java.util.concurrent.locks.ReentrantReadWriteLock:一種鎖實現,其中讀讀操做不加鎖,讀寫和寫寫操做加鎖
  • java.util.concurrent.locks.StampedLock:一個非永久性的讀寫鎖,以樂觀鎖的方式讀取值。

結論

實現線程安全的最佳方法是避免共享狀態。若是須要共享狀態,可使用消息傳遞、不可變類、併發數據結構、synchronized字段和volatile字段。若是想測試應用程序是否線程安全,請免費試用vmlens

相關文章
相關標籤/搜索