InheritableThreadlocal使用問題排查


背景

在做一個微服務系統的時候,我們的參數一般都是接在通過方法定義來進行傳遞的,類似這樣

public void xxx(Param p, ...){
	// do something
}

然后這時有個模塊,因為之前的設計原因,沒有預留傳遞參數的形式,在本着盡可能不修改原來代碼的情況下,決定通過InhertableThreadLocal來進行參數傳遞

InhertableThreadLocal

對於InhertableThreadLocal我們不陌生,其實它的思想是以空間來換取線性安全,對每個線程保留一份線程內私有的變量。
這個類一般是用於存在父子線程的情況下,那么在父子線程中,是怎么工作的?結合源碼來簡單認識下

下面這段代碼是從jdk的Thread中摘取的,我們可以看到,每個被創建出來的線程,都有2個threadlocal,分別對應同名的類

    /* ThreadLocal values pertaining to this thread. This map is maintained
     * by the ThreadLocal class. */
    ThreadLocal.ThreadLocalMap threadLocals = null;

    /*
     * InheritableThreadLocal values pertaining to this thread. This map is
     * maintained by the InheritableThreadLocal class.
     */
    ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;

一開始的時候inheritableThreadLocals是null的,需要在InhertableThreadLocal調用createMap的時候來初始化。
createMap在setInitialValue()當中會被調用,而setInitialValue被get調用

// ThreadLocal.java
    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;
    }
    
    
    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();
    }
    
// InheritableThreadLocal.java
    /**
     * Get the map associated with a ThreadLocal.
     *
     * @param t the current thread
     */
    ThreadLocalMap getMap(Thread t) {
       return t.inheritableThreadLocals;
    }

    /**
     * Create the map associated with a ThreadLocal.
     *
     * @param t the current thread
     * @param firstValue value for the initial entry of the table.
     */
    void createMap(Thread t, T firstValue) {
        t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
    }


一般我們創建InheritableThreadLocal會重寫初始化的方法,類似如下

ThreadLocal<Map<String,Integer>> context = new InheritableThreadLocal<Map<String,Integer>>(){
            @Override
            protected Map<String,Integer> initialValue() {
                System.out.println(Thread.currentThread().getName() + " init value");
                return new HashMap<>();
            }
        };

看到這里估計開始迷糊了,但是只要記住,父子線程的傳遞是通過ThreadLocal.ThreadLocalMap inheritableThreadLocals這個關鍵的成員變量來實現的。
上面講的其實是父線程怎么創建這個成員變量,那么子線程怎么獲取呢?

從線程池中創建線程,或者普通的創建線程,最終都會調用到這個方法

   private void init(ThreadGroup g, Runnable target, String name,
                      long stackSize, AccessControlContext acc,
                      boolean inheritThreadLocals) {
   		//前面省略
   		        if (inheritThreadLocals && parent.inheritableThreadLocals != null)
            this.inheritableThreadLocals =
 ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
   
   }
   		// 后面忽略

注意到這個變量了嗎boolean inheritThreadLocals 這個就是決定是否是要繼承父線程中的inheritableThreadLocals,前提自然是不能為null。

一般的線程new Thread()這個變量是true,也就是繼承父線程中存放的變量。而線程池,默認使用DefaultThreadFactorynewThread(Runnable r)方法,也是如此

到這里就完成了傳遞,解釋了為什么子線程可以得到父線程上set的變量了

回到問題開始

