博文位址
我的GitHub | 我的部落格 | 我的微信 | 我的郵箱 |
---|---|---|---|
baiqiantao | bqt20094 | [email protected] |
目錄
- ThreadLocal
- 總結
- 使用線程池的問題
- 記憶體洩漏問題
- 總結
- 源碼分析
- Thread
-
- 構造方法
- set() 方法
- get() 方法
- initialValue() 方法
- ThreadLocalMap
- 基礎結構
- Hash 算法
- Hash 沖突
- InheritableThreadLocal
- 實作原理
- 測試代碼
- 案例
ThreadLocal 為解決
多線程并發
問題提供了一種新的思路。使用它可以很簡潔地編寫出優美的多線程程式。當使用 ThreadLocal 維護變量時,ThreadLocal 為每個使用該變量的線程提供獨立的
變量副本
,是以每一個線程都可以獨立地改變自己的副本,而不會影響其它線程所對應的副本。
使用場景
- 存儲單個線程上下文資訊
- 使變量線程安全:變量既然成為了每個線程内部的局部變量,自然就不會存在并發問題了
- 減少參數傳遞
ThreadLocal 提供了
線程獨有的局部變量(本地變量、副本變量)
,可以在整個線程存活的過程中随時取用,且線程之間互不幹擾。
- ThreadLocal 裡 set 進去的資料,其實是存儲在目前 Thread 裡的(Thread 也是一個對象)
- 每個 Thread 都有一個屬性
,類型為threadLocals
,它本質上是一個自定義的ThreadLocal.ThreadLocalMap
map
- 這個 map 的 entry 是
,這個 entry 繼承自ThreadLocal.ThreadLocalMap.Entry
WeakReference<ThreadLocal<T>>
- 這表明:
是ThreadLocal<?>
,被弱引用對象引用的對象
(而非通過其 set 進去的資料)會在 gc 時被回收ThreadLocal<T>
- 這個 map 其實隻是一個數組,map 的 get/set 方法的參數(也即象征意義的 key)是
ThreadLocal<?>
- 數組中存儲的值(也即象征意義的 value)即通過
的 set 方法傳過來的 Object(類型就是裡面的泛型)ThreadLocal<?>
- 由于 map 中對
是通過弱引用的方式引用的,是以當ThreadLocal<?>
不再被強引用時,此ThreadLocal<?>
對象就會在 gc 時被回收ThreadLocal<?>
使用線程池可以達到線程複用的效果,但是歸還線程之前記得清除
ThreadLocalMap
,要不然再取出該線程的時候,
ThreadLocal
變量還會存在。
為何存在記憶體洩漏的問題
-
中是以弱引用的方式引用的ThreadLocalMap
,如果一個ThreadLocal
沒有外部強引用來引用它,那麼 GC 的時候,這個ThreadLocal
就會被回收。ThreadLocal
-
被回收後,ThreadLocal
中就會出現ThreadLocalMap
為key
的null
,此後,這些Entry
key
null
Entry
就沒有辦法通路了。value
- 由于這些
key
null
Entry
存在一條強引用鍊:value
,這就造成這些Thread -> ThreaLocalMap -> Entry -> value
永遠無法回收。value
- 如果目前線程遲遲不結束的話,就會造成記憶體洩漏。
如何解決記憶體洩漏的問題
在調用
ThreadLocal
get()、set()、remove()
的時候,都會主動清除掉
ThreadLocalMap
裡所有
key
null
value
。
需要明确,使用
ThreadLocal
肯定是存在記憶體洩漏的問題的,上面的方案雖然解決了部分場景的記憶體洩漏問題,但并不徹底!
首先從
Thread
類源碼入手:
public class Thread implements Runnable {
ThreadLocal.ThreadLocalMap threadLocals = null; // 存儲普通的線程資料(本地變量)
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null; // 可以被子類繼承
}
Thread
類中有一個
threadLocals
和一個
inheritableThreadLocals
變量,它們都是
ThreadLocalMap
類型的變量。
預設情況下這兩個變量都是 null,隻有目前線程調用
ThreadLocal
類的
set
或
get
方法時才建立它們,實際上調用這兩個方法的時候,最終調用的是
ThreadLocalMap
類對應的方法。
ThreadLocalMap 是 ThreadLocal 的内部類,本質是一個自定義的 map。
public ThreadLocal()
protected T initialValue()
public void set(T value)
public T get()
public void remove()
private final int threadLocalHashCode = nextHashCode(); //用來在 map 中找到自己
public ThreadLocal() {}
ThreadLocal 執行個體的變量隻有一個
threadLocalHashCode
,用來在 ThreadLocalMap 中找到自己存儲的位置
public void set(T value) {
Thread t = Thread.currentThread(); // 擷取目前線程
ThreadLocalMap map = t.threadLocals; // 傳回目前線程中的成員變量 threadLocals
if (map != null) map.set(this, value); // 注意是将該 ThreadLocal 執行個體作為key
else t.threadLocals = new ThreadLocalMap(this, value); // 初始化線程中的成員變量,并指派
}
set
方法很簡單,主要是判斷
ThreadLocalMap
是否存在,然後使用
ThreadLocalMap
中的
set
方法進行資料處理。具體邏輯在後面剖析
ThreadLocalMap
源碼時再看。
通過上面的
set
方法及下面
get
方法可知,通過
ThreadLoca
set
方法存儲的對象,都是存儲在目前線程對象的
ThreadLocalMap
中的,其他線程通路不到,各個線程中通過
get
方法通路的是不同的對象。
public T get() {
Thread t = Thread.currentThread(); // 擷取目前線程
ThreadLocalMap map = t.threadLocals; // 擷取目前線程中的 ThreadLocalMap
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this); // 擷取 ThreadLocal 對應的鍵值對
if (e != null) return (T)e.value; // 如果鍵值對存在,則傳回目前 ThreadLocal 對應的值
}
return setInitialValue(); // 如果鍵值對不存在,則傳回 initialValue() 方法指定的初始值
}
If the variable has no value for the current thread, it is first initialized to the value returned by an invocation of the
initialValue()
method.
private T setInitialValue() {
T value = initialValue(); // 擷取指定的初始值
set(value); // 中間這一塊的邏輯和 set 方法完全一樣
return value;
}
get
方法的邏輯也很簡單,如果通過目前
ThreadLocal
能在
map
中找到非空的
Entry
,則正常傳回
Entry
中的值(也即之前通過
set
方法存儲的值),否則傳回
initialValue()
方法指定的初始值。
核心邏輯依舊在
ThreadLocalMap
getEntry(tl)
方法中,具體邏輯同樣在後面剖析
ThreadLocalMap
initialValue()
方法僅在
get
方法中被調用,用于傳回
ThreadLocal
對應的初始化值,一般作為匿名内部類使用。
關于
initialValue()
的注意事項:
- Returns the current thread's "initial value" for this thread-local variable.
- This method will be invoked the first time a thread accesses the variable with the
method, unless the thread previously invoked theget
method, in which case the initialValue() method will not be invoked for the thread.set
- 注意這個方法可能不調用,也可能調用多次,不能在裡面做初始化邏輯
一般沒啥用的一個方法
ThreadLocalMap
類似
HashMap
的結構,隻是
HashMap
是由數組+連結清單實作的,而
ThreadLocalMap
中并沒有連結清單結構。
ThreadLocalMap is a customized hash map
suitable 适用于 only for maintaining 維護 thread local values.
static class ThreadLocalMap {
//The table, resized as necessary. table.length MUST always be a power of two.
private Entry[] table; // 可自動擴容的數組
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value; // The value associated with this ThreadLocal
Entry(ThreadLocal<?> k, Object v) {
super(k); // key
value = v; // value
}
}
//...
}
- ThreadLocalMaps are constructed
, so we only create one when we have at least one entry tolazily
in it.put
- To help deal with very large and long-lived 長期存在 usages, the hash table entries use
forWeakReferences
.keys
- However, since
are not used, stale 過時的 entries are guaranteed 確定 to be removed only when the table starts running out of space 沒有空間.ReferenceQueue
- Note that
mean that the key is no longer referenced, so the entry can be expunged 移除 from table.null keys
int i = key.threadLocalHashCode & (table.len-1); // 計算目前 key 在散清單中對應的數組下标位置
這裡最關鍵的就是
threadLocalHashCode
值的計算,這個值其實在建立 ThreadLocal 時已經計算好了。
private final int threadLocalHashCode = nextHashCode(); // ThreadLocal 的 hash 值
private static int nextHashCode() {
return nextHashCode.getAndAdd(HASH_INCREMENT); // Atomically adds the given value to the current value.
}
private static AtomicInteger nextHashCode = new AtomicInteger(); //The next hash code to be given out. Updated atomically. Starts at zero.
private static final int HASH_INCREMENT = 0x61c88647;
每當建立一個
ThreadLocal
對象,這個
ThreadLocal.nextHashCode
這個值就會在之前的基礎上增長
0x61c88647
。這個值很特殊,它是斐波那契數也叫黃金分割數,hash 增量為這個數字的好處就是:hash 分布非常均勻。
HashMap
中解決沖突的方法是
連結清單法
,沖突的資料挂載到連結清單上,如果連結清單長度超過一定數量則會轉化成紅黑樹。
ThreadLocalMap
開放位址法
(
線性探測法
):插入一個
Entry
時,如果通過
hash
計算的槽位中已經有了
Entry
資料,此時就會線性向後查找,一直找到
Entry
null
的槽位才會停止查找,并将目前元素放入此槽位中
往
ThreadLocalMap
中
set
資料分為以下幾種情況:
- 通過
計算後的槽位對應的hash
資料為空:直接将資料放到該槽位即可Entry
-
hash
資料不為空Entry
- 如果
值與目前key
ThreadLocal
計算擷取的hash
值一緻:更新該槽位的資料即可key
-
key
ThreadLocal
hash
值不一緻:線性向後查找key
- 往後周遊過程中,在找到
Entry
的槽位之前,沒有遇到null
過期的key
:周遊散列數組,線性往後查找,如果找到Entry
Entry
的槽位,則将資料放入該槽位中,或者往後周遊過程中,遇到了null
值相等的資料,直接更新即可key
-
Entry
的槽位之前,遇到了null
key
:會進行一輪探測式清理操作,具體邏輯就不去理了,意義不大Entry
- 往後周遊過程中,在找到
- 如果
在
set()
方法其實做了很多事情,包括:添加資料、更新資料、清理資料、優化資料桶的位置、數組擴容,具體邏輯就不去理了,意義不大。
get
的邏輯和
set
類似,分為以下幾種情況:
-
hash
和查找的Entry.key
一緻:則直接傳回key
-
hash
Entry.key
不一緻:則往後疊代查找,查找過程中也會進行一輪探測式清理操作key
使用
ThreadLocal
的時候,在異步場景下是無法給
子線程
共享
父線程
中建立的線程副本資料的。使用
InheritableThreadLocal
便可以解決這個問題。
實作原理很簡單:在父線程中通過
new Thread()
建立子線程時,
Thread#init
方法會在
Thread
的構造方法中被調用,在
init
方法中會拷貝父線程的
InheritableThreadLocal
中的資料到子線程中:
private void init(...boolean inheritThreadLocals) {
if (inheritThreadLocals && parent.inheritableThreadLocals != null)
this.inheritableThreadLocals = ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
}
static ThreadLocalMap createInheritedMap(ThreadLocalMap parentMap) {
return new ThreadLocalMap(parentMap);
}
ThreadLocal<String> threadLocal = new ThreadLocal<>();
ThreadLocal<String> local = new InheritableThreadLocal<>();
threadLocal.set("白乾濤");
local.set("白乾濤");
ThreadLocal<String> threadLocal2 = ThreadLocal.withInitial(() -> "白乾濤2");
ThreadLocal<String> local2 = InheritableThreadLocal.withInitial(() -> "白乾濤2");
new Thread(() -> {
System.out.println("子線程擷取父線程ThreadLocal資料:" + threadLocal.get() + "-" + threadLocal2.get()); // null-白乾濤2
System.out.println("子線程擷取父線程InheritableThreadLocal資料:" + local.get() + "-" + local2.get()); // 白乾濤-白乾濤2
}).start();
public class Test {
ThreadLocal<String> mNameLocal = new ThreadLocal<>();
ThreadLocal<Long> mIdLocal = ThreadLocal.withInitial(() -> {
System.out.println(Thread.currentThread().getName() + " 調用了 get");
return -1L; //初始化資料
});
public static void main(String[] args) throws InterruptedException {
new Test().test();
}
private void test() throws InterruptedException {
mNameLocal.set(Thread.currentThread().getName()); //更新【主線程】資料
mIdLocal.set(Thread.currentThread().getId()); //更新【主線程】資料
Thread thread = new Thread(() -> {
mNameLocal.set(Thread.currentThread().getName()); //更新【子線程】資料
//mIdLocal.set(Thread.currentThread().getId()); //更新【子線程】資料
System.out.println(mNameLocal.get() + " " + mIdLocal.get()); //Thread-0 -1
});
thread.start();
thread.join(); //效果等同于同步:等 thread 執行完畢後再執行下面的邏輯
System.out.println(mNameLocal.get() + " " + mIdLocal.get()); //main 1
mNameLocal.remove();
mIdLocal.remove();
System.out.println(mNameLocal.get() + " " + mIdLocal.get()); //null -1
}
}
列印結果:
Thread-0 調用了 get
Thread-0 -1
main 1
main 調用了 get
null -1
2019-1-21
本文來自部落格園,作者:白乾濤,轉載請注明原文連結:https://www.cnblogs.com/baiqiantao/p/7257326.html