ThreadLocal是開發中最常用的技術之一,也是面試重要的考點。本文將由淺入深,介紹ThreadLocal的使用方式、實現原理、記憶體洩漏問題以及使用場景。
ThreadLocal作用
在併發程式設計中時常有這樣一種需求:每條執行緒都需要存取一個同名變數,但每條執行緒中該變數的值均不相同。
如果是你,該如何實現上述功能?常規的思路如下:
使用一個執行緒共享的Map<Thread,Object>
,Map中的key為執行緒物件,value即為需要儲存的值。那麼,我們只需要通過map.get(Thread.currentThread())
即可獲取本執行緒中該變數的值。
這種方式確實可以實現我們的需求,但它有何缺點呢?——答案就是:需要同步,效率低!
由於這個map物件需要被所有執行緒共享,因此需要加鎖來保證執行緒安全性。當然我們可以使用java.util.concurrent.*
包下的ConcurrentHashMap
提高併發效率,但這種方法只能降低鎖的粒度,不能從根本上避免同步鎖。而JDK提供的ThreadLocal
就能很好地解決這一問題。下面來看看ThreadLocal是如何高效地實現這一需求的。
如何使用ThreadLocal
在介紹ThreadLocal原理之前,首先簡單介紹一下它的使用方法。
public class Main{
private ThreadLocal<Integer> threadLocal = new ThreadLocal<>();
public void start() {
for (int i=0; i<10; i++) {
new Thread(new Runnable(){
@override
public void run(){
threadLocal.set(i);
threadLocal.get();
threadLocal.remove();
}
}).start();
}
}
}
複製程式碼
- 首先我們需要建立一個執行緒共享的ThreadLocal物件,該物件用於儲存Integer型別的值;
- 然後在每條執行緒中可以通過如下方法操作ThreadLocal:
set(obj)
:向當前執行緒中儲存資料get()
:獲取當前執行緒中的資料remove()
:刪除當前執行緒中的資料
ThreadLocal的使用方法非常簡單,關鍵在於它背後的實現原理。回到上面的問題:ThreadLocal究竟是如何避免同步鎖,從而保證讀寫的高效?
ThreadLocal實現原理
ThreadLocal的內部結構如下圖所示:
ThreadLocal
並不維護ThreadLocalMap
,並不是一個儲存資料的容器,它只是相當於一個工具包,提供了操作該容器的方法,如get、set、remove等。而ThreadLocal
內部類ThreadLocalMap
才是儲存資料的容器,並且該容器由Thread
維護。
每一個Thread
物件均含有一個ThreadLocalMap
型別的成員變數threadLocals
,它儲存本執行緒中所有ThreadLocal物件及其對應的值。
ThreadLocalMap
由一個個Entry
物件構成,Entry
的程式碼如下:
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
複製程式碼
Entry
繼承自WeakReference<ThreadLocal<?>>
,一個Entry
由ThreadLocal
物件和Object
構成。由此可見,Entry
的key是ThreadLocal物件,並且是一個弱引用。當沒指向key的強引用後,該key就會被垃圾收集器回收。
那麼,ThreadLocal是如何工作的呢?下面來看set和get方法。
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;
}
複製程式碼
當執行set方法時,ThreadLocal首先會獲取當前執行緒物件,然後獲取當前執行緒的ThreadLocalMap物件。再以當前ThreadLocal物件為key,將值儲存進ThreadLocalMap物件中。
get方法執行過程類似。ThreadLocal首先會獲取當前執行緒物件,然後獲取當前執行緒的ThreadLocalMap物件。再以當前ThreadLocal物件為key,獲取對應的value。
由於每一條執行緒均含有各自私有的ThreadLocalMap容器,這些容器相互獨立互不影響,因此不會存線上程安全性問題,從而也無需使用同步機制來保證多條執行緒訪問容器的互斥性。
為何要使用弱引用?
對弱引用不了解的同學可以參考筆者的另一篇文章:http://blog.csdn.net/u010425776/article/details/50760053。
Java設計之初的一大宗旨就是——弱化指標。
Java設計者希望通過合理的設計簡化程式設計,讓程式設計師無需處理複雜的指標操作。然而指標是客觀存在的,在目前的Java開發中也不可避免涉及到“指標操作”。如:
Object a = new Object();
複製程式碼
上述程式碼建立了一個強引用a,只要強引用存在,垃圾收集器是不會回收該物件的。如果該物件非常龐大,那麼為了節約記憶體空間,在該物件使用完成後,我們需要手動拆除該強引用,如下面程式碼所示:
a = null;
複製程式碼
此時,指向該物件的強引用消除了,垃圾收集器便可以回收該物件。但在這個過程中,仍然需要程式設計師處理指標。為了弱化指標這一概念,弱引用便出現了,如下程式碼建立了一個Person型別的弱引用:
WeakReference<Person> wr = new WeakReference<Person>(new Person());
複製程式碼
此時程式設計師不用再關注指標,只要沒有強引用指向Person物件,垃圾收集器每次執行都會自動將該物件釋放。
那麼,ThreadLocalMap中的key使用弱引用的原因也是如此。當一條執行緒中的ThreadLocal物件使用完畢,沒有強引用指向它的時候,垃圾收集器就會自動回收這個Key,從而達到節約記憶體的目的。
那麼,問題又來了——這會導致記憶體洩漏問題!
ThreadLocal的記憶體洩漏問題
在ThreadLocalMap中,只有key是弱引用,value仍然是一個強引用。當某一條執行緒中的ThreadLocal使用完畢,沒有強引用指向它的時候,這個key指向的物件就會被垃圾收集器回收,從而這個key就變成了null;然而,此時value和value指向的物件之間仍然是強引用關係,只要這種關係不解除,value指向的物件永遠不會被垃圾收集器回收,從而導致記憶體洩漏!
不過不用擔心,ThreadLocal提供了這個問題的解決方案。
每次操作set、get、remove操作時,ThreadLocal都會將key為null的Entry刪除,從而避免記憶體洩漏。
那麼問題又來了,如果一個執行緒執行週期較長,而且將一個大物件放入LocalThreadMap後便不再呼叫set、get、remove方法,此時該仍然可能會導致記憶體洩漏。
這個問題確實存在,沒辦法通過ThreadLocal解決,而是需要程式設計師在完成ThreadLocal的使用後要養成手動呼叫remove的習慣,從而避免記憶體洩漏。
ThreadLocal的使用場景
Web系統Session的儲存就是ThreadLocal一個典型的應用場景。
Web容器採用執行緒隔離的多執行緒模型,也就是每一個請求都會對應一條執行緒,執行緒之間相互隔離,沒有共享資料。這樣能夠簡化程式設計模型,程式設計師可以用單執行緒的思維開發這種多執行緒應用。
當請求到來時,可以將當前Session資訊儲存在ThreadLocal中,在請求處理過程中可以隨時使用Session資訊,每個請求之間的Session資訊互不影響。當請求處理完成後通過remove方法將當前Session資訊清除即可。