什麼是 ThreadLocal?
ThreadLocal 誕生於 JDK 1.2,用於解決多線程間的數據隔離問題。也就是說 ThreadLocal 會爲每一個線程創建一個單獨的變量副本。
ThreadLocal 有什麼用?
ThreadLocal 最典型的使用場景有兩個:
-
ThreadLocal 可以用來管理 Session,因爲每個人的信息都是不一樣的,所以就很適合用 ThreadLocal 來管理;
-
數據庫連接,爲每一個線程分配一個獨立的資源,也適合用 ThreadLocal 來實現。
其中,ThreadLocal 也被用在很多大型開源框架中,比如 Spring 的事務管理器,還有 Hibernate 的 Session 管理等,既然 ThreadLocal 用途如此廣泛,那接下來就讓我們共同看看 ThreadLocal 要怎麼用?ThreadLocal 使用中要注意什麼?以及 ThreadLocal 的存儲原理等,一起來看吧。
ThreadLocal 使用
ThreadLocal 基本使用
ThreadLocal 常用方法有 set(T)、get()、remove() 等,具體使用請參考以下代碼。
ThreadLocal threadLocal = new ThreadLocal();
// 存值
threadLocal.set(Arrays.asList("老王", "Java 面試題"));
// 取值
List list = (List) threadLocal.get();
System.out.println(list.size());
System.out.println(threadLocal.get());
//刪除值
threadLocal.remove();
System.out.println(threadLocal.get());
以上程序執行結果如下:
2
[老王, Java 面試題]
null
ThreadLocal 所有方法,如下圖所示:
ThreadLocal 數據共享
既然 ThreadLocal 設計的初衷是解決線程間信息隔離的,那 ThreadLocal 能不能實現線程間信息共享呢?
答案是肯定的,只需要使用 ThreadLocal 的子類 InheritableThreadLocal 就可以輕鬆實現,來看具體實現代碼:
ThreadLocal inheritableThreadLocal = new InheritableThreadLocal();
inheritableThreadLocal.set("老王");
new Thread(() -> System.out.println(inheritableThreadLocal.get())).start();
以上程序執行結果如下:
老王
從以上代碼可以看出,主線程和新創建的線程之間實現了信息共享。
ThreadLocal 內存溢出
內存溢出
下面我們用代碼實現 ThreadLocal 內存溢出的情況,請參考以下代碼。
class ThreadLocalTest {
static ThreadLocal threadLocal = new ThreadLocal();
static Integer MOCK_MAX = 10000;
static Integer THREAD_MAX = 100;
public static void main(String[] args) throws InterruptedException {
ExecutorService executorService = Executors.newFixedThreadPool(THREAD_MAX);
for (int i = 0; i < THREAD_MAX; i++) {
executorService.execute(() -> {
threadLocal.set(new ThreadLocalTest().getList());
System.out.println(Thread.currentThread().getName());
});
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
executorService.shutdown();
}
List getList() {
List list = new ArrayList();
for (int i = 0; i < MOCK_MAX; i++) {
list.add("Version:JDK 8");
list.add("ThreadLocal");
list.add("Author:老王");
list.add("DateTime:" + LocalDateTime.now());
list.add("Test:ThreadLocal OOM");
}
return list;
}
}
設置 JVM(Java 虛擬機)啓動參數 -Xmx=100m
(最大運行內存 100 M),運行程序不久後就會出現如下異常:
此時我們用 VisualVM 觀察到程序運行的內存使用情況,發現內存一直在緩慢地上升直到內存超出最大值,從而發生內存溢出的情況。
內存使用情況,如下圖所示:
內存溢出原理分析
在開始之前,先來看下 ThreadLocal 是如何存儲數據的。
首先,找到 ThreadLocal.set() 的源碼,代碼如下(此源碼基於 JDK 8):
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
可以看出 ThreadLocal 首先獲取到 ThreadLocalMap 對象,然後再執行 ThreadLocalMap.set() 方法,進而打開此方法的源碼,代碼如下:
private void set(ThreadLocal<?> key, Object value) {
Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1);
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();
if (k == key) {
e.value = value;
return;
}
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
tab[i] = new Entry(key, value);
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
從整個代碼可以看出,首先 ThreadLocal 並不存儲數據,而是依靠 ThreadLocalMap 來存儲數據,ThreadLocalMap 中有一個 Entry 數組,每個 Entry 對象是以 K/V 的形式對數據進行存儲的,其中 K 就是 ThreadLocal 本身,而 V 就是要存儲的值,如下圖所示:
可以看出:一個 Thread 中只有一個 ThreadLocalMap,每個 ThreadLocalMap 中存有多個 ThreadLocal,ThreadLocal 引用關係如下:
其中:實線代表強引用,虛線代表弱引用(弱引用具有更短暫的生命週期,在執行垃圾回收時,一旦發現只具有弱引用的對象,不管當前內存空間足夠與否,都會回收它的內存)。
看到這裏我們就理解了 ThreadLocal 造成內存溢出的原因:如果 ThreadLocal 沒有被直接引用(外部強引用),在 GC(垃圾回收)時,由於 ThreadLocalMap 中的 key 是弱引用,所以一定就會被回收,這樣一來 ThreadLocalMap 中就會出現 key 爲 null 的 Entry,並且沒有辦法訪問這些數據,如果當前線程再遲遲不結束的話,這些 key 爲 null 的 Entry 的 value 就會一直存在一條強引用鏈:Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value 並且永遠無法回收,從而造成內存泄漏。
ThreadLocal 的正確使用方法
既然已經瞭解了 ThreadLocal 內存溢出的原因,那解決的辦法就很簡單了,只需要在使用完 ThreadLocal 之後,把內容刪除掉(remove)就可以,因此 ThreadLocal 完整的正確使用代碼如下:
class ThreadLocalTest {
static ThreadLocal threadLocal = new ThreadLocal();
static Integer MOCK_MAX = 10000;
static Integer THREAD_MAX = 100;
public static void main(String[] args) throws InterruptedException {
ExecutorService executorService = Executors.newFixedThreadPool(THREAD_MAX);
for (int i = 0; i < THREAD_MAX; i++) {
executorService.execute(() -> {
threadLocal.set(new ThreadLocalTest().getList());
System.out.println(Thread.currentThread().getName());
// 移除對象
threadLocal.remove();
});
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
executorService.shutdown();
}
List getList() {
List list = new ArrayList();
for (int i = 0; i < MOCK_MAX; i++) {
list.add("Version:JDK 8");
list.add("ThreadLocal");
list.add("Author:老王");
list.add("DateTime:" + LocalDateTime.now());
list.add("Test:ThreadLocal OOM");
}
return list;
}
}
可以看出核心代碼,我們添加了一句 threadLocal.remove() 命令就解決了內存溢出的問題,這個時候運行代碼觀察,發現內存的值一直在一個固定的範圍內,如下圖所示:
這樣就解決了 ThreadLocal 內存溢出的問題了。
相關面試題
1.ThreadLocal 爲什麼是線程安全的?
答:ThreadLocal 爲每一個線程維護變量的副本,把共享數據的可見範圍限制在同一個線程之內,因此 ThreadLocal 是線程安全的,每個線程都有屬於自己的變量。
2.ThreadLocal 如何共享數據?
答:通過 ThreadLocal 的子類 InheritableThreadLocal 可以天然的支持多線程間的信息共享。
3.以下程序打印的結果是 true 還是 false?
ThreadLocal threadLocal = new InheritableThreadLocal();
threadLocal.set("老王");
ThreadLocal threadLocal2 = new ThreadLocal();
threadLocal2.set("老王");
new Thread(() -> {
System.out.println(threadLocal.get().equals(threadLocal2.get()));
}).start();
答:false。
題目分析:因爲 threadLocal 使用的是 InheritableThreadLocal(共享本地線程),所以 threadLocal.get() 結果爲 老王,而 threadLocal2 使用的是 ThreadLocal,因此在新線程中 threadLocal2.get() 的結果爲 null,因而它們比較的最終結果爲 false。
4.ThreadLocal 爲什麼會發生內存溢出?
答:ThreadLocal 造成內存溢出的原因:如果 ThreadLocal 沒有被直接引用(外部強引用),在 GC(垃圾回收)時,由於 ThreadLocalMap 中的 key 是弱引用,所以一定就會被回收,這樣一來 ThreadLocalMap 中就會出現 key 爲 null 的 Entry,並且沒有辦法訪問這些數據,如果當前線程再遲遲不結束的話,這些 key 爲 null 的 Entry 的 value 就會一直存在一條強引用鏈:Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value 並且永遠無法回收,從而造成內存泄漏。
5.解決 ThreadLocal 內存溢出的關鍵代碼是什麼?
答:關鍵代碼爲 threadLocal.remove()
,使用完 ThreadLocal 之後,調用remove() 方法,清除掉 ThreadLocalMap 中的無用數據就可以避免內存溢出了。
6.ThreadLocal 和 Synchonized 有什麼區別?
答:ThreadLocal 和 Synchonized 都用於解決多線程併發訪問,防止任務在共享資源上產生衝突,但是 ThreadLocal 與 Synchronized 有本質的區別,Synchronized 用於實現同步機制,是利用鎖的機制使變量或代碼塊在某一時刻只能被一個線程訪問,是一種 “以時間換空間” 的方式;而 ThreadLocal 爲每一個線程提供了獨立的變量副本,這樣每個線程的(變量)操作都是相互隔離的,這是一種 “以空間換時間” 的方式。
總結
ThreadLocal 的主要方法是 set(T) 和 get(),用於多線程間的數據隔離,ThreadLocal 也提供了 InheritableThreadLocal 子類,用於實現多線程間的數據共享。但使用 ThreadLocal 一定要注意用完之後使用 remove() 清空 ThreadLocal,不然會操作內存溢出的問題。
下一篇:線程安全
在公衆號菜單中可自行獲取專屬架構視頻資料,包括不限於 java架構、python系列、人工智能系列、架構系列,以及最新面試、小程序、大前端均無私奉獻,你會感謝我的哈
往期精選