在簡單的介紹完了如何實現變量的傳遞后,我們來看看一開始的問題,測試的代碼如下

   @Test
    public void ParentChildThread(){
        ThreadLocal<Map<String,Integer>> context = new InheritableThreadLocal<Map<String,Integer>>(){
            @Override
            protected Map<String,Integer> initialValue() {
                System.out.println(Thread.currentThread().getName() + " init value");
                return new HashMap<>();
            }
        };

        final String TEST_KEY = "tt";

        class ChildThread implements Runnable{
            @Override
            public void run() {
                try{
                    System.out.println(Thread.currentThread().getName());
                    int a = context.get().get(TEST_KEY);;
                    System.out.println(a);
                }
                finally {
                    // 注意這里
                    context.remove();
                }

            }
        }

        ExecutorService executorService = Executors.newFixedThreadPool(1);
        String tname = Thread.currentThread().getName();

        int c = 0;
        try {
            while(c++ < 2) {
                System.out.printf("%s ======== %d ========\n", tname, c);

                System.out.println(Thread.currentThread().getName() + " set");
                // 第一次這里會觸發createMap
                // 這里這里存放的是c
                context.get().put(TEST_KEY, c);

                executorService.execute(new ChildThread());

                System.out.println(Thread.currentThread().getName() + " remove");
                
                TimeUnit.MILLISECONDS.sleep(5000L);
                context.remove();
            }
            // 驗證在線程池中remove會不會影響父線程的值,以此來判斷是否需要在父線程中remove

        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

main線程來模擬spring的線程池,因此需要放在一個循環中,重復的set和remove,子線程來模擬我在多線程環境下獲取參數,因為在線程池中,所以需要記得remove,避免因為線程池復用的關系,而導致參數不對

讓我們來調試一下,輸出的信息如下

Connected to the target VM, address: '127.0.0.1:46617', transport: 'socket'
main ======== 1 ========
main set
main init value
main remove
pool-1-thread-1
0
main ======== 2 ========
main set
main init value
main remove
pool-1-thread-1
pool-1-thread-1 init value
Exception in thread "pool-1-thread-1" java.lang.NullPointerException
	at com.cnc.core.utils.CommonUtilTest$1ChildThread.run(CommonUtilTest.java:43)
	at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
	at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
	at java.lang.Thread.run(Thread.java:748)

從第一次的使用來看,ok,似乎沒有問題,看第二次,怎么報錯了,比較第一次和第二次,我們發現,因為在子線程中使用了remove,因此第二次需要重新進行初始化pool-1-thread-1 init value,畢竟我們已經remove了,所以肯定是需要重新初始化的,這個沒有問題

注意到沒這里線程池只有1個線程,這么做的原因是簡化情景,因為實際的情況是32個線程,NPE的錯誤是在一定請求之后發生的

這個錯誤的發生,其實是在復用了之前的線程才出現的,也就是之前線程使用了remove后,就會出現這樣的問題。why?

因為我們InheritableThreadLocal中存的是map,這個是父線程變量的拷貝

        class ChildThread implements Runnable{
            @Override
            public void run() {
                try{
                    System.out.println(Thread.currentThread().getName());
                    int a = context.get().get(TEST_KEY);;
                    System.out.println(a);
                }
                finally {
                    // 把這里注釋掉
//                    context.remove();
                }

            }
        }

注釋上面是保證不再出現異常,我們看看控制台輸出

main ======== 1 ========
main set
main init value
pool-1-thread-1
1
main remove
main ======== 2 ========
main set
main init value
pool-1-thread-1
1
main remove

發現了沒有,輸出的始終是1,我們注意看main線程也有在remove,這其實是切斷了與子線程的聯系

解決措施

根據上面的分析我們知道了,父子線程通過inheritableThreadLocals來進行變量的共享,根據我們設置的容器是map,其實不需要調用remove,而只要把map的內容清空即可,效果是一樣的,因此,下面這個可以實現我們的需求

context.remove(); --> context.get().clear()

運行測試,,這里我多測試了幾個

main ======== 1 ========
main set
main init value
pool-1-thread-1
1
main remove
main ======== 2 ========
main set
pool-1-thread-1
2
main remove
main ======== 3 ========
main set
pool-1-thread-1
3
main remove
main ======== 4 ========
main set
pool-1-thread-1
4
main remove
main ======== 5 ========
main set
pool-1-thread-1
5
main remove
main ======== 6 ========
main set
pool-1-thread-1
6
main remove


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM