參考:
- https://blog.csdn.net/v123411739/article/details/78698834
- https://blog.csdn.net/u010687392/article/details/50549236
- https://www.jianshu.com/p/56f64e3c1b6c
- https://www.jianshu.com/p/377bb840802f
1 簡介
ThreadLocal是線程本地變量的一種實現方式;線程本地變量線程自己私有,不同線程的本地變量互不影響,不存在線程安全問題;
下面是ThreadLocal簡單使用
//ThreadLocal簡單使用
static ThreadLocal<Object> threadLocal = new ThreadLocal<> ();//必須設置爲靜態屬性,避免無意義的多實例
threadLocal.set(obj);//爲當前線程設置一個本地變量
Object obj2 = threadLocal.get();//獲取threadLocal作爲key對應的本地變量
threadLocal.set(obj2);//覆蓋之前的Value obj
threadLocal.remove();//移除這個本地變量,防止內存泄漏
2 存儲結構
首先我們來聊一聊 ThreadLocal 在多線程運行時,各線程是如何存儲變量的,假如我們現在定義兩個 ThreadLocal 實例如下:
static ThreadLocal<User> threadLocal_1 = new ThreadLocal<>();
static ThreadLocal<Client> threadLocal_2 = new ThreadLocal<>();
我們分別在三個線程中使用 ThreadLocal,僞代碼如下:
// thread-1中
threadLocal_1.set(user_1);
threadLocal_2.set(client_1);
// thread-2中
threadLocal_1.set(user_2);
threadLocal_2.set(client_2);
// thread-3中
threadLocal_2 .set(client_3);
這三個線程都在運行中,那此時各線程中的存數數據應該如下圖所示:
下圖是ThreadLocal的存儲結構:
3. 源碼分析
3.1 ThreadLocalMap簡介
ThreadLocalMap 是ThreadLocal 的一個內部類,然而它並沒有繼承Map類,因爲它只供ThreadLocal 內部使用,數據結構採用 數組 + 開方地址法;它的默認變長是16;
static class Entry extends WeakReference<ThreadLocal> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal k, Object v) {
super(k);
value = v;
}
}
Entry 是ThreadLocalMap 的內部類,繼承自 WeakReference,所以Entry存儲的key是一個ThreadLocal弱引用;所以只能自動回收弱引用的key,而強引用 value 的需要手動回收(用expungeStaleEntry()方法)。
3.2 ThreadLocalMap 之 key 的 hashCode
class ThreadLocal{
//...
//hashcode,實例化時執行
private final int threadLocalHashCode = nextHashCode();
// AtomicInteger類型,從0開始
private static AtomicInteger nextHashCode = new AtomicInteger();
// hash code每次增加1640531527
private static final int HASH_INCREMENT = 0x61c88647;
//實例化時調用
private static int nextHashCode() {
return nextHashCode.getAndAdd(HASH_INCREMENT);
}
}
每生成一個ThreadLocal對象,新的hashcode就增加1640531527
;
3.3 set方法
3.3.1 ThreadLocal的set方法
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t); // 獲取當前線程的ThreadLocalMap對象
if (map != null) // 判斷map是否存在
map.set(this, value); // 調用 map 的 set 方法
else
createMap(t, value); // 創建map並插入<this,value>
}
第一次調用set時map爲空,需要creatMap並插入這個鍵值對實體,創建方式比較簡單,不詳解;這裏重要的還是 ThreadLocalMap 的 set 方法。
3.3.2 ThreadLocalMap的set方法
private void set(ThreadLocal<?> key, Object value) {
Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1); // 用key的hashCode計算索引位置
// hash衝突時,使用開放定址法解決衝突
for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();//當前位置的key
if (k == key) { // 若當前key與傳入key相同,則覆蓋value
e.value = value;
return;
}
if (k == null) { // key = null,說明當前Entry是個過期Entry(key爲null的Entry)
//向後找下一個插入位置並清理過期的Entry
replaceStaleEntry(key, value, i);
return;
}
}
// 當前位置爲空,生成一個Entry並插入該位
tab[i] = new Entry(key, value);
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold) // 清除一些過期的entry,並判斷是否需要擴容
rehash(); // 擴容
}
在set一個線程本地變量的過程中,先根據ThreadLocal對象的hash值,定位到Entry table中的位置i,使用開放定址法處理Hash衝突,過程如下:
- 如果當前位置Entry爲空,生成一個Entry並插入該位置;
- 如果當前位置Entry不爲空且當前key與傳入的key相同,用新value覆蓋舊value;
- 如果當前位置Entry不爲空但key不同,說明hash衝突,那麼只能向後找下一個位置;
3.4 get方法
3.4.1 ThreadLocal 的get() 方法
public T get() {
Thread t = Thread.currentThread();//獲取當前線程
ThreadLocalMap map = getMap(t);// 獲取當前線程的 ThreadLocalMap對象
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this); //調用map的getEntry方法
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;//獲取本地變量
return result;
}
}
return setInitialValue(); //如果沒有set過,返回本地變量默認值(可自定義)
}
3.4.2 ThreadLocalMap的getEntry方法
private Entry getEntry(ThreadLocal<?> key) {
int i = key.threadLocalHashCode & (table.length - 1);//獲取key對應的索引位置
Entry e = table[i];//當前位置的Entry
if (e != null && e.get() == key) //若當前key與傳入key相同,則找到目標Entry(無hash衝突情況)
return e;
else
return getEntryAfterMiss(key, i, e); //若不同,查找下一個位置(有hash衝突情況),這個方法中有清除過期Entry的操作
}
在get一個線程本地變量的過程中,先根據ThreadLocal對象的hash值,定位到Entry table中的位置i;
- 若當前key與傳入key相同,則找到目標Entry(無hash衝突情況);
- 若不同,查找下一個位置(有hash衝突情況);
3.5 remove方法
3.5.1 ThreadLocal 之 remove() 方法
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());//獲取當前線程的ThreadLocalMap對象
if (m != null)
m.remove(this); // 調用ThreadLocalMap的remove方法
}
先獲取當前線程的ThreadLocalMap對象,然後調用ThreadLocalMap的remove方法移除指定threadLocal鍵對應的Entry
3.5.2 ThreadLocalMap的remove方法
private void remove(ThreadLocal<?> key) {
Entry[] tab = table;
int len = tab.length;
// 根據hashCode計算出當前ThreadLocal的索引位置
int i = key.threadLocalHashCode & (len-1);
// 從位置i開始遍歷,直到Entry爲null
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
if (e.get() == key) { // 如果找到相同的key
e.clear(); // 調用clear方法, 先清空key
expungeStaleEntry(i);//後調用expungeStaleEntry方法清理過期實體
return;
}
}
}
remove指定key本地變量的過程是一個查找清理的過程,先計算當前ThreadLocal作爲key對應的索引位置i,從i開始往後遍歷,如果找到key相同Entry,清理掉;
remove過程也會調用expungeStaleEntry(i)
方法清理過期Entity;
set、get、remove方法,都會調用expungeStaleEntry(i)
方法清理過期Entry(key=null);
4 hash衝突
當出現不同的key相同的hashcode時就會出現hash衝突;ThreadLocalMap處理衝突的方法是開放定址法,di使用的是線性探測;
5 內存泄露及解決辦法
- 爲什麼?Thread的生命週期可能會比較長;value是強引用,key是弱引用生命週期短;key被GC後是空,value可能長時間(直到當前Thread運行結束)處於無法使用也無法回收的狀態;
- 怎麼解決?手動回收,每次使用完ThreadLocal後調用remove;
6 舉例
public class ThreadLocalDemo {
private static ThreadLocal<String> threadLocal = new ThreadLocal<String> (){
@Override
protected String initialValue() {
return "not be set !";
}
};
static class MyRunnable implements Runnable{
private int num;
public MyRunnable(int num){
this.num = num;
}
@Override
public void run() {
threadLocal.set(String.valueOf(num));
System.out.println(Thread.currentThread().getName()+"'s local Value is "+threadLocal.get());
threadLocal.remove();
}
}
public static void main(String[] args) throws InterruptedException {
Thread thread1=new Thread(new MyRunnable(1));
Thread thread2=new Thread(new MyRunnable(2));
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println(Thread.currentThread().getName()+"'s local Value is "+threadLocal.get());
}
}
Thread-0's local Value is 1
Thread-1's local Value is 2
main's local Value is not be set !
輸出驗證了不同線程的本地變量互不影響;
7 總結
- ThreadLocal是線程本地變量;不同線程的本地變量互不影響,不存在線程安全問題;
- ThreadLocal簡單使用:
//ThreadLocal簡單使用
static ThreadLocal<Object> threadLocal = new ThreadLocal<> ();//必須設置爲靜態屬性,避免無意義的多實例
threadLocal.set(obj);//爲當前線程設置一個本地變量
Object obj2 = threadLocal.get();//獲取threadLocal作爲key對應的本地變量
threadLocal.set(obj2);//覆蓋之前的Value obj
threadLocal.remove();//移除這個本地變量,防止內存泄漏
- ThreadLocal的存儲結構:
每一個線程都有一個ThreadLocalMap類型的threadLocals屬性;而ThreadLocalMap對象持有一個Entry數組的引用,每一個Entry存儲一個鍵值對,Key是一個ThreadLocal的弱引用(實際上是ThreadLocal的hashcode),Value是Object對象,就是本地變量。
- ThreadLocalMap 是ThreadLocal 的一個內部類,Entry 是ThreadLocalMap 的內部類;
- set一個本地變量的過程:先根據ThreadLocal對象的hash值,定位到Entry table中的位置i,使用開放定址法處理Hash衝突,過程如下:
- 如果當前位置Entry爲空,生成一個Entry並插入該位置;
- 如果當前位置Entry不爲空且當前key與傳入的key相同,用新value覆蓋舊value;
- 如果當前位置Entry不爲空但key不同,說明hash衝突,那麼只能向後找下一個位置;
- set、get、remove方法,都會調用expungeStaleEntry(i)方法清理過期Entry(key=null);
- hash衝突處理辦法:ThreadLocalMap處理衝突的方法是開放定址法,di使用的是線性探測;
- 內存泄漏:
- 爲什麼?Thread的生命週期可能會比較長;value是強引用,key是弱引用生命週期短;key被GC後是空,value可能長時間(直到當前Thread運行結束)處於無法使用也無法回收的狀態;
- 怎麼解決?手動回收,每次使用完ThreadLocal後調用remove;
- ThreadLocal變量一定要設置爲static,避免創建不必要的重複對象;