Java讀源碼之ThreadLocal

前言

JDK版本: 1.8java

以前在看Thread源碼時候看到這麼一個屬性面試

ThreadLocal.ThreadLocalMap threadLocals = null;

做用

ThreadLocal實現的是每一個線程都有一個本地的副本,至關於局部變量,這樣就能夠少一些參數傳遞,是以空間換時間的一週策略,其實ThreadLocal就是內部本身實現了一個map數據結構。redis

存在的問題

ThreadLocal確實很重要,但想到看源碼仍是有個小故事的,以前去美團點評面試,問我如何保存用戶登陸token,能夠避免層層傳遞token?數組

心想這好像是在說ThreadLocal,而後開始胡說放在redis裏或者搞個ThreadLocal,給本身挖坑了數據結構

面試官繼續問,ThreadLocal使用時候主要存在什麼問題麼?this

完蛋,確實只瞭解過,沒怎麼用過,涼涼,回來查了下主要存在的問題以下線程

  • ThreadLocal可能內存泄露?

帶着疑惑進入源碼吧3d

源碼

類聲明和重要屬性

package java.lang;

public class ThreadLocal<T> {
    
    // hash值,相似於Hashmap,用於計算放在map內部數組的哪一個index上
    private final int threadLocalHashCode = nextHashCode();
    private static int nextHashCode() { return nextHashCode.getAndAdd(HASH_INCREMENT);}
    // 初始0
    private static AtomicInteger nextHashCode = new AtomicInteger();
    // 神奇的值,這個hash值的倍數去計算index,分佈會很均勻,總之很6 
    private static final int HASH_INCREMENT = 0x61c88647;
    
    static class ThreadLocalMap {

        // 注意這是一個弱引用
        static class Entry extends WeakReference<ThreadLocal<?>> {
            Object value;

            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }
        // 初始容量16,必定要是2的倍數
        private static final int INITIAL_CAPACITY = 16;
        // map內部數組
        private Entry[] table;
        // 當前儲存的數量
        private int size = 0;
        // 擴容指標,計算公式 threshold = 總容量 * 2 / 3,默認初始化以後爲10
        private int threshold;

增改操做

讓咱們先來看看增改方法code

public void set(T value) {
    Thread t = Thread.currentThread();
    // 拿到當前Thread對象中的threadLocals引用,默認threadLocals值是null 
    ThreadLocalMap map = getMap(t);
    if (map != null)
        // 若是ThreadLocalMap已經初始化過,就把當前ThreadLocal實例的引用當key,設置值
        map.set(this, value); //下文詳解
    else
        // 若是不存在就建立一個ThreadLocalMap而且提供初始值
        createMap(t, value);
}

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

void createMap(Thread t, T firstValue) {
    t.threadLocals = new ThreadLocalMap(this, firstValue);
}

讓咱們來看看map.set(this, value)具體怎麼操做ThreadLocalMap對象

private void set(ThreadLocal<?> key, Object value) {
    // 獲取ThreadLocalMap內部數組
    Entry[] tab = table;
    int len = tab.length;
    // 算出須要放在哪一個桶裏
    int i = key.threadLocalHashCode & (len-1);
    // 若是當前桶衝突了,這裏沒有用拉鍊法,而是使用開放定指法,index遞增直到找到空桶,數據量很小的狀況這樣效率高
    for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) {
        // 拿到目前桶中key
        ThreadLocal<?> k = e.get();
        // 若是桶中key和咱們要set的key同樣,直接更新值就ok了
        if (k == key) {
            e.value = value;
            return;
        }
        // 桶中key是null,由於是弱引用,可能被回收掉了,這個時候咱們直接佔爲己有,而且進行cleanSomeSlots,當前key附近局部清理其餘key是空的桶
        if (k == null) {
            replaceStaleEntry(key, value, i);
            return;
        }
    }
    // 若是沒衝突直接新建
    tab[i] = new Entry(key, value);
    int sz = ++size;
    // 當前key附近局部清理key是空的桶,若是一個也沒清除而且當前容量超過閾值了就擴容
    if (!cleanSomeSlots(i, sz) && sz >= threshold)
        rehash();
}


private void rehash() {
    // 這個方法會清除全部key爲null的桶,清理完後size的大小會變小
    expungeStaleEntries();

    // 此時size還大於閾值的3/4就擴容
    if (size >= threshold - threshold / 4)
        // 2倍擴容
        resize();
}

爲何會內存泄漏

總算讀玩了set,大概明白了爲何會發生內存泄漏,畫了個圖

ThreadLocalMap.Entry中的key保存了ThreadLocal實例的一個弱引用,若是ThreadLocal實例棧上的引用斷了,只要GC一發生,就鐵定被回收了,此時Entry的key,就是null,可是呢Entry的value是強引用並且是和Thread實例生命週期綁定的,也就是線程沒結束,值就一直不會被回收,因此產生了內存泄漏。

總算明白了,爲何一個set操做要這麼屢次清理key爲null的桶。

既然這麼麻煩,爲何key必定要用弱引用?

繼續看上面的圖,若是咱們的Entry中保存的是ThreadLocal實例的一個強引用,咱們刪掉了ThreadLocal棧上的引用,同理此時不只value就連key也不會回收了,這內存泄漏就更大了

查詢操做

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
    return setInitialValue();
}

private T setInitialValue() {
    T value = initialValue();
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
        // 若是隻是threadLocals.Entry是空,就設置value爲null
        map.set(this, value);
    else
        // 若是threadLocals是空,就new 一個key是當前ThreadLocal,value是空的ThreadLocalMap
        createMap(t, value);
    return value;
}

protected T initialValue() {
    return null;
}

讓咱們來看看map.getEntry(this)具體怎麼操做ThreadLocalMap

private Entry getEntry(ThreadLocal<?> key) {
    int i = key.threadLocalHashCode & (table.length - 1);
    Entry e = table[i];
    if (e != null && e.get() == key)
        // 最好狀況,定位到了Entry,而且key匹配
        return e;
    else
        // 多是hash衝突重定址了,也多是key被回收了
        return getEntryAfterMiss(key, i, e);
}

private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
    Entry[] tab = table;
    int len = tab.length;
    // 向後遍歷去匹配key,同時清除key爲null的桶
    while (e != null) {
        ThreadLocal<?> k = e.get();
        if (k == key)
            return e;
        if (k == null)
            expungeStaleEntry(i);
        else
            i = nextIndex(i, len);
        e = tab[i];
    }
    return null;
}

如何避免內存泄漏

新增,查詢中無處不在的去清理key爲null的Entry,是否是咱們就能夠放心了,大多數狀況是的,可是若是咱們在使用線程池,核心工做線程是不會中止的,會重複利用,這時咱們的Entry中的value就永遠不會被回收了這很糟糕,還好源碼做者還沒給我提供了remove方法,綜上所述,養成良好習慣,只要使用完ThreadLocal,必定要進行remove防止內存泄漏

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) {
            // 主要多了這一步,讓this.referent = null,GC會提供特殊處理
            e.clear();
            expungeStaleEntry(i);
            return;
        }
    }
}
相關文章
相關標籤/搜索