ThreadLocal介紹

NO IMAGE

概述

ThreadLocal 是 java 提供的一個方便對象在本線程內不同方法中傳遞和獲取的類。用它定義的變量,僅在本線程中可見和維護,不受其他線程的影響,與其他線程相互隔離。

雖然在本線程不同方法中使用變量,可以通過在方法中傳入參數解決,但是當涉及多個方法甚至多個類時,為每個方法增加同樣的參數將是一場噩夢,此時 ThreadLocal 就能很好地解決這個問題。它可以在本線程內任何一個地方賦值,在任何一個地方獲取值,並且不用作為函數參數傳入。這看起來像靜態成員變量,但是 ThreadLocal 變量相比靜態成員變量的一個優勢就是,ThreadLocal 是線程隔離的,其值不會受另一個線程的影響,也不用考慮加鎖或值被其他線程篡改的問題,而這些問題都是靜態成員變量無法做到的。因此當涉及一個對象需要在很多不同方法之間傳遞時,應該考慮使用 ThreadLocal 對象來簡化代碼。

使用

ThreadLocal 通過 set 方法可以給變量賦值,通過 get 方法獲取變量的值。當然,也可以在定義變量時通過 ThreadLocal.withInitial 方法給變量賦初始值,或者定義一個繼承 ThreadLocal 的類,然後重寫 initialValue 方法。

示例代碼如下

public class TestThreadLocal
{
private static ThreadLocal<StringBuilder> builder = ThreadLocal.withInitial(StringBuilder::new);
public static void main(String[] args)
{
for (int i = 0; i < 5; i++)
{
new Thread(() -> {
String threadName = Thread.currentThread().getName();
for (int j = 0; j < 3; j++)
{
append(j);
System.out.printf("%s append %d, now builder value is %s, ThreadLocal instance hashcode is %d, ThreadLocal instance mapping value hashcode is %d\n", threadName, j, builder.get().toString(), builder.hashCode(), builder.get().hashCode());
}
change();
System.out.printf("%s set new stringbuilder, now builder value is %s, ThreadLocal instance hashcode is %d, ThreadLocal instance mapping value hashcode is %d\n", threadName, builder.get().toString(), builder.hashCode(), builder.get().hashCode());
}, "thread-" + i).start();
}
}
private static void append(int num) {
builder.get().append(num);
}
private static void change() {
StringBuilder newStringBuilder = new StringBuilder("HelloWorld");
builder.set(newStringBuilder);
}
}

在例子中,定義了一個 builderThreadLocal 對象,然後啟動 5 個線程,分別對 builder 對象進行訪問和修改操作,這兩個操作放在兩個不同的函數 appendchange 中進行,兩個函數訪問 builder 對象也是直接獲取,而不是放入函數的入參中傳遞進來。
代碼輸出如下

thread-0 append 0, now builder value is 0, ThreadLocal instance hashcode is 796465865, ThreadLocal instance mapping value hashcode is 566157654
thread-0 append 1, now builder value is 01, ThreadLocal instance hashcode is 796465865, ThreadLocal instance mapping value hashcode is 566157654
thread-4 append 0, now builder value is 0, ThreadLocal instance hashcode is 796465865, ThreadLocal instance mapping value hashcode is 654647086
thread-3 append 0, now builder value is 0, ThreadLocal instance hashcode is 796465865, ThreadLocal instance mapping value hashcode is 1803363945
thread-2 append 0, now builder value is 0, ThreadLocal instance hashcode is 796465865, ThreadLocal instance mapping value hashcode is 1535812498
thread-1 append 0, now builder value is 0, ThreadLocal instance hashcode is 796465865, ThreadLocal instance mapping value hashcode is 2075237830
thread-2 append 1, now builder value is 01, ThreadLocal instance hashcode is 796465865, ThreadLocal instance mapping value hashcode is 1535812498
thread-3 append 1, now builder value is 01, ThreadLocal instance hashcode is 796465865, ThreadLocal instance mapping value hashcode is 1803363945
thread-4 append 1, now builder value is 01, ThreadLocal instance hashcode is 796465865, ThreadLocal instance mapping value hashcode is 654647086
thread-0 append 2, now builder value is 012, ThreadLocal instance hashcode is 796465865, ThreadLocal instance mapping value hashcode is 566157654
thread-0 set new stringbuilder, now builder value is HelloWorld, ThreadLocal instance hashcode is 796465865, ThreadLocal instance mapping value hashcode is 1773033190
thread-4 append 2, now builder value is 012, ThreadLocal instance hashcode is 796465865, ThreadLocal instance mapping value hashcode is 654647086
thread-4 set new stringbuilder, now builder value is HelloWorld, ThreadLocal instance hashcode is 796465865, ThreadLocal instance mapping value hashcode is 700642750
thread-3 append 2, now builder value is 012, ThreadLocal instance hashcode is 796465865, ThreadLocal instance mapping value hashcode is 1803363945
thread-3 set new stringbuilder, now builder value is HelloWorld, ThreadLocal instance hashcode is 796465865, ThreadLocal instance mapping value hashcode is 1706743158
thread-2 append 2, now builder value is 012, ThreadLocal instance hashcode is 796465865, ThreadLocal instance mapping value hashcode is 1535812498
thread-2 set new stringbuilder, now builder value is HelloWorld, ThreadLocal instance hashcode is 796465865, ThreadLocal instance mapping value hashcode is 1431127699
thread-1 append 1, now builder value is 01, ThreadLocal instance hashcode is 796465865, ThreadLocal instance mapping value hashcode is 2075237830
thread-1 append 2, now builder value is 012, ThreadLocal instance hashcode is 796465865, ThreadLocal instance mapping value hashcode is 2075237830
thread-1 set new stringbuilder, now builder value is HelloWorld, ThreadLocal instance hashcode is 796465865, ThreadLocal instance mapping value hashcode is 1970695360

