簡析線程上下文ThreadLocal

在這裏插入圖片描述

前言

在有些時候,單個線程執行任務非常多的時候,後一個步輸入是前一個步驟的輸出,我們有時候會採用責任鏈模式,但是調用鏈路長了以後,這種傳參方式
會顯得冗餘,於是就有了線程上下文的設計,每個線程會有不同的參數實例。原因是每個線程Thread.currentThread()作爲key,這樣就可以保證線程的
獨立性。

需要注意的是,我們會一般用Map存儲,用當前的線程作爲key,當線程生命週期結束後,Map中的實例並不會釋放,因爲還是根可達的,久而久之,會產生內存溢出。

ThreadLocal 簡單介紹

在展開深入分析之前,咱們先來看一個官方示例:


public class ThreadId {

    // Atomic integer containing the next thread ID to be assigned
    private static final AtomicInteger nextId = new AtomicInteger(0);

    // Thread local variable containing each thread's ID
    private static final ThreadLocal<Integer> threadId =
        new ThreadLocal<Integer>() {
            @Override
            protected Integer initialValue() {
                return nextId.getAndIncrement();
            }
        };

    // Returns the current thread's unique ID, assigning it if necessary
    public static int get() {
        return threadId.get();
    }

    public static void main(String[] args) {
        for (int i = 0; i < 5; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    System.out.println("threadName=" + Thread.currentThread().getName() + ",threadId=" + ThreadId.get());
                }
            }).start();
        }
    }

}

運行如下,

threadName=Thread-2,threadId=0
threadName=Thread-3,threadId=1
threadName=Thread-0,threadId=2
threadName=Thread-1,threadId=3
threadName=Thread-4,threadId=4

可以看到每個線程不會相互影響,每個線程存入threadLocal的值是完全互相獨立的。

我再把類註釋放上去

 * This class provides thread-local variables.  These variables differ from
 * their normal counterparts in that each thread that accesses one (via its
 * {@code get} or {@code set} method) has its own, independently initialized
 * copy of the variable.  {@code ThreadLocal} instances are typically private
 * static fields in classes that wish to associate state with a thread (e.g.,
 * a user ID or Transaction ID).

大概意思就是幾條:

  • 它能讓線程擁有了自己內部獨享的變量

  • 每一個線程可以通過get、set方法去進行操作

  • 可以覆蓋initialValue方法指定線程獨享的值

  • 通常會用來修飾類裏private static final的屬性,爲線程設置一些狀態信息,例如user ID或者Transaction ID

  • 每一個線程都有一個指向threadLocal實例的弱引用,只要線程一直存活或者該threadLocal實例能被訪問到,都不會被垃圾回收清理掉

ThreadLocal常用方法介紹

ThreadLocal方法有如下:

ThreadLocal、
childValue、
createInheritedMap、
createMap、
get、
getMap、
initialValue、
nextHashCode、
remove、
set、
setInitialValue、
withInitial、
HASH_INCREMENT、
nextHashCode、
threadLocalHashCode

ThreadLocal常用方法有initialValue、get、set、remove。

initialValue


    protected T initialValue() {
        return null;
    }

就是在爲每個線程變量賦一個默認值,如果沒有設的話,默認爲null。

set

就是如果沒有調用set方法,數據的初始值就是initialValue的值。


   public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }

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

   private void set(ThreadLocal<?> key, Object value) {

            // We don't use a fast path as with get() because it is at
            // least as common to use set() to create new entries as
            // it is to replace existing ones, in which case, a fast
            // path would fail more often than not.

            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)]) {
                ThreadLocal<?> k = e.get();

                if (k == key) {
                    e.value = value;
                    return;
                }

                if (k == null) {
                    replaceStaleEntry(key, value, i);
                    return;
                }
            }

            tab[i] = new Entry(key, value);
            int sz = ++size;
            if (!cleanSomeSlots(i, sz) && sz >= threshold)
                rehash();
    }

  • 獲取當前現成的Thread.currentThread()

  • 根據當前線程獲取到線程的ThreadLocalMap,可以看下面Thread源碼,
    每個線程有一個ThreadLocalMap的引用,先記下,後面有用。


