今天,并發(fā)應(yīng)用程序最關(guān)鍵的方面之一是共享數(shù)據(jù)。當(dāng)您創(chuàng)建實(shí)現(xiàn)Runnable接口的線程,然后使用相同的Runnable對象啟動各種線程對象時,所有線程共享Runnable對象內(nèi)定義的相同屬性。這實(shí)際上意味著,如果您更改線程中的任何屬性,所有線程都將受到此更改的影響,并將在第一個線程中看到修改后的值。有時它是期望的行為,例如多個線程增加/減少相同的計數(shù)器變量;但有時您希望確保每個線程必須在它自己的線程實(shí)例副本上工作,而不影響其他數(shù)據(jù)。
什么時候使用ThreadLocal?
例如,假設(shè)您正在處理一個電子商務(wù)應(yīng)用程序。您需要為每個請求此控制器流程的客戶生成唯一的事務(wù)id,并且需要將此事務(wù)id傳遞給manager/DAO類中的業(yè)務(wù)方法,以便進(jìn)行日志記錄。一個解決方案是將這個事務(wù)id作為參數(shù)傳遞給所有業(yè)務(wù)方法。但這不是一個好的解決方案,因為代碼會產(chǎn)生不必要的冗余。
想要解決這個問題,可以使用ThreadLocal變量。您可以在控制器或任何預(yù)處理器攔截器中生成事務(wù)id;并在ThreadLocal中設(shè)置此事務(wù)id。之后,無論控制器調(diào)用什么方法,它們都可以從threadlocal訪問這個事務(wù)id。還請注意,應(yīng)用程序控制器將同時處理多個請求,由于每個請求在框架級別上在單獨(dú)的線程中處理,因此事務(wù)id對于每個線程都是惟一的,并且可以從線程的所有執(zhí)行路徑訪問它。
更多信息:與JAX-RS ResteasyProviderFactory共享上下文數(shù)據(jù)(ThreadLocalStack示例)
ThreadLocal類內(nèi)部?
Java并發(fā)API使用ThreadLocal類為線程本地變量提供了一種干凈的機(jī)制,具有非常好的性能。
public class ThreadLocal<T> extends Object {...}
該類提供線程本地變量。這些變量與普通的對應(yīng)變量的不同之處在于,每個訪問一個變量的線程(通過其get或set方法)都有自己的、獨(dú)立初始化的變量副本。ThreadLocal實(shí)例通常是類中的私有靜態(tài)字段,類希望將狀態(tài)與線程關(guān)聯(lián)(例如,用戶ID或事務(wù)ID)。
這個類有以下方法:
- get():返回該線程本地變量的當(dāng)前線程副本中的值。
- initialValue():返回這個線程本地變量的當(dāng)前線程的“初始值”。
- remove():刪除這個線程本地變量的當(dāng)前線程值。
- set(T value):將當(dāng)前線程的這個線程本地變量的副本設(shè)置為指定的值。
如何使用ThreadLocal?
下面的例子使用了兩個線程本地變量,即threadId和startDate。兩者都被定義為推薦的private static字段。threadId將用于標(biāo)識當(dāng)前正在運(yùn)行的線程,startDate將用于獲取線程開始執(zhí)行的時間。以上信息將打印在控制臺中,以驗證每個線程都維護(hù)了自己的變量副本。
public class DemoTask implements Runnable {
// 包含要分配的下一個線程ID的原子整數(shù)
private static final AtomicInteger nextId = new AtomicInteger(0);
// 包含每個線程的ID的ThreadLocal變量
private static final ThreadLocal<Integer> threadId = new ThreadLocal<Integer>() {
@Override
protected Integer initialValue() {
return nextId.getAndIncrement();// 獲取值,并遞增
}
};
// 返回當(dāng)前線程的唯一ID,必要時為其賦值
public int getThreadId() {
return threadId.get();
}
// 返回當(dāng)前線程的啟動時間戳
private static final ThreadLocal<Date> startDate = new ThreadLocal<Date>() {
protected Date initialValue() {
return new Date();
}
};
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());
}
}
現(xiàn)在,為了驗證變量基本上能夠維護(hù)它們的狀態(tài),而不需要考慮多個線程的多個初始化,讓我們創(chuàng)建這個任務(wù)的三個實(shí)例;啟動線程;然后驗證它們在控制臺打印的信息。
public class ThreadLocalExample {
public static void main(String[] args) throws Exception {
Thread thread[] = new Thread[3];
for (int i = 0; i < 3; i++) {
DemoTask demoTask = new DemoTask();
thread[i] = new Thread(demoTask, "Thread " + i);
}
for (int i = 0; i < 3; i++) {
thread[i].start();
TimeUnit.SECONDS.sleep((int) Math.rint(Math.random() * 10));
}
}
}
輸出結(jié)果為
Starting Thread: 0 : Mon Mar 04 00:15:28 CST 2019
Thread Finished: 0 : Mon Mar 04 00:15:28 CST 2019
Starting Thread: 1 : Mon Mar 04 00:15:31 CST 2019
Thread Finished: 1 : Mon Mar 04 00:15:31 CST 2019
Starting Thread: 2 : Mon Mar 04 00:15:37 CST 2019
Thread Finished: 2 : Mon Mar 04 00:15:37 CST 2019
在上面的輸出中,每次打印語句的順序都會有所不同。我將它們按順序排列,這樣我們就可以清楚地確定每個線程實(shí)例的線程本地值都是安全的;不會混雜在一起。自己嘗試一下。
ThreadLocal最常見的用法是,當(dāng)您有某個對象不是線程安全的,但是您希望避免使用synchronized關(guān)鍵字/塊對該對象進(jìn)行同步訪問時。相反,應(yīng)該為每個線程提供其要使用的對象的實(shí)例。
同步或threadlocal的一個很好的替代方法是將變量變成一個本地變量。局部變量總是線程安全的。唯一可能阻止您這樣做的是您的應(yīng)用程序設(shè)計約束。
在wabapp服務(wù)器中,它可能保留一個線程池,因此在響應(yīng)客戶機(jī)之前應(yīng)該刪除
ThreadLocal變量,因為當(dāng)前線程可能被下一個請求重用。此外,如果在完成時不清理,那么它對作為已部署webapp的一部分加載的類的任何引用都將保留在永久堆中,永遠(yuǎn)不會被垃圾收集。