如果五輪面試中四輪問道了ThreadLocal,是一種什么體驗?
在java的多線程模塊中,ThreadLocal是經(jīng)常被提問到的一個知識點 ,因此只有理解透徹了,不管怎么問,都能游刃有余。
這篇文章主要從以下幾個角度來分析理解
1、ThreadLocal是什么
2、ThreadLocal怎么用
3、ThreadLocal源碼分析
4、ThreadLocal內(nèi)存泄漏問題
以下源碼均基于jdk1.8。
ThreadLocal是什么?
從名字我們就可以看到ThreadLocal 叫做本地線程變量,意思是說,ThreadLocal 中填充的的是當前線程的變量,該變量對其他線程而言是封閉且隔離的,ThreadLocal 為變量在每個線程中創(chuàng)建了一個副本,這樣每個線程都可以訪問自己內(nèi)部的副本變量。
從字面意思很容易理解,但是實際角度就沒那么容易了,作為一個面試常問的點,使用場景也是很豐富。
1、在進行對象跨層傳遞的時候,使用ThreadLocal可以避免多次傳遞,打破層次間的約束。
2、線程間數(shù)據(jù)隔離
3、進行事務操作,用于存儲線程事務信息。
4、數(shù)據(jù)庫連接,Session會話管理。
現(xiàn)在相信你已經(jīng)對ThreadLocal有一個大致的認識了,下面我們看看如何用?
ThreadLocal怎么用?
下面讓我們來看一個例子:
public class ThreadLocalTest02 {
public static void main(String[] args) {
ThreadLocal<String> local = new ThreadLocal<>();
IntStream.range(0, 10).forEach(i -> new Thread(() -> {
local.set(Thread.currentThread().getName() + ":" + i);
System.out.println("線程:" + Thread.currentThread().getName() + ",local:" + local.get());
}).start());
}
}
輸出結(jié)果:
線程:Thread-0,local:Thread-0:0
線程:Thread-1,local:Thread-1:1
線程:Thread-2,local:Thread-2:2
線程:Thread-3,local:Thread-3:3
線程:Thread-4,local:Thread-4:4
線程:Thread-5,local:Thread-5:5
線程:Thread-6,local:Thread-6:6
線程:Thread-7,local:Thread-7:7
線程:Thread-8,local:Thread-8:8
線程:Thread-9,local:Thread-9:9
從結(jié)果可以看到,每一個線程都有自己的local 值,這就是TheadLocal的基本使用 。
下面我們從源碼的角度來分析一下,ThreadLocal的工作原理。
ThreadLocal源碼分析
1、set 方法
/**
* Sets the current thread's copy of this thread-local variable
* to the specified value. Most subclasses will have no need to
* override this method, relying solely on the {@link #initialValue}
* method to set the values of thread-locals.
*
* @param value the value to be stored in the current thread's copy of
* this thread-local.
*/
public void set(T value) {
//首先獲取當前線程對象
Thread t = Thread.currentThread();
//獲取線程中變量 ThreadLocal.ThreadLocalMap
ThreadLocalMap map = getMap(t);
//如果不為空,
if (map != null)
map.set(this, value);
else
//如果為空,初始化該線程對象的map變量,其中key 為當前的threadlocal 變量
createMap(t, value);
}
/**
* Create the map associated with a ThreadLocal. Overridden in
* InheritableThreadLocal.
*
* @param t the current thread
* @param firstValue value for the initial entry of the map
*/
//初始化線程內(nèi)部變量 threadLocals ,key 為當前 threadlocal
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
/**
* Construct a new map initially containing (firstKey, firstValue).
* ThreadLocalMaps are constructed lazily, so we only create
* one when we have at least one entry to put in it.
*/
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
table = new Entry[INITIAL_CAPACITY];
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
table[i] = new Entry(firstKey, firstValue);
size = 1;
setThreshold(INITIAL_CAPACITY);
}
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
匯總下,ThreadLocalMap 為 ThreadLocal 的一個靜態(tài)內(nèi)部類,里面定義了Entry 來保存數(shù)據(jù)。而且是繼承的弱引用。在Entry內(nèi)部使用ThreadLocal作為key,使用我們設(shè)置的value作為value。
對于每個線程內(nèi)部有個ThreadLocal.ThreadLocalMap 變量,存取值的時候,也是從這個容器中來獲取。
2、get 方法
/**
* Returns the value in the current thread's copy of this
* thread-local variable. If the variable has no value for the
* current thread, it is first initialized to the value returned
* by an invocation of the {@link #initialValue} method.
*
* @return the current thread's value of this thread-local
*/
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
通過上面的分析,相信你對該方法已經(jīng)有所理解了,首先獲取當前線程,然后通過key threadlocal 獲取 設(shè)置的value 。
ThreadLocal 內(nèi)存泄漏問題
我們首先來看下,下面這個類:
/**
* 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;
}
}
注釋說的很清楚了,Note that null keys (i.e. entry.get()* == null)
如果 key threadlocal 為 null 了,這個 entry 就可以清除了。
ThreadLocal是一個弱引用,當為null時,會被當成垃圾回收 。
重點來了,突然我們ThreadLocal是null了,也就是要被垃圾回收器回收了,但是此時我們的ThreadLocalMap(thread 的內(nèi)部屬性)生命周期和Thread的一樣,它不會回收,這時候就出現(xiàn)了一個現(xiàn)象。那就是ThreadLocalMap的key沒了,但是value還在,這就造成了內(nèi)存泄漏。
解決辦法:使用完ThreadLocal后,執(zhí)行remove操作,避免出現(xiàn)內(nèi)存溢出情況。
所以 如同 lock 的操作 最后要執(zhí)行解鎖操作一樣,ThreadLocal使用完畢一定記得執(zhí)行remove 方法,清除當前線程的數(shù)值。
如果不remove 當前線程對應的VALUE ,就會一直存在這個值。
使用了線程池,可以達到“線程復用”的效果。但是歸還線程之前記得清除ThreadLocalMap,要不然再取出該線程的時候,ThreadLocal變量還會存在。這就不僅僅是內(nèi)存泄露的問題了,整個業(yè)務邏輯都可能會出錯。
為什么key使用弱引用?
如果使用強引用,當ThreadLocal 對象的引用(強引用)被回收了,ThreadLocalMap 本身依然還持有ThreadLocal的強引用,如果沒有手動刪除這個key ,則ThreadLocal 不會被回收,所以只要當前線程不消亡,ThreadLocalMap引用的那些對象就不會被回收, 可以認為這導致Entry內(nèi)存泄漏。
附:強引用-軟引用-弱引用
- 強引用:普通的引用,強引用指向的對象不會被回收;
- 軟引用:僅有軟引用指向的對象,只有發(fā)生gc且內(nèi)存不足,才會被回收;
- 弱引用:僅有弱引用指向的對象,只要發(fā)生gc就會被回收。
備注這篇文章寫的也很好:https://blog.csdn.net/puppylpg/article/details/80433271