徹底搞清楚ThreadLocal與弱引用
眾所周知,多線程訪問(wèn)同一個(gè)競(jìng)態(tài)變量的時(shí)候容易出現(xiàn)并發(fā)問(wèn)題,特別是多個(gè)線程對(duì)一個(gè)變量進(jìn)行寫入的時(shí)候,為了保證線程安全,一般使用者在訪問(wèn)共享變量的時(shí)候需要進(jìn)行額外的同步措施才能保證線程安全性。ThreadLocal是除了加鎖這種同步方式之外的一種規(guī)避多線程訪問(wèn)出現(xiàn)線程不安全的方法,它實(shí)現(xiàn)了一種機(jī)制,這種機(jī)制可以復(fù)制一份競(jìng)態(tài)變量的副本,每個(gè)線程只訪問(wèn)一份副本,從而避免了對(duì)競(jìng)態(tài)變量的直接操作,消除了并發(fā)問(wèn)題。那么ThreadLocal的作用原理是什么呢?下面我們將用一段代碼來(lái)揭開ThreadLocal的面紗。
一、ThreadLocal基本原理
示例代碼如下:
public class ThreadLocalDemo {
public static ThreadLocal<String> threadLocal = new ThreadLocal<String>();
public static void main(String[] args) {
ThreadLocalDemo.threadLocal.set("hello world main");
System.out.println("創(chuàng)建新線程前,主線程" + Thread.currentThread().getName() + "的threadlocal字符值為:" + ThreadLocalDemo.threadLocal.get());
try {
Thread thread = new Thread() {
@Override
public void run() {
ThreadLocalDemo.threadLocal.set("new thread");
System.out.println("新線程" + Thread.currentThread().getName() + "的threadlocal字符值為:" + ThreadLocalDemo.threadLocal.get());
}
};
thread.start();
thread.join();
} catch (Exception e) {
System.out.println(e);
}
System.out.println("創(chuàng)建新線程后,主線程" + Thread.currentThread().getName() + "的threadlocal字符值為:" + ThreadLocalDemo.threadLocal.get());
}
}代碼的邏輯很簡(jiǎn)單:在主類中定義了一個(gè)靜態(tài)變量threadLocal,在主線程中先設(shè)置這個(gè)變量的字符值為"hello world main",隨后在主線程中創(chuàng)建一個(gè)新線程,并在新線程的run方法中修改threadLocal的字符值為“new thread”,然后主線程再把threadlocal的字符值打印一次。為了確保新線程一定會(huì)在主線程第二次打印前打印threadlocal的值,這里采用join方法,讓新線程強(qiáng)行“加塞”,阻塞主線程,直到新線程執(zhí)行完run方法后,主線程才解除阻塞,繼續(xù)打印。執(zhí)行結(jié)果如下:

