ThreadLocal底層原理學習

1. 是什麼?

首先ThreadLocal類是一個線程數據綁定類, 有點相似於HashMap<Thread, 你的數據> (但實際上並不是如此), 它全部線程共享, 但讀取其中數據時又只能是獲取線程本身的數據, 寫入也只能給線程本身的數據java

2. 怎麼用?

public class ThreadLocalDemo {
    
    private static final ThreadLocal<String> threadLocal = new ThreadLocal<>();
    
    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                threadLocal.set("zhazha" + Thread.currentThread().getName());
                String s = threadLocal.get();
                System.out.println("threadName = " + Thread.currentThread().getName()  + " [ threadLocal = "  + threadLocal + "\t data = " + s + " ]");
            }, "threadName" + i).start();
        }
    }
}

從他的輸入來看, ThreadLocal是同一個, 數據存的是線程本身的名字, 因此和threadName是同樣的名稱shell

threadName = threadName9 [ threadLocal = java.lang.ThreadLocal@43745e1f     data = zhazhathreadName9 ]
threadName = threadName3 [ threadLocal = java.lang.ThreadLocal@43745e1f     data = zhazhathreadName3 ]
threadName = threadName7 [ threadLocal = java.lang.ThreadLocal@43745e1f     data = zhazhathreadName7 ]
threadName = threadName0 [ threadLocal = java.lang.ThreadLocal@43745e1f     data = zhazhathreadName0 ]
threadName = threadName6 [ threadLocal = java.lang.ThreadLocal@43745e1f     data = zhazhathreadName6 ]
threadName = threadName1 [ threadLocal = java.lang.ThreadLocal@43745e1f     data = zhazhathreadName1 ]
threadName = threadName2 [ threadLocal = java.lang.ThreadLocal@43745e1f     data = zhazhathreadName2 ]
threadName = threadName4 [ threadLocal = java.lang.ThreadLocal@43745e1f     data = zhazhathreadName4 ]
threadName = threadName5 [ threadLocal = java.lang.ThreadLocal@43745e1f     data = zhazhathreadName5 ]
threadName = threadName8 [ threadLocal = java.lang.ThreadLocal@43745e1f     data = zhazhathreadName8 ]

3. 有什麼使用場景

咱們使用獲取到一個保存數據庫請求, tomcat會有一個線程去操做數據庫保存數據和響應數據給客戶, 而操做數據庫須要存在一個數據庫連接Connection對象, 只要是同一個數據庫連接, 就能夠獲得同一個事務
但一個線程是如何獲取同一個Connection從而獲取同一個事務 ?
方法其實很簡單, 使用 ThreadLocal綁定在線程中, 相似於Map<Thread, Connection>去存儲數據庫

4. 底層源碼分析

get方法分析數組

public T get() {
    // 獲取當前線程
    Thread t = Thread.currentThread();
    // 獲取ThreadLocalMap
    ThreadLocal.ThreadLocalMap map = getMap(t);
    // map不爲null
    if (map != null) {
        // 根據this獲取咱們的entry
        ThreadLocal.ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result;
        }
    }
    // 若是map獲取爲空, 則初始化
    return setInitialValue();
}

根據上面源碼分析發現ThreadLocal底層使用的不是相似Map<Thread, Data> 這種結構而是

每一個線程都有一個屬於本身的ThreadLocalMap結構
而他的結構是這樣的緩存

其中的table數組在上面的 setInitialValue() 方法建立詳細源碼在這tomcat

private T setInitialValue() {
    // 這個方法在咱們的用例中沒寫, 因此默認放回 null
    T value = initialValue();
    Thread t = Thread.currentThread();
    // 獲取線程單獨的 ThreadLocalMap
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        // 若是咱們初始化了initialValue() 方法, 那麼它默認初始化的值會被設置到這裏, 
        // 可是實際上咱們用例爲null, 因此不會執行這段代碼
        map.set(this, value);
    } else {
        // 線程ThreadLocalMap 沒被建立, 須要建立出來, 
        // 其中的 table 數組在這裏被建立
        createMap(t, value);
    }
    // 這裏我沒分析, 忽略了
    if (this instanceof TerminatingThreadLocal) {
        TerminatingThreadLocal.register((TerminatingThreadLocal<?>) this);
    }
    return value;
}

他會在ThreadLocalMap中調用構造方法初始化網絡

// 其中 firstValue是咱們的值
void createMap(Thread t, T firstValue) {
    // 關注下 this , 它是ThreadLocal
    t.threadLocals = new ThreadLocalMap(this, firstValue);
}

ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
    // 咱們的table在這裏被建立, INITIAL_CAPACITY == 16
    table = new Entry[INITIAL_CAPACITY];
    // 獲取不超過16的hashCode
    int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
    // 根據計算出來的HashCode設置到對應的table數組中, 這裏key是ThreadLocal, value是咱們的值
    table[i] = new Entry(firstKey, firstValue);
    // 初始時, 已經有一個值了, 因此size = 1
    size = 1;
    // 設置擴容閾值加載因子 threshold = len * 2 / 3; 默認爲長度的三分之二
    setThreshold(INITIAL_CAPACITY);
}

