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

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

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

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

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

此時,假設我們在主線程或者新線程中添加一行代碼 :
ThreadLocalDemo.threadLocal = null;即我們主動釋放掉對ThreadLocalDemo.threadLocal 在兩個線程中的引用,結(jié)果如下圖所示:

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