從輸出中 1~6 行可以看出,不同線程訪問的是同一個 builder 對象(不同線程輸出的 ThreadLocal instance hashcode 值相同),但是每個線程獲得的 builder 對象存儲的實例 StringBuilder 不同(不同線程輸出的 ThreadLocal instance mapping value hashcode 值不相同)。

從輸出中 1~29~10行可以看出,同一個線程中修改 builder 對象存儲的實例的值時,並不會影響到其他線程的 builder 對象存儲的實例(thread-4 線程改變存儲的 StringBuilder 的值並不會引起 thread-0 線程的 ThreadLocal instance mapping value hashcode 值發生改變)

從輸出中 9~13 行可以看出,一個線程對 ThreadLocal 對象存儲的值發生改變時,並不會影響其他的線程(thread-0 線程調用 set 方法改變本線程 ThreadLocal 存儲的對象值,本線程的 ThreadLocal instance mapping value hashcode 發生改變,但是 thread-4ThreadLocal instance mapping value hashcode 並沒有因此改變)。

原理

ThreadLocal 能在每個線程間進行隔離,其主要是靠在每個 Thread 對象中維護一個 ThreadLocalMap 來實現的。因為是線程中的對象,所以對其他線程不可見,從而達到隔離的目的。那為什麼是一個 Map 結構呢。主要是因為一個線程中可能有多個 ThreadLocal 對象,這就需要一個集合來進行存儲區分,而用 Map 可以更快地查找到相關的對象。

ThreadLocalMapThreadLocal 對象的一個靜態內部類,內部維護一個 Entry 數組,實現類似 Mapgetput 等操作,為簡單起見,可以將其看做是一個 Map,其中 keyThreadLocal 實例,valueThreadLocal 實例對象存儲的值。

set

當調用 ThreadLocalset 方法給變量設置值時,ThreadLocal 對象會先獲取本線程的 ThreadLocalMap 對象,然後將當前的 ThreadLocal 對象及要設置值作為鍵值對放入 Map 中。

public void set(T value) {
Thread t = Thread.currentThread();
// 獲取當前線程的 ThreadLocalMap 對象
ThreadLocalMap map = getMap(t);
if (map != null)
// this 指當前的 ThreadLocal 對象
map.set(this, value);
else
// key 不存在,則創建 map 並設置值
createMap(t, value);
}
ThreadLocalMap getMap(Thread t) {
// threadLocals 是 Thread 中的一個變量,因此是線程隔離的,不會受其他線程影響
// 其在 Thread 類中的定義如下:ThreadLocal.ThreadLocalMap threadLocals = null;
return t.threadLocals;
}

get

獲取 ThreadLocal 存儲的對象值時,需要調用 get 方法。此方法也是先獲取本線程的 ThreadLocalMap 對象,然後將當前的 ThreadLocal 對象作為 keyMap 中獲取對應的值,如果沒有,則返回一個初始 null

