1. 程式人生 > 實用技巧 >ThreadLocal原始碼解析

ThreadLocal原始碼解析

在多執行緒的情況下,ThreadLocal提供了一個種為每個執行緒訪問相同的變數,並且執行緒對變數的更新互不影響的機制。也是物件實現執行緒安全的一種方式。

ThreadLocal的實現機制

我們常用的方法有getsetinitialValue,這次將會圍繞這幾個方法的原始碼進行深入解析

  • get方法
    //  獲取元素
    public T get() {
        //  當前執行緒
        Thread t = Thread.currentThread();
        //  通過當前執行緒獲取ThreadLocalMap物件
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            //  獲取Entry,其中key為ThreadLocal物件自身
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;        //  獲取物件的值
                return result;
            }
        }
        //  返回initialValue的值
        return setInitialValue();
    }

首先,通過當前執行緒物件獲取ThreadLocalMap物件,然後以ThreadLocal物件自身為key獲取ThreadLocalMap.Entry,最後在獲取Entry中的value

程式碼的邏輯非常簡單,我們再來看看getMapmap.getEntry方法

  1. getMap方法
ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }

threadLocals是Thread的一個屬性

public class Thread implements Runnable {
    //  ......
    //  threadLocals是Thread的一個屬性
    ThreadLocal.ThreadLocalMap threadLocals = null;
}
  1. map.getEntry方法
    ThreadLocalMapThreadLocal物件的一個內部類,EntryThreadLocalMap的一個內部類
    //  ThreadLocal的內部類
    static class ThreadLocalMap {
        //  Entry是ThreadLocalMap的內部類,是一個弱引用物件
        static class Entry extends WeakReference<ThreadLocal<?>> {
            //  ThreadLocal中的value
            Object value;
            //  Entry的Key為ThreadLocal物件
            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }
        //  Entry陣列,用來存放一個執行緒的多個ThreadLocal變數
        private Entry[] table;
        //  根據ThreadLocal來獲取對應的value
        private Entry getEntry(ThreadLocal<?> key) {
            //  通過hash演算法獲取key在陣列中對應的下標
            int i = key.threadLocalHashCode & (table.length - 1);
            //  獲取下標對應的Entry物件
            Entry e = table[i];
            //  獲取value
            if (e != null && e.get() == key)
                return e;
            else
                //  當key不存在時獲取值,有2中可能
                //  1. 可能過期了
                //  2. 可能擴縮容
                return getEntryAfterMiss(key, i, e);
        }
}

key過期瞭如何獲取值

        private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
            Entry[] tab = table;
            int len = tab.length;

            while (e != null) {
                ThreadLocal<?> k = e.get();
                //  如果key存在,直接返回value
                if (k == key)
                    return e;
                //  如果key為空說明已經過期了,需要清除
                if (k == null)
                    expungeStaleEntry(i);
                else
                    //  獲取下一個key,看看能否找到
                    //  這是由於清除已經過期的key,
                    //  改變了Entry陣列的size引起的位置變更
                    i = nextIndex(i, len);
                e = tab[i];
            }
            return null;
        }
  • set方法
public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }

先獲取ThreadLocalMap物件,然後在以ThreadLocalkey,將value設定到ThreadLocalMap物件中

  1. map.set方法
        //  將ThreadLocal對應的value儲存到ThreadLocalMap物件中
        private void set(ThreadLocal<?> key, Object value) {
            Entry[] tab = table;
            int len = tab.length;
            //  計算table中的下標
            int i = key.threadLocalHashCode & (len-1);
            for (Entry e = tab[i];
                 e != null;
                 e = tab[i = nextIndex(i, len)]) {
                // 獲取ThreadLocal物件 
                ThreadLocal<?> k = e.get();
                //  如果Entry陣列中存在ThreadLocal物件,則替換之前的值
                if (k == key) {
                    e.value = value;
                    return;
                }
                //  去掉過期的ThreadLocal物件
                if (k == null) {
                    replaceStaleEntry(key, value, i);
                    return;
                }
            }
            //  Entry陣列中不存在ThreadLocal物件,建立一個新的Entry物件
            tab[i] = new Entry(key, value);
            int sz = ++size;
            //  清除過期的物件
            if (!cleanSomeSlots(i, sz) && sz >= threshold)
                // Entry陣列的大小改變以後重新計算hash 
                rehash();
        }
  1. createMap方法
void createMap(Thread t, T firstValue) {
        //  當執行緒的threadLocals為null時,為執行緒初始化一個ThreadLocalMap物件
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }
  • initialValue方法
// 可以通過重寫該方法來返回預設值
protected T initialValue() {
        return null;
    }

ThreadLocal記憶體洩漏問題

首先看一下ThreadLocal中物件的引用關係圖

從ThreadLocal中物件的引用關係來看,ThreadThreadLocalMapEntry物件之間都是強引用,如果可能出現記憶體洩漏那就是ThreadLocal物件弱引用引起的。

什麼時候會發生記憶體洩漏

ThreadLocal例項不在有強引用指向,只有弱引用存在,且GC回收了這部分空間時,也就是Entry物件中的key被回收了,但是value還沒有被回收,這時會出現記憶體洩漏,因為value無法得到釋放。

如何避免記憶體洩漏

ThreadLocalMap中是通過expungeStaleEntrykeynull的物件對應的value也設定為null

private int expungeStaleEntry(int staleSlot) {
    Entry[] tab = table;
    int len = tab.length;

    tab[staleSlot].value = null;
    tab[staleSlot] = null;
    size--;

    Entry e;
    int i;
    for (i = nextIndex(staleSlot, len);
        (e = tab[i]) != null;
        i = nextIndex(i, len)) {
        ThreadLocal<?> k = e.get();
        //  如果key為null,會將value也設定成null
        if (k == null) {
            e.value = null;
            tab[i] = null;
            size--;
        } else {
            int h = k.threadLocalHashCode & (len - 1);
            if (h != i) {
                 tab[i] = null;
                while (tab[h] != null)
                      h = nextIndex(h, len);
               tab[h] = e;
            }
        }
    }
    return i;
}

所以只要呼叫expungeStaleEntry方法,且keynull時就可以回收掉value了,我們可以通過呼叫ThreadLocalremove方法進行釋放

避免ThreadLocal出現記憶體洩漏的方式有

  1. 呼叫ThreadLocalremove方法
  2. ThreadLocal變數定義成static型別的,對ThreadLocal的強引用不會消失,所以也不存在記憶體洩漏的問題,但是可能會有所浪費