Java多線程之ThreadLocal
本文介紹下Java多線程方面高頻出現(xiàn)的ThreadLocal類

基本實(shí)踐
在傳統(tǒng)的多線程開(kāi)發(fā)場(chǎng)景中,為了避免多個(gè)線程同時(shí)操作一個(gè)共享變量而引起并發(fā)的問(wèn)題。通常會(huì)通過(guò)加鎖的形式進(jìn)行處理。特別是在這個(gè)共享變量,并不是一個(gè)所謂的共享資源而只是用于線程內(nèi)部各方法傳遞、使用的參數(shù)時(shí),這種加鎖的并發(fā)控制顯然會(huì)降低系統(tǒng)的吞吐量。而ThreadLocal類則給我們提供一個(gè)新的思路——線程本地私有存儲(chǔ)數(shù)據(jù)。簡(jiǎn)單來(lái)說(shuō)就是,ThreadLocal為共享變量在每個(gè)線程內(nèi)部提供了一個(gè)副本,用于進(jìn)行線程內(nèi)部自身的訪問(wèn)、存儲(chǔ)等操作。避免了該共享變量在多線程環(huán)境下的操作沖突。該類典型方法如下所示
// 設(shè)置Value
public void set(T value);
// 獲取數(shù)據(jù)
public T get();
// 移除數(shù)據(jù)
public void remove();
這里通過(guò)一個(gè)Demo來(lái)了解下該如何使用
package com.aaron.ThreadLocalTest;
import java.util.Random;
public class Demo1 {
public static void main(String[] args) throws Exception{
Runnable task = new MyTask();
new Thread( task ).start();
new Thread( task ).start();
}
}
class MyTask implements Runnable {
private Integer num = null;
private ThreadLocal<Integer> threadLocal = new ThreadLocal<>();
@Override
public void run() {
// 每個(gè)線程隨機(jī)生產(chǎn)一個(gè)數(shù)
Integer count = new Random().nextInt(100);
System.out.println(Thread.currentThread().getName() + ", count: " + count );
// 模擬業(yè)務(wù)耗時(shí)
try{
Thread.sleep(5000);
}catch (Exception e) {
}
// 存儲(chǔ)數(shù)據(jù)
num = count;
threadLocal.set(count);
// 獲取數(shù)據(jù)
System.out.println( Thread.currentThread().getName() + ", num: " + num + ", threadLocal: " +threadLocal.get() );
// 移除當(dāng)前線程所存的數(shù)據(jù)
threadLocal.remove();
}
}
通過(guò)上述示例及其測(cè)試結(jié)果,可以看出對(duì)于普通的共享變量num而言,在多線程操作過(guò)程中會(huì)發(fā)生沖突。具體表現(xiàn)為T(mén)hread-0線程下該變量本來(lái)為53,卻又被Thread-1線程修改為74;而對(duì)于threadLocal變量而言,從測(cè)試結(jié)果直觀上我們就可看出并未在多線程環(huán)境下發(fā)生沖突,各線程的threadLocal變量數(shù)據(jù)被線程隔離了