public T get() {
Thread t = Thread.currentThread();
// 獲取當前線程的 ThreadLocalMap 對象
ThreadLocalMap map = getMap(t);
if (map != null) {
// this 指當前的 ThreadLocal 對象
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}

內存洩漏

ThreadLocalMap 中的 key 是一個 ThreadLocal 對象,且是一個弱引用,而 value 卻是一個強引用。

static class ThreadLocalMap {
/**
* The entries in this hash map extend WeakReference, using
* its main ref field as the key (which is always a
* ThreadLocal object).  Note that null keys (i.e. entry.get()
* == null) mean that the key is no longer referenced, so the
* entry can be expunged from table.  Such entries are referred to
* as "stale entries" in the code that follows.
*/
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
// 其他代碼
}

毫無疑問,如果線程執行完關閉,那麼線程的所有對象都會被銷燬,此時不會存在內存洩漏的問題。此外,在執行 getset 操作時,調用進入 ThreadLocalMap 內部的函數,會對 Entry 進行檢查,如果 key 為空,也會將 value 設置為空,讓其可以被垃圾回收。所以一般情況下也不會造成內存洩漏。

// get 或 set 方法,滿足一定條件時會進入 expungeStaleEntry 方法
// 此方法內部會將 key 為 null 的 Entry 的 value 設置為 null,從而使得其可以被垃圾回收
private int expungeStaleEntry(int staleSlot) {
Entry[] tab = table;
int len = tab.length;
// 設置 value 值為 null,清空引用,讓其可以被 GC 回收
// expunge entry at staleSlot
tab[staleSlot].value = null;
tab[staleSlot] = null;
size--;
// Rehash until we encounter null
Entry e;
int i;
for (i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();
if (k == null) {
// 設置 value 值為 null,清空引用,讓其可以被 GC 回收
e.value = null;
tab[i] = null;
size--;
} else {
int h = k.threadLocalHashCode & (len - 1);
if (h != i) {
tab[i] = null;
// Unlike Knuth 6.4 Algorithm R, we must scan until
// null because multiple entries could have been stale.
while (tab[h] != null)
h = nextIndex(h, len);
tab[h] = e;
}
}
}
return i;
}

但是,存在一種情況,可能導致內存洩漏。如果在某一時刻,將 ThreadLocal 實例設置為 null,即沒有 ThreadLocal 沒有強引用了,如果發生 GC 時,由於 ThreadLocal 實例只存在弱引用,所以被回收了,但是 value 仍然存在一個當前線程連接過來的強引用,其不會被回收,只有等到線程結束死亡或者手動清空 value 或者等到另一個 ThreadLocal 對象進行 getset 操作時剛好觸發 expungeStaleEntry 函數並且剛好能夠檢查到本 ThreadLocal 對象 key 為空(概率太小),這樣才不會發生內存洩漏。否則,value 始終有引用指向它,它也不會被 GC 回收,那麼就會導致內存洩漏。雖然發生內存洩漏的概率比較小,但是為了保險起見,也建議在使用完 ThreadLocal 對象後調用一下 remove 方法清理一下值。

ThreadLocal介紹

與線程池結合使用

由於線程池是會複用線程的,因此如果在線程任務中對 ThreadLocal 沒有經過重新設值而直接讀取值的話,可能讀取到的是該線程上一個任務賦值的結果,而不是本次任務的初始值,從而導致一些意向不到的錯誤。如下所示,創建一個固定大小是 3 的線程池,但是往線程池中放入 5 個任務,則最後兩個任務會複用之前創建的線程,此時調用 ThreadLocalget 方法獲取到的是上一個任務賦值的結果,而不是本線程的初始值(程序輸出的第4~5 行就是複用了線程 1113,第一次獲取到的是也是上一個任務賦的值 2,而不是本線程的初始值 1)。

public class TestThreadLocalExecutor
{
private static ThreadLocal<Integer> id = ThreadLocal.withInitial(() -> 1);
public static void main(String[] args)
{
ExecutorService executor = Executors.newFixedThreadPool(3);
for (int i = 0; i < 5; i++)
{
executor.execute(() -> {
long threadId = Thread.currentThread().getId();
// 任務開始時重新賦值,否則可能讀取到的是上一個任務的值
// id.set(1);
int before = id.get();
increment();
int after = id.get();
System.out.printf("Thread id: %d, before increment: %d, after increment: %d\n", threadId, before, after);
});
}
executor.shutdown();
}
private static void increment()
{
int result = id.get() + 1;
id.set(result);
}
}

程序輸出如下

Thread id: 11, before increment: 1, after increment: 2
Thread id: 13, before increment: 1, after increment: 2
Thread id: 12, before increment: 1, after increment: 2
Thread id: 13, before increment: 2, after increment: 3
Thread id: 11, before increment: 2, after increment: 3

為了避免如上情況的發生,可以在每個任務開始時,為 ThreadLocal 對象重新設置初始值(在 get 方法前先調用 set 方法),或者使用原生的創建線程的方式(跳開線程池的方式)。

相關文章

Flutterengine顯示Image邏輯

品HashMap(java8)

SpringSecurity框架下實現CSRF跨站攻擊防禦

Kubernetes時代的安全軟件供應鏈