天天看點

ThreadLocal失效

         在JDK中,解決線程沖突問題,有兩種解決方案:l  給臨界區加鎖;l  本地化臨界區。

第一種解決方案的典型代表是Synchonized。第二種的典型代表是ThreadLocal。而CopyOnWrite是這兩種方案的融合。

         ThreadLocal為每個線程的并發通路資料建立一個副本,通過對副本的操作來隔離臨界區的污染。雖然增加了記憶體空間的消耗,但大大減少了線程同步所帶來性能消耗,也減少了線程并發控制的複雜度。

         當然ThreadLocal不能完全解決并發的問題,因為它隻是隔離了臨界區,抛開了臨界區的同步問題,導緻多個副本的存在。是以使用的時候要因地制宜,符合自身的邏輯。

Demo

         ThreadLocal的使用很簡單,如下所示,先聲明一個ThreadLocal的私有變量。然後線上程啟動的時候指派set(),在使用的時候再get()出來。

private ThreadLocal local = new ThreadLocal();
public void run() {
    local.set(objLocal);
    // ......
    local.get();
           

         也可以通過覆寫初始值的方法initialValue()來實作指派。

private ThreadLocal local = new ThreadLocal() {
    @Override
    protected ObjectinitialValue() {
        // ......
        return XXX;
    }
};
           

Code

         那ThreadLocal是如何能實作本地化臨界區的功能呢?我們到JDK一看究竟。

         線上程類Thread中,通過一個Map屬性threadLocals來存放臨界區的資料。我暫且稱之為本地資料區。   這個Map的key為ThreadLocal執行個體,value為臨界區的資料值。

         這個ThreadLocalMap沒有實作Map接口,但是也用到了散清單來組織資料。

ThreadLocal.ThreadLocalMap threadLocals = null;
           

         接着,我們看到ThreadLocal取值get()時候,需要傳入目前線程t來擷取本地資料區t.threadLocals。擷取到資料區後,使用ThreadLocal執行個體作為key來取值。

public T get() {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null)
                return (T)e.value;
        }
        return setInitialValue();
    }
    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }
           

         同樣,指派set()也是差不多的邏輯。如果t.threadLocals不存在就建立一個ThreadLocalMap對象。

         這裡有個需要注意的點,擷取本地資料區是要傳入目前線程的,因為threadLocals是挂在具體的Thread對象上。是以set()需要線上程運作的時候進行,否則就會導緻傳入主線程,取到了主線程的本地資料區。

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);
    }
           

         最後,我們發現ThreadLocal指派到本地資料區的時候,會建立一個Entry來存放:key為ThreadLocal執行個體,value為副本值。這裡其實隻是把value的值賦過來,沒有做clone的處理。

         是以需要加倍注意的是,此處隻是值拷貝,沒做clone(深度拷貝)。如果傳進來的是個引用,就不能做到隔離了。

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

P.S 隔離失效

         從上面ThreadLocal源碼可以看出:

1)      ThreadLocal賦初始值的時候,需要線上程運作中,即run()中,否則ThreadLocalMap會挂錯線程;

2)      使用ThreadLocal隔離的值不能是引用,否則隔離的隻是引用,而引用所指向的對象則隔離失敗;

3)      本地資料區ThreadLocalMap是挂在Thread對象上的,是以要注意線程複用(線程池)所帶來的污染。

set引用

         下面程式先建立一個SHARE_LIST作為臨界區,然後建立兩個線程同時去做修改。

         給每個線程引入ThreadLocal屬性試圖本地化SHARE_LIST來隔離多線程的沖突。

         根據ThreadLocal的設計初衷,應該是在各個Thread建立自己的本地資料區,互不影響。後來卻發現SHARE_LIST被污染了,所有線程的修改都寫入了同一個SHARE_LIST。最後,主線程和另外兩個線程輸出的結果都是一樣的。

         這裡ThreadLocal隻是本地化(隔離)了引用值,而沒有本地化引用的對象本身,是以出現了這種現象。

public static List<String>SHARE_LIST = new CopyOnWriteArrayList<String>();
    public static void main(String[]args) throws Exception {
        SHARE_LIST.add("Int");
        System.out.println("Change before : " + SHARE_LIST);
        new MyThread2("Tom").start();
        new MyThread2("Jack").start();
        System.out.println("Change after :");
 
        Thread.currentThread().sleep(500);
        System.out.println(Thread.currentThread() + ":" + SHARE_LIST);
    }
 
    static class MyThread2 extends Thread {
        privateThreadLocal<List<String>> local = new ThreadLocal<List<String>>();
        private String name;
 
        public MyThread2(Stringname) {
            this.name = name;
        }
 
        @Override
        public void run() {
            local.set(SHARE_LIST);
            local.get().add(name);   // Change the parameter locally.
            try {
                Thread.currentThread().sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread() + ":" + local.get());
        }
    }
           

挂錯線程

         把上面的代碼稍作修改,把ThreadLocal的賦初始值放在建立線程執行個體的構造函數中。執行程式後,發現擷取本地資料區local.get()的時候抛出了NullPointerException。

         這是因為線程建立的時候還在主線程main中,這個時候指派set()就會把資料放到了main的本地資料區;而到了子線程run()的時候,擷取本地資料區get()取的是子線程的,是以就會抛空指針。

public static void main(String[]args) throws InterruptedException {
        SHARE_LIST.add("Int");
        System.out.println("Change before : " + SHARE_LIST);
        new MyThread3("Tom", SHARE_LIST).start();
        new MyThread3("Jack", SHARE_LIST).start();
        System.out.println("Change after :");
 
        Thread.currentThread().sleep(500);
        System.out.println(Thread.currentThread() + ":" + SHARE_LIST);
    }
 
    static class MyThread3 extends Thread {
        privateThreadLocal<List<String>> local = new ThreadLocal<List<String>>();
        private String name;
 
        public MyThread3(String name, List<String>list) {
            this.name = name;
            local.set(list);
        }
 
        @Override
        public void run() {
            local.get().add(name); // Changethe parameter locally.
            try {
                Thread.currentThread().sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread() + ":" + local.get());
        }
    }
           

繼續閱讀