1. 概念
ThreadLocal 用于提供線程局部變量挪丢,在多線程環(huán)境可以保證各個(gè)線程里的變量獨(dú)立于其它線程里的變量。也就是說 ThreadLocal 可以為每個(gè)線程創(chuàng)建一個(gè)【單獨(dú)的變量副本】,相當(dāng)于線程的 private static 類型變量。
ThreadLocal 的作用和同步機(jī)制有些相反:同步機(jī)制是為了保證多線程環(huán)境下數(shù)據(jù)的一致性;而 ThreadLocal 是保證了多線程環(huán)境下數(shù)據(jù)的獨(dú)立性。
2. 使用示例
public class ThreadLocalTest {
private static String strLabel;
private static ThreadLocal<String> threadLabel = new ThreadLocal<>();
public static void main(String... args) {
strLabel = "main";
threadLabel.set("main");
Thread thread = new Thread() {
@Override
public void run() {
super.run();
strLabel = "child";
threadLabel.set("child");
}
};
thread.start();
try {
// 保證線程執(zhí)行完畢
thread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("strLabel = " + strLabel);
System.out.println("threadLabel = " + threadLabel.get());
}
}
運(yùn)行結(jié)果:
strLabel = child
threadLabel = main
從運(yùn)行結(jié)果可以看出唧席,對于 ThreadLocal 類型的變量,在一個(gè)線程中設(shè)置值嘲驾,不影響其在其它線程中的值袱吆。也就是說 ThreadLocal 類型的變量的值在每個(gè)線程中是獨(dú)立的。
3. ThreadLocal 實(shí)現(xiàn)
ThreadLocal 是怎樣保證其值在各個(gè)線程中是獨(dú)立的呢距淫?下面分析下 ThreadLocal 的實(shí)現(xiàn)绞绒。
ThreadLocal 是構(gòu)造函數(shù)只是一個(gè)簡單的無參構(gòu)造函數(shù),并且沒有任何實(shí)現(xiàn)榕暇。
3.1 set(T value) 方法
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
set(T value) 方法中蓬衡,首先獲取當(dāng)前線程,然后在獲取到當(dāng)前線程的 ThreadLocalMap彤枢,如果 ThreadLocalMap 不為 null狰晚,則將 value 保存到 ThreadLocalMap 中,并用當(dāng)前 ThreadLocal 作為 key缴啡;否則創(chuàng)建一個(gè) ThreadLocalMap 并給到當(dāng)前線程壁晒,然后保存 value。
ThreadLocalMap 相當(dāng)于一個(gè) HashMap业栅,是真正保存值的地方秒咐。
3.2 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();
}
同樣的,在 get() 方法中也會(huì)獲取到當(dāng)前線程的 ThreadLocalMap碘裕,如果 ThreadLocalMap 不為 null携取,則把獲取 key 為當(dāng)前 ThreadLocal 的值;否則調(diào)用 setInitialValue() 方法返回初始值帮孔,并保存到新創(chuàng)建的 ThreadLocalMap 中雷滋。
3.3 initialValue() 方法:
private T setInitialValue() {
T value = initialValue();
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
return value;
}
...
initialValue() 是 ThreadLocal 的初始值,默認(rèn)返回 null,子類可以重寫改方法晤斩,用于設(shè)置 ThreadLocal 的初始值焕檬。
3.4 remove() 方法
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
ThreadLocal 還有一個(gè) remove() 方法,用來移除當(dāng)前 ThreadLocal 對應(yīng)的值澳泵。同樣也是同過當(dāng)前線程的 ThreadLocalMap 來移除相應(yīng)的值揩页。
3.5 當(dāng)前線程的 ThreadLocalMap
在 set,get烹俗,initialValue 和 remove 方法中都會(huì)獲取到當(dāng)前線程,然后通過當(dāng)前線程獲取到 ThreadLocalMap萍程,如果 ThreadLocalMap 為 null幢妄,則會(huì)創(chuàng)建一個(gè) ThreadLocalMap,并給到當(dāng)前線程茫负。
...
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
...
可以看到蕉鸳,每一個(gè)線程都會(huì)持有有一個(gè) ThreadLocalMap,用來維護(hù)線程本地的值:
public class Thread implements Runnable {
...
ThreadLocal.ThreadLocalMap threadLocals = null;
...
}
在使用 ThreadLocal 類型變量進(jìn)行相關(guān)操作時(shí)忍法,都會(huì)通過當(dāng)前線程獲取到 ThreadLocalMap 來完成操作潮尝。每個(gè)線程的 ThreadLocalMap 是屬于線程自己的,ThreadLocalMap 中維護(hù)的值也是屬于線程自己的饿序。這就保證了 ThreadLocal 類型的變量在每個(gè)線程中是獨(dú)立的勉失,在多線程環(huán)境下不會(huì)相互影響。
4. ThreadLocalMap
4.1 構(gòu)造方法
ThreadLocal 中當(dāng)前線程的 ThreadLocalMap 為 null 時(shí)會(huì)使用 ThreadLocalMap 的構(gòu)造方法新建一個(gè) ThreadLocalMap:
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);
}
構(gòu)造方法中會(huì)新建一個(gè)數(shù)組原探,并將將第一次需要保存的鍵值存儲(chǔ)到一個(gè)數(shù)組中乱凿,完成一些初始化工作。
4.2 存儲(chǔ)結(jié)構(gòu)
ThreadLocalMap 內(nèi)部維護(hù)了一個(gè)哈希表(數(shù)組)來存儲(chǔ)數(shù)據(jù)咽弦,并且定義了加載因子:
// 初始容量徒蟆,必須是 2 的冪
private static final int INITIAL_CAPACITY = 16;
// 存儲(chǔ)數(shù)據(jù)的哈希表
private Entry[] table;
// table 中已存儲(chǔ)的條目數(shù)
private int size = 0;
// 表示一個(gè)閾值,當(dāng) table 中存儲(chǔ)的對象達(dá)到該值時(shí)就會(huì)擴(kuò)容
private int threshold;
// 設(shè)置 threshold 的值
private void setThreshold(int len) {
threshold = len * 2 / 3;
}
table 是一個(gè) Entry 類型的數(shù)組型型,Entry 是 ThreadLocalMap 的一個(gè)內(nèi)部類段审。
4.3 存儲(chǔ)對象 Entry
Entry 用于保存一個(gè)鍵值對闹蒜,其中 key 以弱引用的方式保存:
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
4.4 保存鍵值對
調(diào)用 set(ThreadLocal key, Object value) 方法將數(shù)據(jù)保存到哈希表中:
private void set(ThreadLocal key, Object value) {
Entry[] tab = table;
int len = tab.length;
// 計(jì)算要存儲(chǔ)的索引位置
int i = key.threadLocalHashCode & (len-1);
// 循環(huán)判斷要存放的索引位置是否已經(jīng)存在 Entry寺枉,若存在,進(jìn)入循環(huán)體
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
ThreadLocal k = e.get();
// 若索引位置的 Entry 的 key 和要保存的 key 相等绷落,則更新該 Entry 的值
if (k == key) {
e.value = value;
return;
}
// 若索引位置的 Entry 的 key 為 null(key 已經(jīng)被回收了)型凳,表示該位置的 Entry 已經(jīng)無效,用要保存的鍵值替換該位置上的 Entry
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
// 要存放的索引位置沒有 Entry嘱函,將當(dāng)前鍵值作為一個(gè) Entry 保存在該位置
tab[i] = new Entry(key, value);
// 增加 table 存儲(chǔ)的條目數(shù)
int sz = ++size;
// 清除一些無效的條目并判斷 table 中的條目數(shù)是否已經(jīng)超出閾值
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash(); // 調(diào)整 table 的容量甘畅,并重新擺放 table 中的 Entry
}
首先使用 key(當(dāng)前 ThreadLocal)的 threadLocalHashCode 來計(jì)算要存儲(chǔ)的索引位置 i。threadLocalHashCode 的值由 ThreadLocal 類管理,每創(chuàng)建一個(gè) ThreadLocal 對象都會(huì)自動(dòng)生成一個(gè)相應(yīng)的 threadLocalHashCode 值疏唾,其實(shí)現(xiàn)如下:
// ThreadLocal 對象的 HashCode
private final int threadLocalHashCode = nextHashCode();
// 使用 AtomicInteger 保證多線程環(huán)境下的同步
private static AtomicInteger nextHashCode =
new AtomicInteger();
// 每次創(chuàng)建 ThreadLocal 對象是 HashCode 的增量
private static final int HASH_INCREMENT = 0x61c88647;
// 計(jì)算 ThreadLocal 對象的 HashCode
private static int nextHashCode() {
return nextHashCode.getAndAdd(HASH_INCREMENT);
}
在保存數(shù)據(jù)時(shí)蓄氧,如果索引位置有 Entry,且該 Entry 的 key 為 null槐脏,那么就會(huì)執(zhí)行清除無效 Entry 的操作喉童,因?yàn)?Entry 的 key 使用的是弱引用的方式,key 如果被回收(即 key 為 null)顿天,這時(shí)就無法再訪問到 key 對應(yīng)的 value堂氯,需要把這樣的無效 Entry 清除掉來騰出空間。
在調(diào)整 table 容量時(shí)牌废,也會(huì)先清除無效對象咽白,然后再根據(jù)需要擴(kuò)容。
private void rehash() {
// 先清除無效 Entry
expungeStaleEntries();
// 判斷當(dāng)前 table 中的條目數(shù)是否超出了閾值的 3/4
if (size >= threshold - threshold / 4)
resize();
}
清除無用對象和擴(kuò)容的方法這里就不再展開說明了鸟缕。
4.5 獲取 Entry 對象
取值是直接獲取到 Entry 對象晶框,使用 getEntry(ThreadLocal key) 方法:
private Entry getEntry(ThreadLocal key) {
// 使用指定的 key 的 HashCode 計(jì)算索引位置
int i = key.threadLocalHashCode & (table.length - 1);
// 獲取當(dāng)前位置的 Entry
Entry e = table[i];
// 如果 Entry 不為 null 且 Entry 的 key 和 指定的 key 相等,則返回該 Entry
// 否則調(diào)用 getEntryAfterMiss(ThreadLocal key, int i, Entry e) 方法
if (e != null && e.get() == key)
return e;
else
return getEntryAfterMiss(key, i, e);
}
因?yàn)榭赡艽嬖诠_突懂从,key 對應(yīng)的 Entry 的存儲(chǔ)位置可能不在通過 key 計(jì)算出的索引位置上授段,也就是說索引位置上的 Entry 不一定是 key 對應(yīng)的 Entry。所以需要調(diào)用 getEntryAfterMiss(ThreadLocal key, int i, Entry e) 方法獲取番甩。
private Entry getEntryAfterMiss(ThreadLocal key, int i, Entry e) {
Entry[] tab = table;
int len = tab.length;
// 索引位置上的 Entry 不為 null 進(jìn)入循環(huán)侵贵,為 null 則返回 null
while (e != null) {
ThreadLocal k = e.get();
// 如果 Entry 的 key 和指定的 key 相等,則返回該 Entry
if (k == key)
return e;
// 如果 Entry 的 key 為 null (key 已經(jīng)被回收了)缘薛,清除無效的 Entry
// 否則獲取下一個(gè)位置的 Entry模燥,循環(huán)判斷
if (k == null)
expungeStaleEntry(i);
else
i = nextIndex(i, len);
e = tab[i];
}
return null;
}
4.6 移除指定的 Entry
private void remove(ThreadLocal key) {
Entry[] tab = table;
int len = tab.length;
// 使用指定的 key 的 HashCode 計(jì)算索引位置
int i = key.threadLocalHashCode & (len-1);
// 循環(huán)判斷索引位置的 Entry 是否為 null
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
// 若 Entry 的 key 和指定的 key 相等,執(zhí)行刪除操作
if (e.get() == key) {
// 清除 Entry 的 key 的引用
e.clear();
// 清除無效的 Entry
expungeStaleEntry(i);
return;
}
}
}
4.7 內(nèi)存泄漏
在 ThreadLocalMap 的 set()掩宜,get() 和 remove() 方法中蔫骂,都有清除無效 Entry 的操作,這樣做是為了降低內(nèi)存泄漏發(fā)生的可能牺汤。
Entry 中的 key 使用了弱引用的方式辽旋,這樣做是為了降低內(nèi)存泄漏發(fā)生的概率,但不能完全避免內(nèi)存泄漏檐迟。
這句話的意思好象是矛盾的补胚,下面來分析一下。
假設(shè) Entry 的 key 沒有使用弱引用的方式追迟,而是使用了強(qiáng)引用:由于 ThreadLocalMap 的生命周期和當(dāng)前線程一樣長溶其,那么當(dāng)引用 ThreadLocal 的對象被回收后,由于 ThreadLocalMap 還持有 ThreadLocal 和對應(yīng) value 的強(qiáng)引用敦间,ThreadLocal 和對應(yīng)的 value 是不會(huì)被回收的瓶逃,這就導(dǎo)致了內(nèi)存泄漏束铭。所以 Entry 以弱引用的方式避免了 ThreadLocal 沒有被回收而導(dǎo)致的內(nèi)存泄漏,但是此時(shí) value 仍然是無法回收的厢绝,依然會(huì)導(dǎo)致內(nèi)存泄漏契沫。
ThreadLocalMap 已經(jīng)考慮到這種情況,并且有一些防護(hù)措施:在調(diào)用 ThreadLocal 的 get()昔汉,set() 和 remove() 的時(shí)候都會(huì)清除當(dāng)前線程 ThreadLocalMap 中所有 key 為 null 的 value懈万。這樣可以降低內(nèi)存泄漏發(fā)生的概率。所以我們在使用 ThreadLocal 的時(shí)候靶病,每次用完 ThreadLocal 都調(diào)用 remove() 方法会通,清除數(shù)據(jù),防止內(nèi)存泄漏娄周。