從這段代碼能夠發現, firstKey實際上是咱們ThreadLocalMap中的key, 而firstKey就是咱們的ThreadLocal, 而value就是咱們 initialValue() 方法返回的值, 這裏默認爲null, 因此咱們能夠得出這樣一幅圖jvm

總結下
每一個線程都有一個屬於本身的 ThreadLocalMap類, 他用於關聯多個以 ThreadLocal對象爲 key, 以 你的數據valueEntry對象, 且該對象的 key是一個 弱引用對象

接下來咱們分析下這個類Entry, 它繼承了弱引用類WeakReferenceide

static class Entry extends WeakReference<ThreadLocal<?>> {
    Object value;

    Entry(ThreadLocal<?> k, Object v) {
        // ThreadLocal被設置爲弱引用
        super(k);
        // 保存value
        value = v;
    }
}

發現 ThreadLocal 被設置爲弱引用源碼分析

存在什麼問題?

爲何前面的Entry須要繼承弱引用類WeakReference呢?
首先了解下什麼是引用

簡單瞭解下強、軟、弱和虛引用

  • 強引用: 若是引用變量沒被指向null則, 引用對象將被停留在堆中, 沒法被虛擬機回收Object obj = new Object()
  • 軟引用: 若是虛擬機堆內存不夠用了(在發生內存溢出以前), 虛擬機能夠選擇回收軟引用對象, 虛擬機提供SoftReference類實現軟引用, 通常用於相對比較重要但又能夠不用的對象, 好比: 緩存
  • 弱引用: 生於系統回收以前, 死於系統回收完畢以後, 弱引用須要依附於強引用或者軟引用纔可以防止被虛擬機回收, 好比放到一個引用隊列(ReferenceQueue)中或者對象中, 好比: ThreadLocalMapEntry對象, 須要依附於ThreadLocal纔可以不被刪除掉
  • 虛引用: 能夠理解爲跟強引用對象沒了引用變量同樣, 隨時能夠被回收, 只要依附於引用隊列中才不會被回收, 一般用於網絡通信的NIO上, 用於引用直接內存, java提供類PhantomReference來實現虛引用

爲什麼Entry對象須要爲弱引用?

答案很明顯, 防止內存泄漏[1], 咱們來詳細分析分析
首先, 咱們知道ThreadLocalMap中存放的是一個一個Entry對象, 而 Entry對象中的key(ThreadLocal)被設計成弱引用若是key被設置成null
(好比: 外部的測試用例中的private static final ThreadLocal<String> threadLocal = new ThreadLocal<>();這個對象被設置爲 threadLocal = null) 則, 你會發現此時Entry的對象key = null value = xxxx(此時這個Entry實質上是沒有用的, 連key都給設置成null, 它的value還有什麼用?) 而ThreadLocalMap中存儲的仍是Entry對象的地址, 此Entry不會被回收, 但Entry對象的key被設置成弱引用, 就不同了, 直接會被回收掉它

[1]內存泄漏: 程序中己動態分配的堆內存因爲某種緣由程序未釋放或沒法釋放,形成系統內存的浪費,致使程序運行速度減慢甚至系統崩潰等嚴重後果

那麼這樣就沒有問題了麼???(打臉篇)

再次強調, 下面這段話別信, 仔細看到最後, 你會發現這被打臉了

其實應該是沒什麼問題了(__被本身打臉了, 別信這句話__), 只不過不少網友以爲Entry中的key雖然是弱引用, 但Entry可能不會被回收, 由於entryvalue是強引用, 可能致使線程下的entry沒法被回收掉, 最好推薦使用threadLocal.remove方法刪除掉, 前面說的threadLocal = null方法不推薦使用, 那麼爲了以防萬一吧, 仍是手動調用下remove方法比較好一點

下面是我對threadLocal = null方式的代碼測試:

public class ThreadLocalDemo {
    
    private static ThreadLocal<String> threadLocal1 = new ThreadLocal<>() {
        @Override
        protected String initialValue() {
            return "1";
        }
        
        @Override
        protected void finalize() throws Throwable {
            super.finalize();
            System.out.println("threadLocal1被回收");
        }
    };
    private static ThreadLocal<String> threadLocal2 = new ThreadLocal<>() {
        @Override
        protected String initialValue() {
            return "2";
        }
        
        @Override
        protected void finalize() throws Throwable {
            super.finalize();
            System.out.println("threadLocal1被回收");
        }
    };
    
