再看ThreadLocal

ThreadLocal,網上文章很多,大家也基本都會使用,但是不一定用的好,或者說不一定真的能理解

ThreadLocal

ThreadLocal本身並不作爲存儲的容器,而是把值存放在當前線程中的變量裏面,Thread類裏如下:

    /* 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;

有人會奇怪爲什麼會有2個對象,threadLocals就是我們一般情況使用的,而inheritableThreadLocals是供在當前線程又開了一個子線程時用的,這樣可以使子線程也可以使用當前線程的threadLocals對象,需要配合使用ThreadLocal的子類InheritableThreadLocal使用,具體看下一節

threadLocals爲空,翻遍了Thread內的源碼,你也找不到給他賦值的地方,因爲賦值的地方在ThreadLocal這個類裏面:

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

如上所示,給當前thread的threadLocals變量賦值,並且key是this,也就是當前的ThreadLocal對象

注意到使用的是ThreadLocalMap,這個類很像HashMap,但是它並未實現任何接口,這個類在ThreadLocal裏面,雖然定義成成static class ThreadLocalMap,但是它的方法都是private的,意味着這個類只供ThreadLocal使用

其內部使用自定義的Entry對象來存儲數據,Entry類如下:

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

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

繼承了WeakReference,看構造方法,可以知道key是WeakReference類型,value是普通的強引用

這意味着,如果沒有其他引用,那麼線程結束後,key會被自動回收,也即ThreadLocal會被回收,看起來很完美

但是一般情況下,我們的使用方法,都是類似如下:

public abstract class ThreadContext {

    private static final ThreadLocal<Map<Object, Object>> resources = new InheritableThreadLocalMap<>();

}

使用static修飾,那麼即使線程結束,依然有引用,所以不會被回收

而且,很多時候我們使用線程池,可能線程永遠都不會結束,那麼ThreadLocal對象也就永遠不會被回收

上述兩種情況都有可能發生內存泄漏,而且會出現邏輯錯亂的現象,所以最佳實踐就是:在使用完後,顯示的調用ThreadLocal的remove方法

InheritableThreadLocal

InheritableThreadLocal的一般使用方法如下:

public class Test {
 
    public static ThreadLocal<Integer> threadLocal = new InheritableThreadLocal<Integer>();
 
    public static void main(String args[]){
        threadLocal.set(new Integer(123));
 
        Thread thread = new MyThread();
        thread.start();
 
        System.out.println("main = " + threadLocal.get());
    }
 
    static class MyThread extends Thread{
        @Override
        public void run(){
            System.out.println("MyThread = " + threadLocal.get());
        }
    }
}

輸出:

main = 123

MyThread = 123

子線程完美的使用了父線程的對象,這是怎麼實現的呢?

InheritableThreadLocal類很簡單,源碼如下:

public class InheritableThreadLocal<T> extends ThreadLocal<T> {

    protected T childValue(T parentValue) {
        return parentValue;
    }

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

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

繼承了ThreadLocal,覆蓋了父類的getMap方法,返回的是Thread類的inheritableThreadLocals成員變量,也就是我們之前提到的Thread累裏面的另一個成員變量

同時也覆蓋了createMap方法,賦值的也是父類的inheritableThreadLocals成員變量

可以看到InheritableThreadLocal裏面操作的都是父類的inheritableThreadLocals了,和Thread的成員變量threadLocals沒有任何關係了,這裏其實用到了模板模式,父類定義了一個流程,子類按照父類的框架執行一個定義好的流程,只是一些細節可以有自己的實現

我們看一下Thread的構造方法,會調用各種init方法,這些init方法最終都是調用下面的init方法,而且最後一個參數inheritThreadLocals爲true(嚴謹一點,Thread(Runnable target, AccessControlContext acc)這個構造方法調用init方法的inheritThreadLocals爲false,但是該方法不是public的)

private void init(ThreadGroup g, Runnable target, String name,
                      long stackSize, AccessControlContext acc,
                      boolean inheritThreadLocals) {
  ...
    Thread parent = currentThread();
    if (inheritThreadLocals && parent.inheritableThreadLocals != null)
            this.inheritableThreadLocals =
                ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
  ...
}

static ThreadLocalMap createInheritedMap(ThreadLocalMap parentMap) {
        return new ThreadLocalMap(parentMap);
}

和inheritableThreadLocals相關的代碼如上,如果inheritThreadLocals爲true,並且當前線程的inheritableThreadLocals值不爲空,那麼給inheritableThreadLocals賦值,我們再看一下調用的new ThreadLocalMap構造方法

private ThreadLocalMap(ThreadLocalMap parentMap) {
            Entry[] parentTable = parentMap.table;
            int len = parentTable.length;
            setThreshold(len);
            table = new Entry[len];

            for (int j = 0; j < len; j++) {
                Entry e = parentTable[j];
                if (e != null) {
                    @SuppressWarnings("unchecked")
                    ThreadLocal<Object> key = (ThreadLocal<Object>) e.get();
                    if (key != null) {
                        Object value = key.childValue(e.value);
                        Entry c = new Entry(key, value);
                        int h = key.threadLocalHashCode & (len - 1);
                        while (table[h] != null)
                            h = nextIndex(h, len);
                        table[h] = c;
                        size++;
                    }
                }
            }
        }

可看到,是一個遍歷賦值的操作,看過HashMap源碼的都知道,裏面的Entry是一個鏈表,因爲會有key重複的情況,這裏爲什麼沒有呢?

大家看16,17行,如果位置爲h的已經有值了,那麼死循環,重新生成位置h,直到該位置沒有值,所以不需要鏈表

這樣子線程就有用父線程的對象了,需要注意的是,第13行,Object value = key.childValue(e.value);

這個方法在ThreadLocal裏如下:

T childValue(T parentValue) {
        throw new UnsupportedOperationException();
    }

因爲這個方法就不是給他用的,而是給他的子類InheritableThreadLocal用的,該方法在InheritableThreadLocal覆蓋如下:

protected T childValue(T parentValue) {
        return parentValue;
    }

只是簡單的返回參數,一般情況下夠用了,但是如果你的需求是想子線程和父線程的值可以各自修改而不受影響,那麼你們可以繼承InheritableThreadLocal這個類,覆蓋其childValue方法,在該方法裏你就可以進行深度拷貝的操作,注意是深度拷貝

其他

通過如上分析,也可以看出來

  1. 可以再同一個線程裏同時使用ThreadLocal和InheritableThreadLocal,他們對應的是Thread裏的不同變量,互不影響,只是InheritableThreadLocal可以被子類繼承
  2. 可以使用多個ThreadLocal對象,互不影響,因爲源碼裏的ThreadLocalMap的key是ThreadLocal本身,所以,很多人沒看ThreadLocal源碼前,可能會以爲key是當前Thread,如果是的話,就不能同時使用多個ThreadLocal對象了,後面的覆蓋前面的
  3. 使用InheritableThreadLocal時可以覆蓋childValue方法,進行深度拷貝
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章