共享資料是多執行緒應用最常見的問題之一,但有時我們需要為每個執行緒儲存一份獨立的變數。Java API 提供了 ThreadLocal 來解決這個問題。

一個 ThreadLocal 作用的例子:

import java.util.Date;

public class Main {

    public static void main(String[] args) {
Runnable task = new Runnable() { private ThreadLocal<Date> dateVar = new ThreadLocal<Date>(); public void run() {
dateVar.set(new Date());
System.out.printf("%s, GET dateVar: %s\n", Thread.currentThread().getName(), dateVar.get());
try {
Thread.sleep(5000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.printf("%s, FINAL dateVar: %s\n", Thread.currentThread().getName(), dateVar.get());
}
}; for (int i = 0; i < 3; i++) {
String threadName = "Thread" + (i + 1);
Thread thread = new Thread(task, threadName);
thread.start();
try {
Thread.sleep(1000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
} }
}

觀察執行結果:

Thread1, GET dateVar: Fri Oct 14 22:06:33 CST 2016
Thread2, GET dateVar: Fri Oct 14 22:06:34 CST 2016
Thread3, GET dateVar: Fri Oct 14 22:06:35 CST 2016
Thread1, FINAL dateVar: Fri Oct 14 22:06:33 CST 2016
Thread2, FINAL dateVar: Fri Oct 14 22:06:34 CST 2016
Thread3, FINAL dateVar: Fri Oct 14 22:06:35 CST 2016

可以看到每個執行緒都共用一個 Task 例項,執行緒之間間隔 1 秒啟動。執行緒執行 run 方法的時候首先將 dateVar 的值設定為系統當前時間並列印 dateVar 值,然後執行緒會休眠 5 秒,最後再列印 dateVar 的值。注意到 run 方法設定 dateVar 值與最後列印 dateVar 值間隔 5 秒,而下一個執行緒啟動時只間隔 1 秒,在當前執行緒列印 dateVar 之前,下個執行緒甚至是下下個執行緒已經重置 dateVar 的值,但是每個執行緒最後列印 dateVar 值的時候仍然是顯示該執行緒最初設定的值。可見,每個執行緒中的 dateVar 並不會被其他執行緒所幹擾。

再看下沒有使用 ThreadLocal 的情況。

import java.util.Date;

public class Main2 {

    public static void main(String[] args) {
Runnable task = new Runnable() { private Date dateVar; public void run() {
dateVar = new Date();
System.out.printf("%s, GET dateVar: %s\n", Thread.currentThread().getName(), dateVar);
try {
Thread.sleep(5000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.printf("%s, FINAL dateVar: %s\n", Thread.currentThread().getName(), dateVar);
}
}; for (int i = 0; i < 3; i++) {
String threadName = "Thread" + (i + 1);
Thread thread = new Thread(task, threadName);
thread.start();
try {
Thread.sleep(1000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}

觀察執行結果:

Thread1, GET dateVar: Fri Oct 14 22:17:38 CST 2016
Thread2, GET dateVar: Fri Oct 14 22:17:39 CST 2016
Thread3, GET dateVar: Fri Oct 14 22:17:40 CST 2016
Thread1, FINAL dateVar: Fri Oct 14 22:17:40 CST 2016
Thread2, FINAL dateVar: Fri Oct 14 22:17:40 CST 2016
Thread3, FINAL dateVar: Fri Oct 14 22:17:40 CST 2016

可以看到,當直接使用 Date 型別時,執行緒最終列印 dateVar 的值與最初設定的值不一致。這個是因為所有的執行緒共享一個 Task 例項,所以 dateVar 是共享資料,在多個執行緒競爭時,造成資料不一致。

ThreadLocal 的初始化

可以通過覆蓋 initialValue 方法初始化 ThreadLocal 的值。

ThreadLocal<Date> dateVar = new ThreadLocal<Date>() {
@Override
protected Date initialValue() {
return new Date();
}
};

InheritableThreadLocal

如果線上程 A 建立了子執行緒 B,那麼執行緒 A 和 執行緒 B 都是各自維護一份 ThreadLocal 值,執行緒 A 的 ThreadLocal 值不會傳遞給子執行緒 B。Java API 提供了 InheritableThreadLocal 類,它是 ThreadLocal 的子類。如果使用 InheritableThreadLocal,那麼線上程 A 建立子執行緒 B,執行緒 A 和 執行緒 B 仍然都是各自維護一份 InheritableThreadLocal 值,但是現場 A 的 InheritableThreadLocal 值則會傳遞給子執行緒 B。可以重寫 childValue 方法修改從父現場繼承的 InheritableThreadLocal 值。

public static ThreadLocal<Date> dateVar = new InheritableThreadLocal<Date>() {
protected Date initialValue() {
return new Date();
}; protected Date childValue(Date parentValue) {
return new Date(parentValue.getTime() + 1000L);
};
};