從結(jié)果上來(lái)看,新線程對(duì)threadlocal字符值的修改,并沒(méi)有影響到主線程的threadlocal的字符值的變化,即使threadlocal的類型是static的。這說(shuō)明,新線程所修改的threadlocal是一份主線程的threadlocal的副本,那么這一點(diǎn)是怎么實(shí)現(xiàn)的呢?下面我們就一行行分析源代碼來(lái)了解,首先我們先看main方法中的第一行代碼:
ThreadLocalDemo.threadLocal.set("hello world main");我們點(diǎn)進(jìn)這個(gè)set方法,相應(yīng)源碼如下:
/**
* 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();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}這里邊有一個(gè)ThreadLocalMap的類,并且通過(guò)一個(gè)叫g(shù)etMap(t)的方法來(lái)獲取這個(gè)類的一個(gè)實(shí)例,隨后把threadLocal變量的地址作為key,以字符值為value存放在這個(gè)map中。如果map為空的話,會(huì)調(diào)用createMap(t,value)來(lái)創(chuàng)建一個(gè)map,我們點(diǎn)進(jìn)createMap方法,代碼如下:
/**
* 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
*/
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}threadLocals是Thread類的一個(gè)靜態(tài)成員變量,它的類型是Thread的一個(gè)靜態(tài)內(nèi)部類ThreadLocalMap,如下所示:
/* ThreadLocal values pertaining to this thread. This map is maintained
* by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;我們用圖來(lái)總結(jié)一下上面的步驟,程序啟動(dòng)時(shí),執(zhí)行代碼:
public static ThreadLocal<String> threadLocal = new ThreadLocal<String>();此時(shí),整個(gè)棧內(nèi)存和堆內(nèi)存的情況如下圖所示:

然后,在main方法中執(zhí)行:
ThreadLocalDemo.threadLocal.set("hello world main");該過(guò)程創(chuàng)建新的ThreadLocalMap實(shí)例,它的key指向ThreadLocal對(duì)象,value為“hello world main”并且這個(gè)key是個(gè)弱引用(弱引用是什么以及這里為什么使用弱引用,后面會(huì)提),如下圖所示:

隨后,main方法中創(chuàng)建Thread,并在Thread方法中又調(diào)用了
ThreadLocalDemo.threadLocal.set("new thread");因此,堆內(nèi)存中將創(chuàng)建兩個(gè)對(duì)象,一個(gè)是Thread對(duì)象,代表新線程;一個(gè)是Thread的ThreadLoaclMap的實(shí)例,如下圖所示:

總結(jié):每在一個(gè)新線程中調(diào)用一次threadLocal.set("xxx")方法,就會(huì)在堆內(nèi)存中創(chuàng)建一個(gè)新的ThreadLocalMap實(shí)例,這個(gè)實(shí)例通過(guò)Entry的方式保存key和value,value是不同的,而key都指向同一個(gè)ThreadLocal對(duì)象。
二、為什么使用弱引用
我們知道java的引用分為強(qiáng)、軟、弱、虛四種類型,其他類型因篇幅有限,暫且不表。只說(shuō)說(shuō)弱引用,弱引用的定義是:如果一個(gè)對(duì)象僅被一個(gè)弱引用指向,那么當(dāng)下一次GC到來(lái)時(shí),這個(gè)對(duì)象一定會(huì)被垃圾回收器回收掉。觀察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;
}
}我們觀察到ThreadLocalMap的key繼承了弱引用,這是為什么呢?光結(jié)合定義來(lái)體會(huì)肯定無(wú)法深入體會(huì),讓我們結(jié)合圖來(lái)分析一下。還是上面那張圖,假設(shè)兩條虛線不是弱引用,而是強(qiáng)引用,如下圖紅線所示:

此時(shí),假設(shè)我們?cè)谥骶€程或者新線程中添加一行代碼 :
ThreadLocalDemo.threadLocal = null;即我們主動(dòng)釋放掉對(duì)ThreadLocalDemo.threadLocal 在兩個(gè)線程中的引用,結(jié)果如下圖所示:

我們可以看到,雖然兩個(gè)線程都主動(dòng)釋放掉了對(duì)ThreadLocal對(duì)象的引用,但是,從主線程thread引用->ThreadLocal對(duì)象,依然存在這一條可達(dá)路徑。眾所周知,現(xiàn)今主流JVM判斷一個(gè)對(duì)象是否可回收的算法通常為可達(dá)路徑算法,而不是引用計(jì)數(shù)法。可達(dá)路徑算法以GCROOT出發(fā),如果存在一條通向某個(gè)對(duì)象的強(qiáng)引用通路,那么這個(gè)對(duì)象是永遠(yuǎn)不會(huì)回收掉的(即便發(fā)生OOM也不會(huì)回收)。thread的引用是主線程的一個(gè)本地變量,根據(jù)GCROOT算法,thread的引用是可以作為一個(gè)GCROOT的,那么現(xiàn)狀就是:我們顯式地釋放掉了threadLocal的引用(ThreadLocalDemo.threadLocal = null;),因?yàn)槲覀兇_認(rèn)后續(xù)我們不會(huì)使用到它了,但是,由于存在GCROOT的一條可達(dá)通路,程序并沒(méi)有像我們希望的那樣立刻釋放掉ThreadLocal對(duì)象,直到我們所有的線程都釋放掉了,即程序結(jié)束,ThreadLocal對(duì)象才會(huì)被真正的釋放掉,這無(wú)疑就是內(nèi)存泄露。為了解決這個(gè)問(wèn)題,我們把圖中的紅線換成弱引用,如下圖所示