public
class Thread implements Runnable {
/* ThreadLocal values pertaining to this thread. This map is maintained
     * by the ThreadLocal class. */
    ThreadLocal.ThreadLocalMap threadLocals = null;
}

  • 如果沒有獲取到就創建,獲取到就賦值

  • 先說下這個創建map的方法,首先會new一個ThreadLocalMap,創建entry,用當前的ThreadLocal作爲key,要存的數據作爲value,結束

  • 如果map存在,就遍歷entry,找到如果有相同的ThreadLocal,那就用新的值替換掉,結束。

  • 如果沒有找到相同的ThreadLocal,則創建entry,用當前的ThreadLocal作爲key,要存的數據作爲value

注意,如果遍歷過程中,entry中key的值爲null,那就用新的key替代,還會根據當前的size和閾值進行比較,超過閾值,則進行key值爲null的清理。

get


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

    private T setInitialValue() {
        T value = initialValue();
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
        return value;
    }

  • 獲取當前線程內部的ThreadLocalMap

  • map存在則獲取當前ThreadLocal對應的value值

  • map不存在或者找不到value值,則調用setInitialValue,進行初始化

看看ThreadLocalMap


    static class Entry extends WeakReference<ThreadLocal<?>> {
        /** The value associated with this ThreadLocal. */
        Object value;

        Entry(ThreadLocal<?> k, Object v) {
            super(k);
            value = v;
        }
    }

EntryWeakReference類型的,Java垃圾回收時,看一個對象需不需要回收,就是看這個對象是否可達。什麼是可達,
就是能不能通過引用去訪問到這個對象。

jdk1.2以後,引用就被分爲四種類型:強引用、弱引用、軟引用和虛引用。強引用就是我們常用的Object obj = new Object(),obj就是一個強引用,指向了對象內存空間。當內存空間不足時,Java垃圾回收程序發現對象有一個強引用,寧願拋出OutofMemory錯誤,也不會去回收一個強引用的內存空間。而弱引用,即WeakReference,意思就是當一個對象只有弱引用指向它時,垃圾回收器不管當前內存是否足夠,都會進行回收。反過來說,這個對象是否要被垃圾回收掉,取決於是否有強引用指向。ThreadLocalMap這麼做,是不想因爲自己存儲了ThreadLocal對象,而影響到它的垃圾回收,而是把這個主動權完全交給了調用方,一旦調用方不想使用,設置ThreadLocal對象爲null,內存就可以被回收掉。

看個例子


public class MemoryLeak {

    public static void main(String[] args) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 100000; i++) {
                    TestClass t = new TestClass(i);
                    t.printId();
                    t = null;
                }
            }
        }).start();
    }


    static class TestClass{
        private int id;
        private int[] arr;
        private ThreadLocal<TestClass> threadLocal;
        TestClass(int id){
            this.id = id;
            arr = new int[1000000];
            threadLocal = new ThreadLocal<>();
            threadLocal.set(this);
        }

        public void printId(){
            System.out.println(threadLocal.get().id);
        }
    }
}

運行的結果爲: java.lang.OutOfMemoryError: Java heap space

稍作修改

public class MemoryLeak {

    public static void main(String[] args) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 100000; i++) {
                    TestClass t = new TestClass(i);
                    t.printId();
                    t.threadLocal.remove();
                }
            }
        }).start();
    }


    static class TestClass{
        private int id;
        private int[] arr;
        private ThreadLocal<TestClass> threadLocal;
        TestClass(int id){
            this.id = id;
            arr = new int[1000000];
            threadLocal = new ThreadLocal<>();
            threadLocal.set(this);
        }

        public void printId(){
            System.out.println(threadLocal.get().id);
        }
    }
}

正常完成,比代碼只有一處不同:t = null改爲了t.threadLocal.remove();

我們來看下remove的源碼


     public void remove() {
         ThreadLocalMap m = getMap(Thread.currentThread());
         if (m != null)
             m.remove(this);
     }

就一句話,獲取當前線程內部的ThreadLocalMap,存在則從map中刪除這個ThreadLocal對象。

分析下爲什麼?

   ThreadLocalMap.class

    static class Entry extends WeakReference<ThreadLocal<?>> {
        /** The value associated with this ThreadLocal. */
        Object value;

        Entry(ThreadLocal<?> k, Object v) {
            super(k);
            value = v;
        }
    }

Entry中的key指向的是ThreadLocal,說明key是弱引用,當線程的週期結束後,會自動回收,也就是key這時爲null,但是value是Object,這個是強引用,沒有辦法回收,這也是之前看set方法源碼的時候,會清除key爲null的數據,我們在
實際應用的時候記得線程結束後,調用remove方法。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章