概述
ThreadLocal無論是在平常的項目中還是面試都會比較頻繁的出現,今天星期天,抽時間總結一下。首先ThreadLocal的出現是爲解決多線程程序的併發問題的,ThreadLocal提供了線程內存儲變量的能力,這些變量不同之處在於每一個線程讀取的變量是對應的互相獨立的。各個線程通過ThreadLocal的get和set方法就可以設置和得到當前線程對應的值。這樣說起來比較抽象,我們下面通過實際的案例來解釋說明。
目錄
一、ThreadLocal的基本使用
爲了說明ThreadLocal的作用,我們先看一下沒有ThreadLocal的情況
public class ThreadLocalBDemo{
static Integer num ;
public static void main(String[] args) {
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() +":" + num);
num = 1;
System.out.println(Thread.currentThread().getName() + ":" + num);
}
});
Thread thread2 = new Thread(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() +":" + num);
num = 2;
System.out.println(Thread.currentThread().getName() + ":" + num);
}
});
thread1.start();
try {
/**
*加入這個join方法主要是爲了讓線程thread1和thread2順序執行
* 我們在main線程中調用了thread1的join方法,那麼main線程就會被阻塞
* 等到thread1線程執行完後,main線程纔會繼續執行,所以在main線程中的
* thread2.start()纔會執行,這樣就保證了thread1和thread2順序執行
*/
thread1.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
thread2.start();
}
}
我們在ThreadLocalBDemo定義了一個全局變量 num,並且開啓兩個線程thread1和thread2,在兩個線程中分別對num進行賦值處理,看一下輸出結果:
Thread-0:null
Thread-0:1
Thread-1:1
Thread-1:2
通過輸出結果我們可以看出,我們在thread1中給num賦值爲1後,在thread2中的第一個輸出語句中也拿到num=1的結果。在一些業務需求中我們不希望這個變量線程共享,而是每個線程都有屬於它自己獨立的一份,比如在實際業務中我們每個線程代表一個用戶,這個num代表每個用戶自己特有的訂單數量等屬性,那麼像上面的程序就很明顯不符合業務需求和邏輯。這個時候我們引入了ThrealLocal,ThreadLocal會爲這個變量保存一個只屬於它自己線程本身的變量副本,從而避免上面兩個線程相互影響的情況。我們還是以上面的程序爲案例,用ThreadLocal對其進行改造:
package com.zhangxudong;
public class ThreadLocalDemo {
static ThreadLocal<Integer> threadLocal = new ThreadLocal<Integer>();
public static void main(String[] args) {
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
System.out.println(threadLocal.get());
threadLocal.set(1);
System.out.println(threadLocal.get());
}
});
Thread thread2 = new Thread(new Runnable() {
@Override
public void run() {
System.out.println(threadLocal.get());
threadLocal.set(2);
System.out.println(threadLocal.get());
}
});
thread1.start();
try {
thread1.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
thread2.start();
}
}
輸出結果:
null
1
null
2
通過上面的輸出結果我們可以看到雖然我在先執行的thread1中通過ThreadLocal的set方法賦值爲1,但在後執行的thread2中的第一行輸出語句中,通過get方法拿到的值確實null,這就實現了變量的隔離。那ThreadLocal是怎麼實現的這種各個線程變量隔離的呢?下面我們就進入ThreadLocal源碼內部瞭解一下。
二、ThrealLocal實現原理
首先我們看一下ThreadLocal在使用set方法賦值的時候,是怎麼實現爲每個線程保存一個變量副本的。我們先從set()方法入手:
set()方法
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
可以看到,我們在thread1線程中利用ThreadLocal的set方法設置值的時候,在set方法內部首先是通過Thread t = Thread.currentThread()拿到當前的線程,這個線程當然就是thread1本身。緊接着調用了getMap(t)方法,這個方法傳進去的參數就是當前線程thread1,那我們繼續看一下getMap(t)這個方法:
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
可以看到它返回值是一個ThreadLocalMap類型的threadLocals變量,這個threadLocals其實就是線程類Thread中的一個成員變量,當前這個時候也就是thread1實例對象中的threadLocals成員變量:
//Thread類中的成員變量
ThreadLocal.ThreadLocalMap threadLocals = null;
那麼這個ThreadLocalMap又是一個什麼東西呢?這個你就簡單的先理解爲它就是一個類似我們平常用的Map結構,裏面存放的是(key,value)鍵值對形式的數據。上面介紹的一切,總結起來其實就是一句話:在線程類中有一個成員變量threadLocals,這個成員變量的類型是ThreadLocalMap類型的,我們可以往裏面放一條條“鍵值對”形式的數據。所以我們每new一個線程,那麼在這個線程對象了裏就有一個只屬於這個線程對象的ThreadLocalMap類型的threadLocals變量。
繼續接着上面的set源碼看,通過getMap(t)方法在拿到這個屬於當前線程對象(thread1)的threadLocals變量後(map),判斷其是否爲空,如果爲空的話createMap()方法創建一個,如果不爲空的話,就往裏面存一個(key,value)形式的鍵值對,其中的這個key就是this,表示我們最開始創建的ThreadLocal對象,value就是我們最開始要設置的值。所以不管我們創建多少個線程實例,在每個線程實例中都有一個屬於它自己的threadLocals變量,通過set方法往裏面放值的時候,也是放到了只屬於這個線程的threadLocals中去(雖然各個線程中threadLocals的key是一樣的,都是我們最開始創建的ThreadLocal對象,但threadLocals本身是不一樣的,它屬於不同的線程,這裏可能有點繞,好好理解一下),所以我們在拿值得時候,也就會只從屬於當前線程得threadLocals中去拿,這就實現了變量的隔離。到此,ThreadLocal的set方法就講解完了。
get()方法
理解了set方法其實get方法就很好理解了,我們看一下get方法得源碼:
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null)
return (T)e.value;
}
return setInitialValue();
}
我們可以看到,get方法中首先也是拿到當前線程,然後也是通過getMap()方法拿到屬於它得threadLocals,如果這個threadLocals不爲空,則通過getEntry()方法得到存在裏面的鍵值對Entry類型的對象,這個鍵值對中的鍵就是ThreadLocal對象,值就是我們之前在set方法中設置的值,最後的e.value拿到的就是這個值。
三、ThreadLocal的內存泄漏問題
在上面講解THreadLocal的set()方法的時候我們知道,set方法會把ThreadLocal對象作爲鍵,具體的value作爲值,以鍵值對的形式存放在ThreadLocalMap類型的threadLocals變量中,而這個鍵值對整體是以一個Entry類型存在的。我們接着上面set源碼中的map.set(this, value)繼續點進去看:
private void set(ThreadLocal key, Object value) {
// We don't use a fast path as with get() because it is at
// least as common to use set() to create new entries as
// it is to replace existing ones, in which case, a fast
// path would fail more often than not.
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();
}
上面的源碼我們只看第8行和第28行就可以了,其他的先忽略,首先在這個set方法中的最開始有這麼一行代碼Entry[] tab = table,它維護了一個Entry類型的數組tab;接着看第28行:tab[i] = new Entry(key, value),可以發現是根據我們傳過來的key和value值來new了一個Entry對象,並把它放到了Entry數組tab中。整體關係就是Thread中有一個ThreadLocalMap類型的成員變量threadLocals,而在ThreadLocalMap是ThreadLocal類的一個靜態內部類,Entry又是ThreadLocalMap內部的一個靜態內部類。 我們繼續往下走,看看Entry的具體細節:
static class Entry extends WeakReference<ThreadLocal> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal k, Object v) {
super(k);
value = v;
}
}
Entry 是ThreadLocalMap的一個靜態內部類,並且它繼承了弱引用(關於弱引用可以參考我前篇的文章java中四種引用類型),在Entry的構造方法中有這麼一段:super(k),這說明它調用了其父類“弱應用WeakReference”的構造方法,並且將鍵值k傳了進去(這個key值就是指向ThreadLocal對象的引用)。那麼這個時候key就以一種弱引用的形式指向了ThreadLocal對象,我們通過一張圖來說明一下這個:
首先我們在最開始通過ThreadLocal t1 = new ThreadLocal()這新建一個ThreadLocal對象時,這個t1是一個強引用並且執行ThreadLocal對象。之後通過一系列上面的操作,我們知道鍵值對Entry中的key會以弱引用的形式也指向Thread Local對象。
那爲什麼Entry要使用弱引用呢?因爲若使用的是強引用,即使t1=null,但key依然以強引用的形式指向ThreadLocal對象,因此ThreadLocal對象不會被釋放掉,所以會有內存泄漏的問題。但如果是弱引用,當t1=null後,因爲key是以弱引用的形式指向ThreadLocal的對象,那麼gc碰到弱引用它是不關心的,依然會把ThreadLocal對象回收掉。但是雖然這樣,ThreadLocal還是會存在內存泄漏問題,因爲當ThreadLocal對象被回收後,key值就變成了null,這就導致整個Entry中的value再也無法訪問到,因此依然存在內存泄漏問題。綜上,ThreadLocal是存在內存泄漏的 問題的。