一般有多個孩子的家庭,買玩具都得買多個。如果就買一個,嘿嘿就比較刺激了。這就是避免共享,給孩子每人一個玩具對應到我們Java中也就是每個執行緒都有自己的本地變數,我們們自己玩自己的,避免爭搶,和諧相處使得執行緒安全。
Java就是通過ThreadLocal
來實現執行緒本地儲存的。
這思路也很清晰,就是每個執行緒要有自己的本地變數唄,那就Thread裡面搞一個私有屬性唄ThreadLocal.ThreadLocalMap threadLocals = null;
就是如下圖所示的這個關係
ThreadLocal
簡單的應用如下
public class Demo {
private static final ThreadLocal<Foo> fooLocal = new ThreadLocal<Foo>();
public static Foo getFoo() {
return fooLocal.get();
}
public static void setFoo(Foo foo) {
fooLocal.set(foo);
}
}
複製程式碼
再深入瞭解一下內部情況,ThreadLocalMap
是ThreadLocal
的內部靜態類,它雖然叫Map
但是和java.util.Map
沒有啥親戚關係,只是它實現的功能像Map
static class ThreadLocalMap {
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
private static final int INITIAL_CAPACITY = 16;
private Entry[] table;
複製程式碼
可以看到ThreadLocalMap
裡面有個Entry
陣列,只有陣列沒有像HashMap
那樣有連結串列,因此當hash衝突的之後,ThreadLocalMap
是採用線性探測的方式解決hash衝突。
線性探測,就是先根據初始key
的hashcode
值確定元素在table
陣列中的位置,如果這個位置上已經有其他key
值的元素被佔用,則利用固定的演算法尋找一定步長的下個位置,依次直至找到能夠存放的位置。在ThreadLocalMap
步長是1。
用這種方式解決hash衝突的效率很低,因此要注意ThreadLocal的數量。
/**
* Increment i modulo len.
*/
private static int nextIndex(int i, int len) {
return ((i + 1 < len) ? i + 1 : 0);
}
/**
* Decrement i modulo len.
*/
private static int prevIndex(int i, int len) {
return ((i - 1 >= 0) ? i - 1 : len - 1);
}
複製程式碼
而且可以看到這個Entry
把ThreadLocal
的弱引用作為key。那為什麼要搞成弱引用(只要發生了GC弱引用物件就會被回收)呢?
首先ThreadLocal
內部沒有儲存任何的值,它的作用只是當我們的ThreadLocalMap的key
,讓執行緒可以拿到對應的value
。當我們不需要用這個key的時候我們,我們把fooLocal=null
這樣強引用就沒了。假設Entry裡面也是強引用的話,那等於這個ThreadLocal
例項還有個強引用在,那麼我們想讓GC回收fooLocal
就回收不了了。那可能有人想,你弄成弱引用不是很危險啊,萬一GC一下不是沒了?別怕只要fooLocal
這個強引用在這個ThreadLocal
例項就不會回收的。(關於強軟弱虛引用可以看我之前的文章四種引用方式的區別)
因此弄成弱引用,主要是讓沒用的ThreadLocal
得以GC清除。
這裡可能還有人問那key清除掉了,value咋辦,這個Entry還在的呀。是的,當在使用執行緒池的情況下,由於執行緒的生命週期很長,某些大物件的key被移除了之後,value一直存在的就可能會導致記憶體洩漏。
不過java考慮到這點了。當呼叫get()、set()
方法時會去找到那個key被幹掉的entry然後幹掉它。並且提供了remove()
方法。雖然get()、set()
會清理key
為null的Entry
,但是不是每次呼叫就會清理的,只有當get
時候直接hash沒中,或者set
時候也是直接hash沒中,開始線性探測時候,碰到key為null的才會清理。
//get 方法
private Entry getEntry(ThreadLocal<?> key) {
int i = key.threadLocalHashCode & (table.length - 1);
Entry e = table[i];
if (e != null && e.get() == key)
return e; //命中就直接返回
else
return getEntryAfterMiss(key, i, e); //直接沒命中
}
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
Entry[] tab = table;
int len = tab.length;
while (e != null) { //開始探測了
ThreadLocal<?> k = e.get();
if (k == key) //命中了就返回
return e;
if (k == null) //探測到key是null的就清理
expungeStaleEntry(i);
else
i = nextIndex(i, len); //否則繼續
e = tab[i];
}
return null;
}
//set 方法
private void set(ThreadLocal<?> key, Object value) {
Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1);
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();
if (k == key) {
e.value = value; //如果已經有就替換原有的value
return;
}
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
tab[i] = new Entry(key, value);
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
複製程式碼
因此,當不需要threadlocal
的時候還是顯示呼叫remove()
方法較好。
結語
執行緒本地儲存本質就是避免共享,在使用中注意記憶體洩露問題和hash碰撞問題即可。使用還是很廣泛的像spring中事務就用到threadlocal
。
如有錯誤歡迎指正!
個人公眾號:yes的練級攻略
有相關面試進階(分散式、效能調優、經典書籍pdf)資料等待領取