本篇文章旨在將ThreadLocal的原理說清楚,講明白。全文主要完成了如下四個部分的工做:html
首先,讓咱們看看ThreadLocal能產生什麼樣的效果:java
public class ThreadLocalDemo {
public static void main(String[] args) {
final ThreadLocal<Integer> local = new ThreadLocal<>();
local.set(100);
Thread t = new Thread(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " local: " + local.get());
}
});
t.start();
System.out.println("Main local: " + local.get());
}
}
複製代碼
打印結果以下:c++
Thread-0 local: null
Main local: 100
複製代碼
local在主線程set的值,能夠在主線程調用get方法獲得,但在線程t內調用get方法,結果結果爲null。算法
本文接下來以local調用的set方法爲入口,探究產生這一結果的緣由。小程序
在ThreadLocal源碼中set()是這樣實現的:bash
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
複製代碼
首先得到當前執行local.set()語句所在的線程對象,也就是t,而後經過local的getMap()得到t內部持有的ThreadLocalMap對象,進入Thread類的源碼查看,其中就包含名爲threadLocals的字段:數據結構
ThreadLocal.ThreadLocalMap threadLocals = null;
複製代碼
而查看getMap()的源碼,返回的就是threadLocals:jvm
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
複製代碼
若是map != null,則執行map.set(this, value),這裏的this就是local。ide
ThreadLocalMap的具體實現後面再展開,在這裏姑且先簡單的理解爲按鍵值對存儲數據的數據結構,那麼咱們很容易發現,local仍是那個local,並無在每一個線程產生local副本,只不過調用set方法的時候,將它與傳入的值以鍵值對的形式,存儲於每一個線程內部持有的ThreadLocalMap對象裏。性能
若是map == null,則執行createMap(t, value),源碼以下:
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
複製代碼
建立ThreadLocalMap對象賦給threadLocals。
至此,ThreadLocal的基本原理就已經很清晰了:各線程對共享的ThreadLocal實例進行操做,其實是以該實例爲鍵對內部持有的ThreadLocalMap對象進行操做。
除了set(),ThreadLocal還提供了get()、remove()等操做,實現比較簡單,就不敷述了。
要想真正理解ThreadLocal,還須要知道ThreadLocalMap到底是什麼。
註釋中是這樣介紹的:ThreadLocalMap is a customized hash map suitable only for maintaining thread local values.
ThreadLocalMap屬於自定義的map,是一個帶有hash功能的靜態內部類,和java.util包下提供的Map類並無關係。內部有一個靜態的Entry類,下面具體分析Entry。
首先,這個類代碼以下:
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
複製代碼
這裏引用代碼中給出的註釋: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。
第一句話實際上告訴了咱們,entry繼承自WeakReference,用main方法引用的字段做爲entry中的key。
第二句的意思是,當entry.get() == null的時候,意味着鍵將再也不被引用。
後續將解析這兩句註釋。
在開始這一小結以前,須要先掌握兩點:
接下來,先閱讀源代碼,當構造器傳入參數後,表明鍵的k會傳入super()中,也就是它會首先執行父類的構造器:
public WeakReference(T referent) {
super(referent);
}
複製代碼
WeakReference的構造器繼續先調用父類的構造器:
Reference(T referent) {
this(referent, null);
}
Reference(T referent, ReferenceQueue<? super T> queue) {
this.referent = referent;
this.queue = (queue == null) ? ReferenceQueue.NULL : queue;
}
複製代碼
除此以外,咱們在Reference類裏面看不到任何native方法,但能看到一些實例方法,好比get(),後續咱們還將談到這個方法。
這個時候會疑惑弱引用的功能是怎麼實現的,在註釋中,有這樣的字眼:「special treatment by the garbage collector.」 可見WeakReference的功能實現交給了垃圾回收器處理,那麼這裏就不展開了,感興趣的能夠參考文末的連接。在這裏咱們只須要了解WeakReference的使用方法。
弱引用和強引用的使用方法並不相同,下面是一個弱引用的示例:
public class WeakReferenceDemo {
public static void main(String[] args) {
WeakReference<Fruit> fruitWeakReference = new WeakReference<>(new Fruit());
// Fruit f = fruitWeakReference.get();
if (fruitWeakReference.get() != null) {
System.out.println("Before GC, this is the result");
}
System.gc();
if (fruitWeakReference.get() != null) {
System.out.println("After GC, fruitWeakReference.get() is not null");
} else {
System.out.println("After GC, fruitWeakReference.get() is null");
}
}
}
class Fruit {
}
複製代碼
輸出結果以下:
Before GC, this is the result
After GC, fruitWeakReference.get() is null
複製代碼
經過fruitWeakReference.get(),能夠獲得弱引用指向的對象,當執行System.gc()後,該對象被回收。
用一張圖表示強弱引用彼此間的關係:
要明確的是,相似「Object obj = new Object()」這般產生的引用屬於強引用,因此fruitWeakReference是強引用,此時它指向的是一個WeakReference對象,在new這個對象時,咱們還傳入了一個new出來的Fruit對象,整行代碼的目的,就是要創造一個弱引用,指向這個Fruit對象。而這個弱引用,就在fruitWeakReference指向的對象裏。
用個不嚴謹的比喻,弱引用就像一隻薛定諤的貓,咱們想知道它的狀態,卻不能經過普通的Java代碼調用出它自己來觀測它,若是將前文列出的WeakReferenceDemo內的雙斜槓註釋去掉,用一個變量f指向fruitWeakReference.get(),不過就是將一個強引用指向了本來由弱引用指向的對象而已,此時再運行程序,獲得以下結果:
Before GC, this is the result
After GC, fruitWeakReference.get() is not null
Process finished with exit code 0
複製代碼
因爲對象被強引用,因此不會被垃圾回收。
有了前面的基礎,很容易就能理解Entry的構造原理。爲了方便說明,不妨假設咱們能建立一個Entry對象,代碼以下:
Entry entry = new Entry(local, 100);
複製代碼
此時強弱引用彼此間的關係圖以下:
到這裏,就能理解前面那兩句註釋了,entry繼承自WeakReference,內部維護一個弱引用,指向main方法中local指向的對象;entry.get()返回的是弱引用指向的對象,若是entry.get() == null,天然表示的就是鍵將再也不被引用了。
因此,和普通Map的Entry類不一樣,ThreadLocalMap的Entry實例被建立是時,鍵是弱引用,至此ThreadLocal內部ThreadLocalMap的基本結構也就清楚了。
再次貼出ThreadLocal中set()的源碼:
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
複製代碼
注意第5行的語句,local調用set()時,一旦當前線程對象持有的ThreadLocalMap類型變量threadLocals不爲null,則會執行map.set(this, value)這一行語句,上一節分析了ThreadLocalMap的結構,這一節將聚焦ThreadLocalMap的操做方法set()。
下面給出set()的源碼:
private void set(ThreadLocal<?> key, Object value) {
Entry[] tab = table;
int len = tab.length;
// 計算出hash表的位置i
int i = key.threadLocalHashCode & (len-1);
// 處理set方法關鍵邏輯
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();
if (k == key) {
e.value = value;
return;
}
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
// 在hash表中保存新生成的Entry對象
tab[i] = new Entry(key, value);
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
複製代碼
代碼中i是hash表(亦稱hash桶)的索引,也就是存放新設置的entry的位置,固然在存放以前還要進行一番比較操做。threadLocalHashCode是以下方式獲得的:
private static AtomicInteger nextHashCode = new AtomicInteger();
private static final int HASH_INCREMENT = 0x61c88647;
private static int nextHashCode() {
return nextHashCode.getAndAdd(HASH_INCREMENT);
}
private final int threadLocalHashCode = nextHashCode();
複製代碼
採用0x61c88647是爲了實現更好的散列,每當有新的ThreadLocal對象調用threadLocalHashCode的時候,後者自增一個0x61c88647大小的值。至於爲何0x61c88647能夠實現更好的散列,這涉及到Fibonacci Hashing算法(這個數的二進制形式取反加1就是一個Fibonacci Hashing常數),具體細節可跳轉到文末參考連接。
固然,在計算i以前還要進行一個位運算,很是簡單,好比在沒擴展以前len是16(2的4次方),那麼len - 1的二進制形式就是1111,按位與也就是取後四位。
爲了防止碰撞衝突,這裏採用的是線性探測法,並無採用拉鍊法。探測的索引規則以下:
private static int nextIndex(int i, int len) {
return ((i + 1 < len) ? i + 1 : 0);
}
複製代碼
for循環的執行邏輯是這樣的:
在跳出循環並在hash表相應位置保存新生成的Entry對象後,size也會加1,在知足!cleanSomeSlots(i, sz) && sz >= threshold的條件下,還要從新進行rehash()處理。
replaceStaleEntry以及cleanSomeSlots的主要做用都是用來刪除弱引用爲null的entry,後者查找的時間是log2(n),限於篇幅就不展開了,而threshold和HashMap中定義的預置做用類似,主要是擴容用的,這裏爲len * 2 / 3。
仍是沿用最初的例子,若是將local置爲null,那麼new出來的ThreadLocal對象就只被線程中的ThreadLocalMap實例弱引用,此時只要調用System.gc(),對象將在下一次垃圾收集時被回收。若是要主動斷掉弱引用呢?Java提供了以下方法:
clear()
複製代碼
它是Reference抽象類提供的方法。
接下來用一個例子討論ThreadLocal可能出現的內存泄漏問題。
實例源碼以下:
public class ThreadLocalTest throws InterruptedException{
public static void main(String[] args) {
MyThreadLocal<Create50MB> local = new MyThreadLocal<>();
ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(5, 5, 1,
TimeUnit.MINUTES, new LinkedBlockingQueue<Runnable>());
for (int i = 0; i < 5; i++) {
final int[] a = new int[1];
final ThreadLocal[] finallocal = new MyThreadLocal[1];
finallocal[0] = local;
a[0] = i;
poolExecutor.execute(new Runnable() {
@Override
public void run() {
finallocal[0].set(new Create50MB());
System.out.println("add i = " + a[0]);
}
});
}
Thread.sleep(50000);
local = null;
}
static class Create50MB {
private byte[] bytes = new byte[1024 * 1024 * 50];
}
static class MyThreadLocal<T> extends ThreadLocal {
private byte[] bytes = new byte[1024 * 1024 * 500];
}
}
複製代碼
先說一說該小程序的設計思路:
該程序旨在構造出一種內存泄漏的狀況:當線程池執行完當前任務處於等待狀態的時候,將local置null,回收main方法一開始new出來的MyThreadLocal對象,線程池內單個線程的ThreadLocalMap實例雖然弱引用於這個MyThreadLocal對象,但內部持有的value卻仍然被強引用着不能回收。
在該程序中,咱們自定義了一個MyThreadLocal,目的是使new出來的MyThreadLocal對象的大小能達到500MB;Create50MB是建立出來的容量包,每一個線程最後持有的value就是一個50MB大小的Create50MB對象;線程池也是自定義傳參,作到更好的掌控,一次能同時工做5個線程;for循環中用到了兩個臨時變量,是爲了規避匿名內部類引用外部變量必需要聲明爲final的語言限制。
啓動程序,運行狀態見下圖:
使用的堆的大小是750MB,這符合預期,new出來的MyThreadLocal對象500MB,有五個線程,每一個線程50MB,加起來一共750MB。
50秒後,將local置null,這個時候再也不有強引用指向new的MyThreadLocal對象,此時執行垃圾回收,結果以下:
使用的堆大小變爲250MB,單就這個結果還不能證實每一個線程內對MyThreadLocal對象存在弱引用,可是必定不存在強引用。
以前本人曾研究過線程池的源碼,線程池內的線程在執行完一個任務後,並無銷燬,在本例中,它們處於waiting狀態,因此,本程序始終維持在250MB大小,得不到釋放,一旦將程序中的條件改得足夠大,就能出現明顯的性能問題。解決的方法一般是在線程內調用ThreadLocal的remove方法,實際上,ThreadLocal提供的公有API並很少,可是這個方法足夠解決問題。
不得不說,經過對ThreadLocal的解析,本人收穫不少。整篇文章寫起來也是一鼓作氣(因此可能也包藏着錯誤),估摸着若是之後有對共享變量進行私有設置的需求時,也能夠參考這種方法來寫;以前對四種引用只是瞭解,此次算是弄明白怎麼運用;用線性探測解決hash表的碰撞衝突,有別於HashMap,也是ThreadLocal的特色;最後列舉的內存泄漏,算是對前面寫的內容進行了一次實戰。
cool.
What is the meaning of 0x61C88647 constant in ThreadLocal.java
打印GC:-XX:+PrintGCDetails,更多可見:查看GC日誌時使用的虛擬機參數