目錄
什麼是ThreadLocal?
ThreadLocal,意爲線程本地變量
ThreadLocal是JDK包提供的,它提供了線程本地變量,也就是如果你創建了一個ThreadLocal變量,那麼訪問這個變量的每個線程都會有這個變量的一個本地副本。當多個線程操作這個變量時,實際操作的是自己本地內存裏面的變量,從而避免了線程安全問題。
如何使用ThreadLocal?
我們先看下面這段代碼
public class ThreadTest{
// 創建ThreadLocal變量
static ThreadLocal<String> localVariable = new ThreadLocal<>();
static void print(String str) {
// 打印當前線程副本變量的值
System.out.println(str + ":" + localVariable.get());
}
public static void main(String[] args) {
// 創建線程1
Thread threadOne = new Thread(new Runnable() {
@Override
public void run() {
localVariable.set("this is threadOne local variable");
print("threadOne");
System.out.println("threadOne remove after:" + localVariable.get());
}
});
// 創建線程2
Thread threadTwo = new Thread(new Runnable() {
@Override
public void run() {
localVariable.set("this is threadTwo local variable");
print("threadTwo");
System.out.println("threadTwo remove after:" + localVariable.get());
}
});
// 開啓線程
threadOne.start();
threadTwo.start();
}
}
通過運行結果,我們可以看出,兩個線程通過調用print方法都打印出了當前線程副本變量的值,然後又在run()方法中獲取到了當前線程的副本變量的值。
接下來我們在print()方法中添加一條語句,也就是輸出完之後就將副本變量刪除掉
// 清除當前線程副本變量
localVariable.remove();
然後再來看一下運行結果
我們可以發現,在run()方法中再次獲取localVariable變量的值時,已經獲取不到了,因爲那個副本變量已經在當前線程中刪除掉了。
內存泄漏
爲什麼會導致內存泄漏?
ThreadLocalMap 使用 ThreadLocal 的弱引用作爲key,如果一個 ThreadLocal 沒有外部強引用來引用它,那麼系統 GC 的時候,這個 ThreadLocal 就會被回收,由此一來,ThreadLocalMap 中就會出現 key 爲 null 的 Entry,也就沒有辦法再去訪問對應的 value,如果當前線程再遲遲不結束的話,這些 key 爲 null 的 Entry 的 value 就會一直存在一條強引用鏈:Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value 永遠無法回收,然後下次再使用的時候則會繼續創建新的,越來越多,最終造成內存泄漏。
如何避免內存泄漏?
爲了避免內存泄漏,當不需要使用本地變量時可以通過調用ThreadLocal變量的remove方法,從當前線程的threadLocals裏面刪除該本地變量,如果一直不刪除,最終會導致內存泄漏。
源碼解析:
Thread類中有一個threadLocals和inheritableThreadLocals,它們都是ThreadLocalMap類型的變量,而ThreadLocalMap是一個特殊的HashMap
void set()方法
(在當前線程中,設置一個變量的副本)
先獲取當前線程,然後根據當前線程作爲key,獲取到對應線程的變量,如果map不爲空就爲這個map賦值,否則就創建一個新的變量,併爲其賦值
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
getMap()源碼如下:
就是獲取對應線程自己的變量threadLocals
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
createMap()源碼如下:
創建一個新的threadLocals變量
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
T get()方法
(獲取當前線程中副本變量的值)
首先獲取當前線程,然後根據當前線程作爲key,獲取到對應線程的ThreadLocals變量,如果變量不爲空,則返回當前線程綁定的本地變量,否則執行setInitialValue()
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();
}
getEntry源碼如下:
官方解釋:此方法本身只處理快速路徑::直接命中已存在的鍵,爲了最大限度地提高直接命中的性能而設計的...
private Entry getEntry(ThreadLocal<?> key) {
int i = key.threadLocalHashCode & (table.length - 1);
Entry e = table[i];
if (e != null && e.get() == key)
return e;
else
return getEntryAfterMiss(key, i, e);
}
setInitialValue()源碼如下:
先設置一個value,初始化爲null,然後獲取當前線程,在獲取當前線程的threadLocals變量,如果變量不爲空,則設置變量的值爲value(也就是null),否則就新建一個threadLocals變量。
private T setInitialValue() {
T value = initialValue();
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
return value;
}
void remove()方法
(刪除當前線程中的副本變量)
private void remove(ThreadLocal<?> key) {
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)]) {
if (e.get() == key) {
e.clear();
expungeStaleEntry(i);
return;
}
}
}
InheritableThreadLocal類
ThreadLocal不支持繼承性:也就是說,同一個ThreadLocal變量在父線程中被設置值後,在子線程中是獲取不到的,那麼怎樣才能在子線程也能獲取到呢?這時就需要使用InheritableThreadLocal類了。
- InheritableThreadLocal繼承自ThreadLocal,其提供了一個特性,就是讓子線程可以訪問在父線程中設置的本地變量。
- 當父線程創建子線程時,構造函數會把父線程中inheritableThreadLocals變量裏面的本地變量複製一份保存到子線程的inheritableThreadLocals變量裏面。
例:
public class ThreadTest{
// 創建ThreadLocal變量
static ThreadLocal<String> localVariable = new ThreadLocal<>();
public static void main(String[] args) {
// 在主線程中設置變量的值
localVariable.set("Hello Java");
// 創建線程1
Thread threadOne = new Thread(new Runnable() {
@Override
public void run() {
// 子線程輸出變量的值
System.out.println("threadOne:" + localVariable.get());
}
});
// 開啓線程
threadOne.start();
// 主線程輸出變量的值
System.out.println("main:" + localVariable.get());
}
}
輸出結果如下 :
main:Hello Java
threadOne:null
我們可以看到,主線程中設置了變量的值,在子線程中是獲取不到的
然後我們修改上面代碼中創建變量的方法:
// 創建ThreadLocal變量
static ThreadLocal<String> localVariable = new InheritableThreadLocal<>();
接下來再運行一遍,看一下結果:
main:Hello Java
threadOne:Hello Java
可以看到,子線程中也獲取到了變量的值
因爲在父線程創建子線程時,構造函數會把父線程中inheritableThreadLocals變量裏面的副本變量複製一份保存到子線程的inheritableThreadLocals變量裏面。
注意:ThreadLocal使用完後,一定要刪除,否則會造成內存泄漏,平時的時候可能察覺不到,但當數據量很大的時候,就會出現內存泄漏的情況,而且也會影響業務邏輯。