
ThreadLocal簡單介紹
ThreadLocal同ReentrantLock,CyclicBarrier等都屬于并發(fā)工具類,他們都是為了解決多線程數(shù)據(jù)一致性問題而出現(xiàn)的。與ReentrantLock不同的是,ThreadLocal采取的是一種以空間換時間的策略。
舉個簡單的例子,假設(shè)現(xiàn)在有100個人填寫信息表,可是只有一支筆,為了防止哄搶,以ReentrantLock為代表的鎖所使用的思路是,通過控制人員使用筆的順序,來達(dá)到防止哄搶的目的。而ThreadLocal采取的思想則是給每個人發(fā)一只筆,這樣大家只使用自己手頭里的筆,也不就不存在競爭問題了。
正如ThreadLocal其名,ThreadLocal所擁有的變量是線程私有的,既然多個線程同時訪問一個共享變量會造成線程安全問題,那么我為什么不給每一個線程分配一個變量,這樣就避免了多線程之間的同步,沒有了同步,代碼的運行所需要的時間就會減少。雖然ThreadLocal的使用可以減少時間上的開銷,可是我們也很容易發(fā)現(xiàn)其會增大內(nèi)存空間上的開銷。
ThradLocal使用場景
SimpleDateFormat作為一個格式化日期的常用類,卻存在著線程不安全的問題,運行下面的代碼
public static void main(String[] args)
{
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
ExecutorService es = Executors.newFixedThreadPool(100);
for (int i = 0; i < 1000; i++)
{
es.execute(()->{
try
{
Date date = sdf.parse("2019-11-13 08:23:" + new Random().nextInt(60));
} catch (ParseException e)
{
e.printStackTrace();
}
});
}
es.shutdown();
}
會報出如下異常
java.lang.NumberFormatException: For input string: ""
at java.lang.NumberFormatException.forInputString(NumberFormatException.java:65)
究其原因,在于SimpleDateFormat類內(nèi)部有一個Calendar對象引用,它用來儲存和這個SimpleDateFormat相關(guān)的日期信息,而這個對象又是線程共享的且沒有做任何同步處理。
有了上面的分析,要解決這些異常也就不是什么難事了。大家首先想到的可能是加鎖,即用synchronize或ReeentrantLock把調(diào)用parse方法那一部分包起來,但是這種方法在多線程競爭激烈的時候會帶來效率問題,代碼這里我就不寫了。除了加鎖,還有一種更好的方法,那便是使用ThreadLocal。
public static void main(String[] args)
{
ThreadLocal<SimpleDateFormat> threadLocal = new ThreadLocal<>();
ExecutorService es = Executors.newFixedThreadPool(100);
for (int i = 0; i < 1000; i++)
{
es.execute(()->{
try
{
if(threadLocal.get() == null)
threadLocal.set(new SimpleDateFormat("yyyy-MM-dd hh:mm:ss"));
Date date = threadLocal.get().parse("2019-11-13 08:23:" + new Random().nextInt(60));
} catch (ParseException e)
{
e.printStackTrace();
}
});
}
es.shutdown();
}
ThreadLocal 可以確保每個線程都可以得到單獨的一個 SimpleDateFormat 的對象,那么自然也就不存在競爭問題了。
除了常用的SimpleDateFormat,我們還可以在Spring框架中找到ThreadLocal的身影。
@RestController
public class TestController
{
@Autowired
private HttpServletRequest request;
@GetMapping("/hello")
public String hello()
{
String token = request.getHeader("token");
return token;
}
}
對于上面的代碼,細(xì)心的人可能會問,直接把request當(dāng)作一個成員變量注入,這樣所有請求將共享一個request對象,程序肯定會亂套啊。但是當(dāng)我們運行上述代碼的時候,我們會發(fā)現(xiàn)程序并沒有什么問題,這時為什么呢。原因很簡單,spring是一個非常成熟的框架,當(dāng)我們要注入一個HttpServletRequest對象作為一個成員變量時,它會以ThreadLocal的形式進(jìn)行注入,這樣每個請求的request對象都是不同的。
ThreadLocal原理分析
看了上面的介紹,或許有人不禁要問,ThreadLocal這么強大,那它是怎么實現(xiàn)的呢。其實ThreadLocal與JUC中其他類的最大不同點是,ThreadLocal本身不存儲數(shù)據(jù),它更像一個工具類,負(fù)責(zé)變量的維護(hù)與獲取,就像java.utilCollections類,它本身并不存儲任何數(shù)據(jù)結(jié)構(gòu),但是可以完成許多數(shù)據(jù)結(jié)構(gòu)的操作。當(dāng)我們對ThreadLocal對象進(jìn)行set操作時,ThreadLocal并沒有把那些對象保存在自己這里,而是保存在了調(diào)用該方法的Thread對象里。

