想必很多小伙伴們對ThreadLocal
並不陌生,ThreadLocal
叫做線程本地變量,也就是ThreadLocal為變量在每個線程中都創建了一個副本,每個線程可以訪問自己內部的副本變量。那么,我們使用ThreadLocal一定線程安全么?話不多說,先上結論:
如果threadlocal.get之后的副本,只在當前線程中使用,那么是線程安全的;如果對其他線程暴露,不一定是線程安全的。
為了演示下錯誤的使用方式,先看下如下代碼(雖然小伙伴們都不會這樣寫代碼 ^_^):
static class Container {
int num;
}
public static void main(String[] args) throws InterruptedException {
ThreadLocal<Container> tl = new ThreadLocal<>();
tl.set(new Container()); // 先set下ThreadLocal
Container container = tl.get();
Runnable task = () -> {
for (int i = 0; i < 10000; i++) {
container.num++;
}
};
Thread t1 = new Thread(task);
Thread t2 = new Thread(task);
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(tl.get().num);
}
筆者的一次結果輸出為:17581
結合代碼,我們知道,在執行threadlcoal.get
獲取到線程變量副本之后,不要讓其他線程來訪問它了,否則就是多線程操作同一個變量,可能造成線程安全問題。
除了上述討論的ThreadLocal線程安全性問題之外,ThreadLocal如果使用不當,可能存在內存泄露問題。ThreadLocal變量是保存在Thread.threadLocals
中(ThreadLocalMap類型)以Entry類型保存的,其中Entry.key(也就是弱引用referent實際指向對象)為ThreadLocal變量,該變量為弱類型;Entry.value為實際set的value。
// Entry,里面保存在ThreadLocal變量,也就是key,是弱引用
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
雖然Entry.referent是弱類型,指向ThreadLocal變量,但是如果ThreadLocal變量本身引用不置為null的話,這里的Entry.referent指向對象是不會釋放的。比如我們常用的定義方式:
// 靜態變量和對象屬性
static ThreadLocal<String> tls = new ThreadLocal<>();
ThreadLocal<Integer> tli = new ThreadLocal<>();
類似於靜態變量和對象屬性這種引用,如果不將tls或tli設置為null,那么ThreadLocal變量無法釋放(這不是廢話么,人家可是強引用呀),此時的Entry.referent弱類型沒啥卵用;只有在tls或tli為null時,Entry.referent弱類型就起作用了,在第一次GC時就會將Entry.referent弱類型指向的對象回收。
如果Entry.referent弱類型指向的對象回收了(沒調用ThreadLocal.remove操作),Entry.value對象還在,並且Entry.value可是強引用的,此時就發生了內存泄露。這也就是ThreadLocal使用不當(沒調用ThreadLocal.remove)時產生的內存泄漏問題。不過,伴隨着其他ThreadLocal對象的set/get/remove
的進行,會清除一部分Entry.referent為null但是Entry.value不為null的對象的,也就是修復內存泄露問題,注意,這個只是清除部分這樣的Entry,並不能保證一次就能清除全部這樣的Entry,所以還是要遵循ThreadLocal.set,用完之后就remove。
討論完了ThreadLocal的潛在問題之后,你是不是意猶未盡,想深入了解下ThreadLocal實現原理
?OK,那就搬起小板凳,一起嘮嘮吧~
ps:如果小伙伴對ThreadLocal原理已經熟悉了,那么恭喜你,后面的內容可以不看了~
ThreadLocal實現原理
ThreadLocal變量主要有get/set/remove
三個操作,理解了這三個操作流程,基本上就理解了ThreadLocal實現原理。
get
get流程如下:
- 獲取當前線程的threadLocals(map結構),從threadLocals中獲取當前ThreadLocal變量對應的ThreadLocalMap.Entry(pair類型,包含了當前ThreadLocal變量及其對應的value),非空直接返回對應的value
- 為空時使用默認值(默認為null)構造ThreadLocalMap.Entry,放到當前線程的threadLocals中,下次再get時直接返回ThreadLocalMap.Entry對應的value即可
/** * 當前線程的threadLocalMap中獲取當前ThreadLocal對應的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;
}
}
// 設置null值,下次直接返回null了
return setInitialValue();
}
/** * 如果一次找到了entry,直接返回;否則就是set時hash沖突了 * 遍歷后續的slot,進行查找 * 這里其實JDK可以做個優化,在set之后,將slot位置記錄在Threadlocal變量中,下次直接到對應slot位置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);
}
注意:線程的threadLocals是一個基於開放定址法實現的map結構。
set
- set操作就是將ThreadLocal變量的值put到當前線程的threadLocals中,ThreadLocal變量及其對應的值會構造成一個ThreadLocalMap.Entry放到threadLocals中。
- 因為線程的threadLocals是一個基於開放定址法實現的map結構,所以在出現hash沖突后會繼續尋找下一個空位進行set操作。
- 因為是基於開放定址法,如果map中元素過多,會影響get和put性能,所以需要擴容,map的數組結構默認大小為
INITIAL_CAPACITY = 16
,默認擴容閾值為threshold = INITIAL_CAPACITY * 2 / 3
,擴容時按照成倍擴容。
/** * 獲取當前線程的threadLocalMap,非空直接set value; * 否則新建一個包含value的threadLocalMap。 * threadLocalMap的key對應程序中定義的ThreadLocal變量,value對應要set的值 */
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t); // Thread.threadLocals
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
// Entry,里面保存在ThreadLocal變量,也就是key,是弱引用
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
/** * hash碼的生成,這里所有的ThreadLocal對象hash生成都是基於static變量nextHashCode來做的 * 創建ThreadLocal對象時threadLocalHashCode已初始化完成 */
private final int threadLocalHashCode = nextHashCode();
private static int nextHashCode() {
return nextHashCode.getAndAdd(HASH_INCREMENT);
}
private static AtomicInteger nextHashCode =
new AtomicInteger();
/** * 當前線程的threadLocalMap非空直接set value */
private void set(ThreadLocal<?> key, Object value) {
Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1);
// 如果當前table[i] hash沖突,那么就以i為起點,遍歷后續table[i],
// 這其實就是hash沖突中的開放定址法,另外一種是分離鏈接法
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();
// key已存在,更新vlaue即可
if (k == key) {
e.value = value;
return;
}
// key為null,復制value即可
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
// 新建Entry,清理一部分Entry.key為null,value不為null的數據,避免內存泄露
// 超過了threshold時rehash操作
tab[i] = new Entry(key, value);
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
remove
/** * 從ThreadLocalMap刪除對應key */
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
private void remove(ThreadLocal<?> key) {
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)]) {
if (e.get() == key) {
// 清除Entry.key弱引用,設置為null
e.clear();
// 清除Entry.value引用,可能還涉及部分key為null的Entry數據清理
expungeStaleEntry(i);
return;
}
}
}
小結
從ThreadLocal的get/set
操作流程來看,ThreadLocal的value 是 Lazy Init(延遲初始化的)
。ThreadLocal為什么是延遲初始化,這個問題應該是容易理解的,原因是:在沒有具體業務場景前提下,這樣的做法避免內存浪費。
ThreadLocal變量默認放在基於開放定址法實現的map結構中,這種結構在hash沖突時會造成多次get/set
操作,理論上可以通過記錄ThreadLocal變量set時的位置,這樣下次直接通過該位置獲取對應value即可,可以參考netty的FastThreadLocal
,它的實現思路就是這樣的,提高了set/get的效率。
最后來一張ThreadLocal的整體圖:
參考資料:
1、https://luoxn28.github.io/2019/04/27/ni-de-threadlocal-yi-ding-xian-cheng-an-quan-ma/