實(shí)現(xiàn)原理
前面我們提到ThreadLocal是通過(guò)線程本地私有存儲(chǔ)數(shù)據(jù)實(shí)現(xiàn)線程安全的。這里結(jié)合ThreadLocal、Thread類的源碼做進(jìn)一步闡述。以set方法為例,其首先通過(guò)currentThread獲取當(dāng)前線程的Thread實(shí)例。并通過(guò)getMap方法獲取該線程的threadLocals屬性,即一個(gè)ThreadLocal.ThreadLocalMap實(shí)例。而在ThreadLocalMap則通過(guò)Entry實(shí)現(xiàn)對(duì)ThreadLocal及其值的存儲(chǔ)。進(jìn)一步地,為了支持存儲(chǔ)多個(gè)ThreadLocal變量及其值,ThreadLocalMap類中提供一個(gè)Entry數(shù)組類型的table字段。get方法同理,不再贅述。簡(jiǎn)而言之,ThreadLocal之所以可以實(shí)現(xiàn)對(duì)數(shù)據(jù)的線程隔離,是因?yàn)槠鋵?shù)據(jù)存儲(chǔ)到Thread實(shí)例中
public class ThreadLocal<T> {
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
map.set(this, value);
} else {
createMap(t, value);
}
}
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();
}
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
static class ThreadLocalMap {
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
private Entry[] table;
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);
}
}
}
...
public class Thread implements Runnable {
ThreadLocal.ThreadLocalMap threadLocals = null;
}
應(yīng)用場(chǎng)景
線程內(nèi)資源的復(fù)用
眾所周知,時(shí)間格式化類SimpleDateFormat在多線程環(huán)境下是非線程安全的。為此傳統(tǒng)的解決方案,要么通過(guò)加鎖的方式實(shí)現(xiàn),此舉會(huì)產(chǎn)生阻塞顯著降低效率;要么則定義為局部變量,每次使用需自行new該實(shí)例,如果任務(wù)的數(shù)量較多,顯然會(huì)嚴(yán)重浪費(fèi)內(nèi)存、CPU等資源。而ThreadLocal則可以很好的解決該問(wèn)題。將SimpleDateFormat實(shí)例與線程實(shí)例進(jìn)行綁定。一方面,各線程使用不同的SimpleDateFormat實(shí)例,避免了SimpleDateFormat線程不安全問(wèn)題;另一方面,在基于線程池的方式利用線程的場(chǎng)景下,該線程所綁定的SimpleDateFormat實(shí)例在本次任務(wù)完成后,可以在該線程下一次的任務(wù)中繼續(xù)復(fù)用。避免根據(jù)任務(wù)頻繁地創(chuàng)建SimpleDateFormat實(shí)例。示例Demo如下所示
public class Demo2 {
public static void main(String[] args) throws Exception{
Runnable task = new Task();
new Thread( task ).start();
new Thread( task ).start();
}
}
class Task implements Runnable {
private SimpleDateFormat dateFormat1 = new SimpleDateFormat("HH:mm:ss");
// 使用static進(jìn)行修飾,保持對(duì)ThreadLocal實(shí)例的強(qiáng)引用。這樣只要該線程不結(jié)束退出,該SimpleDateFormat即可通過(guò)dateFormat2重復(fù)訪問(wèn)、使用
private static ThreadLocal<SimpleDateFormat> dateFormat2 = ThreadLocal.withInitial(
() -> new SimpleDateFormat("HH:mm:ss")
);
@Override
public void run() {
Long ts = RandomUtil.randomLong(10L, 5000000L);
Date date = new Date(ts);
System.out.println(Thread.currentThread().getName() + ", date: " + date );
String str1 = dateFormat1.format(date);
String str2 = dateFormat2.get().format(date);
System.out.println( Thread.currentThread().getName() + ", str1: " + str1 + ", str2: " + str2 );
}
}
測(cè)試結(jié)果如下所示,兩個(gè)線程str1輸出的結(jié)果證明了SimpleDateFormat的非線程安全

傳遞上下文
鑒于ThreadLocal的線程隔離特性,可以很方便我們?cè)谝粋€(gè)線程內(nèi)的多個(gè)方法進(jìn)行參數(shù)傳遞。即所謂的Context上下文。典型地,包括用戶身份信息、數(shù)據(jù)庫(kù)連接信息等。以避免通過(guò)添加入?yún)⒌男问竭M(jìn)行傳遞
內(nèi)存泄露
Entry的Key
前面我們提到在ThreadLocalMap內(nèi)部是通過(guò)Entry實(shí)現(xiàn)對(duì)ThreadLocal及其值的存儲(chǔ)。而Entry的key字段則是一個(gè)指向ThreadLocal實(shí)例的弱引用。這里對(duì)弱引用 WeakReference 作必要的補(bǔ)充說(shuō)明:GC進(jìn)行回收時(shí),對(duì)于只具有弱引用的對(duì)象,不管當(dāng)前內(nèi)存空間是否充足,均會(huì)回收該對(duì)象。故當(dāng)一個(gè)ThreadLocal實(shí)例沒(méi)有外部強(qiáng)引用時(shí),其必然可以被GC回收,顯然利用static修飾的ThreadLocal變量除外。試想如果Entry的key字段是一個(gè)指向ThreadLocal實(shí)例的強(qiáng)引用,那么如果該線程永遠(yuǎn)不結(jié)束退出,則會(huì)導(dǎo)致ThreadLocal實(shí)例無(wú)法被回收
Entry的Value
需要注意的是,Entry存儲(chǔ)value則是通過(guò)強(qiáng)引用進(jìn)行關(guān)聯(lián)的。結(jié)合前面通過(guò)對(duì)Entry的key進(jìn)行分析可知,一旦ThreadLocal實(shí)例不存在外部的強(qiáng)引用而被GC回收后,則相應(yīng)的Entry實(shí)例就會(huì)變?yōu)閗ey為null而value依然存在強(qiáng)引用。除非該線程退出結(jié)束,否則該value對(duì)象將會(huì)一直被Entry實(shí)例強(qiáng)引用而無(wú)法進(jìn)行回收。這也是大家通常所說(shuō)的ThreadLocal存在內(nèi)存泄露的根源所在。其實(shí),在ThreadLocal的set、get方法內(nèi)部實(shí)現(xiàn)中也會(huì)對(duì)Entry數(shù)組進(jìn)行檢查,對(duì)key為null的Entry實(shí)例進(jìn)行清除。但顯然這種清除的觸發(fā)是有一定的條件。故推薦大家在使用完ThreadLocal后,通過(guò)remove方法顯式地清除該value值。或者將ThreadLocal變量修飾為static屬性,以保持對(duì)ThreadLocal實(shí)例的強(qiáng)引用。這樣就能保證任何時(shí)候均可以通過(guò)ThreadLocal實(shí)例對(duì)value進(jìn)行更新、清除等操作
