翻譯自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包提供併發的map,queue,dequeue,set和list。這些數據結構通過高度優化和線程安全測試。
若是上述技術都沒有使用,還可使用同步鎖。經過在同步塊處添加鎖,確保一次只有一個線程能夠執行此部分。
synchronized(lock)
{
i++;
}
複製代碼
請注意,當使用多個嵌套同步塊時,會有死鎖的風險。當兩個線程試圖獲取對方線程持有的鎖和對象時,就會發生死鎖。
正常狀況下,非易失性字段能夠緩存在寄存器或高速緩存中。經過將變量聲明爲volatile,能夠通知JVM和編譯器始終返回最新的寫入值。這不只適用於變量自己,並且適用於寫入volatile字段的線程所寫的全部值。volatile實例變量的示例:
public class ExampleVolatileField {
private volatile int volatileField;
}
複製代碼
實現線程安全的最佳方法是避免共享狀態。若是須要共享狀態,可使用消息傳遞、不可變類、併發數據結構、synchronized字段和volatile字段。若是想測試應用程序是否線程安全,請免費試用vmlens。