現在,并發應用程序最關鍵的方面之一是共享數據。當你創建線程實現Runnable接口,然后開始各種線程對象使用相同的Runnable對象,所有線程共享,Runnable對象內部定義相同的屬性。這本質上意味著,如果您更改了線程中的任何屬性,所有線程都將受到此更改的影響,并將通過第一個線程看到修改后的值。有時它是你希望的行為,例如多個線程增加/減少相同的計數器變量;但有時您希望確保每個線程都必須工作在自己的線程實例副本上,并且不影響其他數據。
ThreadLocal
每個線程的Thread對象中都有一個ThreadLocalMap對象,這個對象存儲來一組以ThreadLocal.threadLocalHashCode為鍵,以本地線程變量為值的K-V值對,ThreadLocal對象就是當前線程的ThreadLocalMap的訪問入口,每一個ThreadLocal對象都包含獨一無二的threadLocalHashCode值,使用這個值就可以在線程K-V值中終會對應的本地線程變量。
何時使用ThreadLocal
比如,你正在從事電子商務應用程序,需要為每個客戶請求生成一個唯一的交易ID,控制器進程需要將此交易ID傳遞給Manager / DAO類中的業務方法,以便進行日志記錄。一個解決方案可能是將此交易ID作為參數傳遞給所有業務方法。但這不是一個好的解決方案,因為代碼是多余的和不必要的。
為了解決這個問題,在這里你可以使用ThreadLocal變量。你可以在控制器或任何預處理器攔截交易ID,并設置此交易ID到ThreadLocal里。在這之后,不管該控制器調用什么方法,它們都可以從ThreadLocal訪問此交易ID。請注意,應用程序控制器將在一次服務多個請求,因為每個請求在單獨的線程中在框架級別處理,交易ID將是每個線程唯一的,并且將從線程的執行路徑中訪問。
ThreadLocal API介紹
Java Concurrency API為ThreadLocal變量的使用提供良好的機制和優秀的性能。
public class ThreadLocal<T> extends Object {...}
該類提供線程本地變量。這些變量與一般的變量不同,每個線程訪問一個線程(通過get或set方法)有自己獨立的變量初始化副本。ThreadLocal實例通常是私有的靜態字段在類希望關聯狀態的線程(例如,一個用戶ID或交易ID)
這個類有以下方法:
get():返回該線程局部變量的當前線程的值復制。
initialvalue():返回該線程局部變量的當前線程的“初始值”。
remove():刪除該線程局部變量的當前線程的值。
set(T value):將當前線程的本地線程變量的副本設置為指定的值。
如何使用ThreadLocal
下面的例子使用了兩個線程局部變量,即threadId和startDate。它們都被定義為建議的“私有靜態”字段。threadId將被用來確定當前正在運行的線程和startDate用來表示啟動線程的執行的時間。以上信息將打印在控制臺中,以驗證每個線程是否保留了自己的變量副本。
public class DemoTask implements Runnable {
private static final AtomicInteger nextId = new AtomicInteger(0);
private static final ThreadLocal<Integer> threadId = new ThreadLocal<Integer>() {
@Override
protected Integer initialValue() {
return nextId.getAndIncrement();
}
};
public static int getThreadId() {
return threadId.get();
}
private static final ThreadLocal<Date> startDate = new ThreadLocal<Date>() {
@Override
protected Date initialValue() {
return new Date();
}
};
@Override
public void run() {
System.out.printf("Starting Thread: %s : %s\n", getThreadId(), startDate.get());
try {
TimeUnit.SECONDS.sleep((int) Math.rint(Math.random() * 10));
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.printf("Thread Finished: %s : %s\n", getThreadId(), startDate.get());
}
public static void main(String[] args) {
DemoTask demoTask = new DemoTask();
Thread thread1 = new Thread(demoTask);
Thread thread2 = new Thread(demoTask);
Thread thread3 = new Thread(demoTask);
thread1.start();
thread2.start();
thread3.start();
try {
thread1.join();
thread2.join();
thread3.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
現在來驗證變量基本上能夠保持自己的狀態,不論多初始化為多少線程。我們創建了該任務的三個實例;啟動線程;然后驗證信息在控制臺打印它們。
Starting Thread: 0 : Sat Aug 12 15:38:30 CST 2017
Starting Thread: 2 : Sat Aug 12 15:38:30 CST 2017
Starting Thread: 1 : Sat Aug 12 15:38:30 CST 2017
Thread Finished: 0 : Sat Aug 12 15:38:30 CST 2017
Thread Finished: 2 : Sat Aug 12 15:38:30 CST 2017
Thread Finished: 1 : Sat Aug 12 15:38:30 CST 2017
在上面的輸出中,打印語句的順序每次都會不同。我們可以清楚地看到ThreadLocal值為每個線程實例所保管。
最常見的ThreadLocal使用是當您有一些對象不是線程安全的,但您希望避免使用同步關鍵字/塊同步訪問該對象。相反,給每個線程自己的對象實例來工作。
一個很好的替代synchronization(同步)或ThreadLocal是使用局部變量。局部變量始終是線程安全的。唯一阻止你這樣做的是應用程序設計約束。
在webapp服務器,它可能保持一個線程池,所以一個ThreadLocal變量應該在響應客戶端請求前刪除,因為當前線程可以被下一個請求重復使用。另外,如果你當你完成請求不清理的時候,任何引用它加載的類將保持在永久堆作為部署webapp的一部分,并永遠不會被垃圾回收。
InheritableThreadLocal
InheritableThreadLocal類是ThreadLocal的子類。為了解決ThreadLocal實例內部每個線程都只能看到自己的私有值,所以InheritableThreadLocal允許一個線程創建的所有子線程訪問其父線程的值。
參考
1.Java ThreadLocal Variables – When and How to Use?
2.Java TheadLocal