    public static void main(String[] args) throws InterruptedException, NoSuchFieldException, IllegalAccessException {
        // 獲取ThreadLocalMap
        Thread thread = Thread.currentThread();
        Class<? extends Thread> clazz = thread.getClass();
        Field threadLocals = clazz.getDeclaredField("threadLocals");
        threadLocals.setAccessible(true);
        Object threadLocalsObj = threadLocals.get(thread);
        // 獲取ThreadLocalMap下的table數組
        Class<?> threadLocalsMapClass = threadLocalsObj.getClass();
        Field tableField = threadLocalsMapClass.getDeclaredField("table");
        tableField.setAccessible(true);
        Object[] tableObj = (Object[]) tableField.get(threadLocalsObj);
        threadLocal1.set("zhazha");
        threadLocal2.set("xixi");
        System.out.println(threadLocal1.get());
        System.out.println(threadLocal2.get());
        // 在這裏下一個斷點看看ThreadLocal被回收, Entry是否被回收
        threadLocal1 = null;
        threadLocal2 = null;
        System.gc();
        Thread.sleep(5000);
        System.out.println(tableObj);
        System.out.println("主線程結束");
    }
}

輸出是這樣的:

WARNING: An illegal reflective access operation has occurred
WARNING: Illegal reflective access by com.zhazha.threadlocal.ThreadLocalDemo (file:/D:/program/codes/java/Concurrentcy/reviewjuc/target/classes/) to field java.lang.Thread.threadLocals
WARNING: Please consider reporting this to the maintainers of com.zhazha.threadlocal.ThreadLocalDemo
WARNING: Use --illegal-access=warn to enable warnings of further illegal reflective access operations
WARNING: All illegal access operations will be denied in a future release
zhazha
xixi
[Ljava.lang.ThreadLocal$ThreadLocalMap$Entry;@aecb35a
主線程結束

若是上面的代碼不調用gc方法, 很長一段時間內不會被回收, 應該是jvm gc還沒開始被動回收

但!!!但!!!但!!! 看調試代碼

數組中的referent字段仍是存在的, 下圖是gc回收以前查看數組中的元素髮現, 字段referent(也就是ThreadLocal) 它還在

gc方法執行完畢後, referent被回收掉了, referent = null

可是那個對象怎麼回事??? 沒被回收掉?? 打臉了??? 求助廣大網友給我看看

那讓咱們試試 remove方法試試?

好了, 直接沒了, 找不到那兩個屬性了

An illegal reflective access operation has occurred這個問題怎麼幫? 這回真不知道了, 應該不影響咱們的代碼麼?

算了爲了把這個紅色的字改沒掉, 改了改源碼

public class ThreadLocalDemo {
    
    private static ThreadLocal<String> threadLocal1 = new ThreadLocal<>() {
        @Override
        protected String initialValue() {
            return "1";
        }
        
        @Override
        protected void finalize() throws Throwable {
            super.finalize();
            System.out.println("threadLocal1被回收");
        }
    };
    private static ThreadLocal<String> threadLocal2 = new ThreadLocal<>() {
        @Override
        protected String initialValue() {
            return "2";
        }
        
        @Override
        protected void finalize() throws Throwable {
            super.finalize();
            System.out.println("threadLocal1被回收");
        }
    };
    
    private static Unsafe unsafe;
    
    static {
        Class<Unsafe> unsafeClass = Unsafe.class;
        Unsafe unsafe = null;
        try {
            Field unsafeField = unsafeClass.getDeclaredField("theUnsafe");
            unsafeField.setAccessible(true);
            ThreadLocalDemo.unsafe = (Unsafe) unsafeField.get(null);
        } catch (NoSuchFieldException | IllegalAccessException e) {
            e.printStackTrace();
        }
    }
    
    public static void main(String[] args) throws InterruptedException, NoSuchFieldException {
        Thread thread = Thread.currentThread();
        long threadLocalsFieldOffset = unsafe.objectFieldOffset(Thread.class.getDeclaredField("threadLocals"));
        Object threadLocalMapObj = unsafe.getObject(thread, threadLocalsFieldOffset);
        long tableOffset = unsafe.objectFieldOffset(threadLocalMapObj.getClass().getDeclaredField("table"));
        Object tableObj = unsafe.getObject(threadLocalMapObj, tableOffset);
        threadLocal1.set("zhazha");
        threadLocal2.set("xixi");
        System.out.println(threadLocal1.get());
        System.out.println(threadLocal2.get());
        threadLocal1 = null;
        threadLocal2 = null;
        // threadLocal1.remove();
        // threadLocal2.remove();
        System.gc();
        System.out.println(tableObj);
        System.out.println("主線程結束");
    }
}

好了沒這個問題了

zhazha
xixi
threadLocal1被回收
threadLocal1被回收
[Ljava.lang.ThreadLocal$ThreadLocalMap$Entry;@7dc222ae
主線程結束
與目標VM斷開鏈接, 地址爲: ''127.0.0.1:58958',傳輸: '套接字'', 傳輸: '{1}'

進程已結束,退出代碼0
相關文章
相關標籤/搜索