ThreadLocal 是什么
首先 它是一個(gè)數(shù)據(jù)結(jié)構(gòu) 類似HashMap
可以保存 Key Value 鍵值對 但是ThreadLocal只能保存一個(gè) 并且每個(gè)線程互不干擾
public static void main(String[] args) {
final ThreadLocal<String> localName = new ThreadLocal();
final HashMap<Integer, String> map = new HashMap<>(2);
new Thread("線程1") {
@Override
public void run() {
localName.set("Sincerity");
String s = localName.get();
System.out.println(Thread.currentThread().getName() + "獲取到ThreadLocal值=" + s);
map.put(0, Thread.currentThread().getName());
System.out.println(Thread.currentThread().getName() + "獲取到map的長度" + map.size());
}
}.start();
String s = localName.get();
System.out.println("主線程獲取到ThreadLocal值=" + s);
new Thread("線程2") {
@Override
public void run() {
String s = localName.get();
System.out.println(Thread.currentThread().getName() + "獲取到ThreadLocal值=" + s);
System.out.println(Thread.currentThread().getName() + "獲取到map的長度" + map.size());
}
}.start();
//得到結(jié)果
主線程獲取到ThreadLocal值=null
線程1獲取到ThreadLocal值=Sincerity
線程1獲取到map的長度1
線程2獲取到ThreadLocal值=null
線程2獲取到map的長度1
思考一下為什么會(huì)出現(xiàn)這樣的情況呢 我們已經(jīng)知道ThreadLocal
是一種數(shù)據(jù)結(jié)構(gòu) 為什么除了賦值的線程之外數(shù)據(jù)無法獲取呢 同樣是HashMap
為什么可以可以全局獲取到數(shù)據(jù)呢 帶著問題 我們一起探索一下
為何ThreadLocal
能實(shí)現(xiàn)每個(gè)線程的數(shù)據(jù)互不干擾
讀懂源碼
public class ThreadLocal<T> {
...
//說明創(chuàng)建ThreadLocal的時(shí)候什么也沒有做
public ThreadLocal() {
}
...
//set方法怎么說
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t); //默認(rèn)情況下為null
if (map != null)
//set的時(shí)候 把自己當(dāng)做Key 傳遞的值當(dāng)做Value
map.set(this, value);
else
createMap(t, value); //創(chuàng)建一個(gè)map對象
}
...
//獲取線程中保留的 ThreadLocal的映射 默認(rèn)在Thread中為空
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
//創(chuàng)建一個(gè)ThreadLocalMap
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
//get方法
public T get() {
Thread t = Thread.currentThread();
//得到當(dāng)前線程的ThreadLocalMap映射
ThreadLocalMap map = getMap(t);
if (map != null) {
//拿到key等于當(dāng)前ThreadLocal的Entry
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
//處理map等于null的情況
return setInitialValue();
}
/**
*主要就是將一個(gè)null重新存入map中 并且返回null
*/
private T setInitialValue() {
T value = initialValue();//得到一個(gè)Null值
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
return value;
}
protected T initialValue() {
return null;
}
}
看到這里其實(shí)我們也就明白 ThreadLocal
為什么能保證每個(gè)線程數(shù)據(jù)獨(dú)立了 其內(nèi)部維護(hù)著一個(gè)當(dāng)前線程的映射ThreadLocalMap
然后通過線程映射得到當(dāng)前線程的ThreadLocalMap
這里就出現(xiàn)了一個(gè)問題 同一個(gè)ThreadLocal的Hashcode是一致的 怎么保證每個(gè)線程的數(shù)據(jù)獨(dú)立呢
看看ThreadLocalMap
static class ThreadLocalMap {
//數(shù)組中的桶 弱引用
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
private static final int INITIAL_CAPACITY = 16;
private Entry[] table;
//得到key的hashCode
private final int threadLocalHashCode = nextHashCode();
//生成hash code間隙為這個(gè)魔數(shù)阶剑,
//可以讓生成出來的值或者說ThreadLocal的ID較為均勻地分布在2的冪大小的數(shù)組中摔蓝。
private static final int HASH_INCREMENT = 0x61c88647;
private static int nextHashCode() {
return nextHashCode.getAndAdd(HASH_INCREMENT);
}
//構(gòu)造方法 默認(rèn)添加一個(gè)值
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
//創(chuàng)建一個(gè)默認(rèn)大小為16的數(shù)組
table = new Entry[INITIAL_CAPACITY];
//用firstKey的threadLocalHashCode與初始大小16取模得到哈希值
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
table[i] = new Entry(firstKey, firstValue);
size = 1;
//設(shè)置閾值
setThreshold(INITIAL_CAPACITY);
}
private void setThreshold(int len) {
threshold = len * 2 / 3; //直接寫成2/3了 ....
}
//向ThreadLocalMap中添加元素
private void set(ThreadLocal<?> key, Object value) {
Entry[] tab = table;
int len = tab.length;
//得到key的hashCode 線性探測法得到
//每個(gè)ThreadLocal對象都有一個(gè)hash值threadLocalHashCode财剖,每初始化一個(gè)ThreadLocal對象奠货,
//hash值就增加一個(gè)固定的大小0x61c88647
int i = key.threadLocalHashCode & (len-1);
//根據(jù)ThreadLocal大小的hash值得到table中的i的元素
for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) {
//如果I位置已經(jīng)有一個(gè)Entry對象 說明hash沖突了
//得到當(dāng)前存儲(chǔ)元素的key
ThreadLocal<?> k = e.get();
//如果這個(gè)元素額key正好是設(shè)置的key 重新給元素中的value賦值
if (k == key) {
e.value = value;
return;
}
// 當(dāng)前i位置entry對象為空
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
//如果當(dāng)前key的hashCode位置為空 插入一個(gè)enrty在i位置
tab[i] = new Entry(key, value);
int sz = ++size;
//清理一個(gè)沒用的數(shù)據(jù) 后大小達(dá)到閾值
if (!cleanSomeSlots(i, sz) && sz >= threshold)
//擴(kuò)容
rehash(); //2倍擴(kuò)容
}
}
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
可以看出偶器,它是在上一個(gè)被構(gòu)造出的ThreadLocal的ID/threadLocalHashCode的基礎(chǔ)上加上一個(gè)魔數(shù)0x61c88647的搀矫。這個(gè)魔數(shù)的選取與斐波那契散列有關(guān)官觅,0x61c88647對應(yīng)的十進(jìn)制為1640531527刑峡。斐波那契散列的乘數(shù)可以用(long) ((1L << 31) * (Math.sqrt(5) - 1))可以得到2654435769洋闽,如果把這個(gè)值給轉(zhuǎn)為帶符號(hào)的int玄柠,則會(huì)得到-1640531527。換句話說
(1L << 32) - (long) ((1L << 31) * (Math.sqrt(5) - 1))
得到的結(jié)果就是1640531527也就是0x61c88647诫舅。通過理論與實(shí)踐羽利,當(dāng)我們用0x61c88647作為魔數(shù)累加為每個(gè)ThreadLocal分配各自的ID也就是threadLocalHashCode再與2的冪取模,得到的結(jié)果分布很均勻刊懈。
ThreadLocalMap使用的是線性探測法这弧,均勻分布的好處在于很快就能探測到下一個(gè)臨近的可用slot,從而保證效率虚汛。這就回答了上文拋出的為什么大小要為2的冪的問題匾浪。為了優(yōu)化效率。對于
& (INITIAL_CAPACITY - 1)
卷哩,相信有過算法競賽經(jīng)驗(yàn)或是閱讀源碼較多的程序員蛋辈,一看就明白,對于2的冪作為模數(shù)取模殉疼,可以用&(2n-1)來替代%2n梯浪,位運(yùn)算比取模效率高很多。至于為什么瓢娜,因?yàn)閷?^n取模,只要不是低n位對結(jié)果的貢獻(xiàn)顯然都是0句喷,會(huì)影響結(jié)果的只能是低n位悉稠。可以說在ThreadLocalMap中喻频,形如
key.threadLocalHashCode & (table.length - 1)
(其中key為一個(gè)ThreadLocal實(shí)例)這樣的代碼片段實(shí)質(zhì)上就是在求一個(gè)ThreadLocal實(shí)例的哈希值,只是在源碼實(shí)現(xiàn)中沒有將其抽為一個(gè)公用函數(shù)褒颈。
內(nèi)存泄漏
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
通過之前的分析已經(jīng)知道,當(dāng)使用ThreadLocal保存一個(gè)value時(shí)励堡,會(huì)在ThreadLocalMap中的數(shù)組插入一個(gè)Entry對象谷丸,按理說key-value都應(yīng)該以強(qiáng)引用保存在Entry對象中,但在ThreadLocalMap的實(shí)現(xiàn)中应结,key被保存到了WeakReference對象中刨疼。
這就導(dǎo)致了一個(gè)問題,ThreadLocal在沒有外部強(qiáng)引用時(shí)鹅龄,發(fā)生GC時(shí)會(huì)被回收揩慕,如果創(chuàng)建ThreadLocal的線程一直持續(xù)運(yùn)行,那么這個(gè)Entry對象中的value就有可能一直得不到回收扮休,發(fā)生內(nèi)存泄露迎卤。
如何避免內(nèi)存泄露
既然已經(jīng)發(fā)現(xiàn)有內(nèi)存泄露的隱患,自然有應(yīng)對的策略玷坠,在調(diào)用ThreadLocal的get()蜗搔、set()可能會(huì)清除ThreadLocalMap中key為null的Entry對象劲藐,這樣對應(yīng)的value就沒有GC Roots可達(dá)了,下次GC的時(shí)候就可以被回收樟凄,當(dāng)然如果調(diào)用remove方法瘩燥,肯定會(huì)刪除對應(yīng)的Entry對象。
如果使用ThreadLocal的set方法之后不同,沒有顯示的調(diào)用remove方法厉膀,就有可能發(fā)生內(nèi)存泄露,所以養(yǎng)成良好的編程習(xí)慣十分重要二拐,使用完ThreadLocal之后服鹅,記得調(diào)用remove方法。
ThreadLocal<String> localName = new ThreadLocal();
try {
localName.set("Sincerity");
} finally {
localName.remove();
}