ThreadLocal的原理

參考:

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);

 這三個線程都在運行中,那此時各線程中的存數數據應該如下圖所示:

 由上圖看出Thread-1和Thread-1雖然使用相同的ThreadLocal引用作爲Key,但是他們的Value互不影響。

 下圖是ThreadLocal的存儲結構:

每一個線程都有一個ThreadLocalMap類型的threadLocals屬性;而ThreadLocalMap對象持有一個Entry數組的引用,每一個Entry存儲一個鍵值對,Key是一個ThreadLocal的弱引用(實際上是ThreadLocal的hashcode),Value是Object對象,就是本地變量

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,避免創建不必要的重複對象;
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章