Thread類內(nèi)部有許多成員變量,其中182行聲明的
ThreadLocal.ThreadLocalMap對象就是用來保存Threadlocal執(zhí)行set方法時的對象這樣,當(dāng)調(diào)用get方法時,ThreadLocal會去調(diào)用該方法的Thread對象里去取之前set的value并返回。看上去一切事那么的完美,可是當(dāng)一個線程有多個ThreadLocal對象來進(jìn)行g(shù)et操作時,我們要怎么才能獲取到該ThreadLocal對應(yīng)的值呢?這還不簡單嘛,直接用一個map取維護(hù)ThreadLocal和value的映射不就行了。對,java里就是這么做的,只不過這個map跟我們平常所見的HashMap、TreeMap不太一樣,是一個被稱為ThreadLocalMap的Map,這個類被定義為ThreadLocal的一個內(nèi)部靜態(tài)類,我們可以把它當(dāng)成一個HashMap來看待(如果仔細(xì)閱讀其源碼我們會發(fā)現(xiàn)其處理Hash沖突所采用的是線性探測法)

其三者UML關(guān)系如圖所示,其中Entry對象代表了ThreadLocalMap里的一個鍵值對。
ThreadLocal的get方法源碼如下
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();
}
首先第二行獲取執(zhí)行該方法的當(dāng)前線程,然后第三行調(diào)用getMap方法來獲取該線程對應(yīng)的ThreadLocalMap,其方法聲明如下
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
獲取到了Map之后首先進(jìn)行判空處理,我們知道一個Map實際上是有許多Entry聚合而成的,而這些Entry保存的是所有的鍵值對(鍵為ThreadLocal,值為指定的泛型)信息。我們現(xiàn)在已經(jīng)獲取到了Map和鍵(當(dāng)前ThreadLocal),我們要獲取對應(yīng)的值,需要先去在該Map中根據(jù)該鍵去查找對應(yīng)的鍵值對,然后從這個鍵值對里獲取value。而map.getEntry(this)所做的就是去從這個當(dāng)前線程對應(yīng)的Map中去查找鍵位該ThreadLocal的鍵值對。
ThreadLocal內(nèi)存泄漏問題
我們前面說過,ThreadLocalMap雖然可以當(dāng)作一個Map來使用,但是其和一般的Map還是有一定的差別的。在這里最重要的一點就是其鍵值對對象Entry的聲明
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
我們發(fā)現(xiàn)其繼承了一個WeakReference的對象。那么這個WeakReference對象是個什么鬼呢,要講這個就必須要牽涉到j(luò)vm的垃圾回收了?,F(xiàn)代jvm采用的垃圾回收方法一般都是可達(dá)性分析,而一個對象是否可達(dá)則取決于是否存在一條從GCRoot到當(dāng)前對象的引用鏈。java虛擬機規(guī)范里規(guī)定了四種引用類型,分別是強引用,軟引用,弱引用和虛引用。其中弱引用也就是WeakReference,每次垃圾回收時,如果發(fā)現(xiàn)有弱引用對象,就將其回收。
我們看到Entry繼承自WeakReference,并指定泛型為ThreadLocal,在構(gòu)造函數(shù)時調(diào)用了super(k);,這表明只要這個ThreadLocal失去了其他的強引用,該Entry就會被回收。

如圖,此時Entry對象不會被回收,雖然ThreadLocal對象和Entry之間是弱引用,但ThreadLocal引用和ThreadLocal是強引用。當(dāng)代碼執(zhí)行出ThreadLocal的作用域時,在棧上的ThreadLocal引用會被清除,此時在堆上的ThreadLocal對象只有一個Entry對象的引用,由于此引用是弱引用,所以在下一次垃圾回收來臨時,該ThreadLocal對象會被垃圾回收器回收。我們在不難發(fā)現(xiàn),ThreadLocal的Entry之所以設(shè)計成一個弱引用的對象,就是為了防止ThreadLocal對象內(nèi)存泄露。雖然解決了ThreadLocal對象的內(nèi)存泄漏,但是會產(chǎn)生一個新的問題,那就是value對象的內(nèi)存泄露。當(dāng)ThreadLocal被回收后,ThreadLocalMap中就會出現(xiàn)key為null的Entry,就沒有辦法訪問這些key為null的Entry的value,如果當(dāng)前線程再遲遲不結(jié)束的話,這些key為null的Entry的value就會一直存在一條強引用鏈:Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value永遠(yuǎn)無法回收,造成內(nèi)存泄漏
其實,ThreadLocalMap的設(shè)計中已經(jīng)考慮到這種情況,也加上了一些防護(hù)措施:在ThreadLocal的get(),set(),remove()的時候都會清除線程ThreadLocalMap里所有key為null的value。
但是這些被動的預(yù)防措施并不能保證不會內(nèi)存泄漏:
- 使用static的ThreadLocal,延長了ThreadLocal的生命周期,可能導(dǎo)致的內(nèi)存泄漏
- 分配使用了ThreadLocal又不再調(diào)用get(),set(),remove()方法,那么就會導(dǎo)致內(nèi)存泄漏。
綜合上面的分析,我們可以理解ThreadLocal內(nèi)存泄漏的前因后果,那么怎么避免內(nèi)存泄漏呢?
- 每次使用完ThreadLocal,都調(diào)用它的remove()方法,清除數(shù)據(jù)。
- 在使用線程池的情況下,沒有及時清理ThreadLocal,不僅是內(nèi)存泄漏的問題,更嚴(yán)重的是可能導(dǎo)致業(yè)務(wù)邏輯出現(xiàn)問題。所以,使用ThreadLocal就跟加鎖完要解鎖一樣,用完就清理。