神來之筆-線程變量實踐

ThreadLocal作用

ThreadLocal是用於存儲線程在任務執行過程便於獲取參數和設置參數的線程變量,它是跟線程緊密相關的;它的出現是爲了解決多線程環境下的變量訪問併發問題;所以單線程的任務或進程用不着它,直接用全局實例變量或類變量即可。

ThreadLocal實例

線程變量於我而言,像是開啓進入JAVA世界另一空間的鑰匙。

package com.zte.sunquan.demo.thread.local;

/**
 * ThreadLocalTest class
 *
 * @author 10184538
 * @date 2019/2/21
 */
public class ThreadLocalTest {
    private static final ThreadLocal<String> nameThreadLocal = new ThreadLocal<String>() {
        @Override
        protected String initialValue() {
            return "sunquan";
        }
    };

    public static void main(String[] args) throws InterruptedException {
        nameThreadLocal.set("abc");
        Thread th1 = new Thread(() -> {
            //sunquan
            System.out.println(Thread.currentThread().getName() + ":" + nameThreadLocal.get());
        });
        th1.start();
        //abc
        System.out.println(Thread.currentThread().getName() + ":" + nameThreadLocal.get());
        Thread.sleep(1000);
    }
}

打印結果:

main:abc
Thread-0:sunquan

上面示例中定義了線程變量nameThreadLocal,在main線程中設置了變量值爲abc,從thread-0打印的結果sunquan說明ThreadLocal線程變量在線程間相互隔離互不干擾。

子線程共享父線程的變量

在系統開發中如何實現子父線程變量共享,如上面的示例,thread-0是main線程的子線程。

對Java中的線程,父線程的概念,只是一種邏輯稱呼,創建線程的當前線程就是新線程的父線程,新線程的一些資源來自於這個父線程

JDK中提供了InheritableThreadLocal用於實現該需求

代碼示例與上節一致,只是將變量換成InheritableThreadLocal

package com.zte.sunquan.demo.thread.local;

/**
 * ThreadLocalTest class
 *
 * @author 10184538
 * @date 2019/2/21
 */
public class InheritThreadLocalTest {
    private static final ThreadLocal<String> nameThreadLocal = new InheritableThreadLocal<String>() {
        @Override
        protected String initialValue() {
            return "sunquan";
        }
    };

    public static void main(String[] args) throws InterruptedException {
        nameThreadLocal.set("abc");
        Thread th1 = new Thread(() -> {
            //abc(子線程中會引起父線程變更)
            System.out.println(Thread.currentThread().getName() + ":" + nameThreadLocal.get());
        });
        th1.start();
        //abc
        System.out.println(Thread.currentThread().getName() + ":" + nameThreadLocal.get());
        Thread.sleep(1000);
    }
}

打印結果:

main:abc
Thread-0:abc

子線程中該變量值繼承自父線程,與父線程中保持一致。
當我們只打印變量對象,可以得出子父線程中是是同一對象。(默認淺拷貝)

main:com.zte.sunquan.demo.thread.local.InheritThreadLocalTest$1@568db2f2
Thread-0:com.zte.sunquan.demo.thread.local.InheritThreadLocalTest$1@568db2f2

InheritableThreadLocal原理

inheritableThreadLocals:其設計的初衷是爲了增強ThreadLocal類型,使其具備變量可以被子線程繼承的特性,具體表現爲當前線程創建子線程時,會將此刻的ThreadLocal集合拷貝一份到子線程的ThreadLocal集合去, 這裏的拷貝並非一定是淺拷貝或者是深拷貝,默認則是淺拷貝,可以通過重寫ThreadLocal類中T childValue(T parentValue)這個接口來實現深拷貝

下面通過實例加深理解:

package com.zte.sunquan.demo.thread.local;

/**
 * ThreadLocalTest class
 *
 * @author 10184538
 * @date 2019/2/21
 */
public class InheritThreadLocal2Test {
    private static final ThreadLocal<String> nameThreadLocal = new InheritableThreadLocal<String>() {
        @Override
        protected String initialValue() {
            return "sunquan";
        }
    };

    public static void main(String[] args) throws InterruptedException {
        nameThreadLocal.set("abc");
        Thread th1 = new Thread(() -> {
            System.out.println(Thread.currentThread().getName() + ":" + nameThreadLocal.get());
            //nameThreadLocal.remove();
            //在子線程中修改線程變量
            nameThreadLocal.set("efg");
            System.out.println(nameThreadLocal);
            System.out.println(Thread.currentThread().getName() + ":" + nameThreadLocal.get());

        });
        th1.start();
        Thread.sleep(2000);
        Thread th2 = new Thread(() -> {
            //子線程中不會引起父線程變更
            System.out.println(Thread.currentThread().getName() + ":" + nameThreadLocal.get());

        });
        th2.start();
        //abc
        System.out.println(Thread.currentThread().getName() + ":" + nameThreadLocal.get());
        Thread.sleep(1000);
        System.out.println(nameThreadLocal);
    }
}

打印結果:

Thread-0:abc
com.zte.sunquan.demo.thread.local.InheritThreadLocal2Test$1@1c8c9112
Thread-0:efg
main:abc
Thread-1:abc
com.zte.sunquan.demo.thread.local.InheritThreadLocal2Test$1@1c8c9112

不知道你是否會有這個疑問,明明我在thread-0中修改了線程變量爲efg了,爲何thread-1與main線程爲何打印的還是abc。答案主要因爲String在JAVA中(8類基本類型)通過常量池管理。

簡單證明一下:

package com.zte.sunquan.demo.thread.local;

/**
 * ThreadLocalTest class
 *
 * @author 10184538
 * @date 2019/2/21
 */
public class InheritThreadLocal22Test {
    static class Temp {
        String name;

        @Override
        public String toString() {
            final StringBuilder sb = new StringBuilder("Temp{");
            sb.append("name='").append(name).append('\'');
            sb.append('}');
            return sb.toString();
        }
    }


    private static final ThreadLocal<Temp> nameThreadLocal = new InheritableThreadLocal<Temp>() {
        @Override
        protected Temp initialValue() {
            Temp temp = new Temp();
            temp.name = "sunquan";
            return temp;
        }
    };

    public static void main(String[] args) throws InterruptedException {
        //這一步必須要在主線程中設置變更,纔會傳遞至子線程中
        Temp temp = new Temp();
        temp.name = "sunquan";
        nameThreadLocal.set(temp);

        Thread th1 = new Thread(() -> {
            System.out.println(Thread.currentThread().getName() + ":" + nameThreadLocal.get());
            //修改線程變量
            nameThreadLocal.get().name = "efg";
            System.out.println(Thread.currentThread().getName() + ":" + nameThreadLocal.get());

        });
        th1.start();
        Thread.sleep(2000);
        Thread th2 = new Thread(() -> {
            //abc(子線程中會引起父線程變更)
            System.out.println(Thread.currentThread().getName() + ":" + nameThreadLocal.get());

        });
        th2.start();
        //abc
        System.out.println(Thread.currentThread().getName() + ":" + nameThreadLocal.get());
        Thread.sleep(1000);
    }
}

打印結果:

Thread-0:Temp{name=‘sunquan’}
Thread-0:Temp{name=‘efg’}
main:Temp{name=‘efg’}
Thread-1:Temp{name=‘efg’}

每個線程都有一個ThreadLocal.ThreadLocalMap inheritableThreadLocals用於記錄線程變量。
示例:

// 全局定義的ThreadLocal類型的對象引用;
ThreadLocal varKey = new ThreadLocal();
// 讀取該線程的ThreadLocal變量的實際值
String myValue = thread. inheritableThreadLocals.get( varKey );
// 把值設置給新線程
newThread.inheritableThreadLocals.put( varKey, myValue );

InheritableThreadLocal缺陷

在使用線程的時候往往不會只是簡單的new Thread對象,使用線程池的特點:

  1. 爲了減小創建線程的開銷,線程池會緩存已經使用過的線程
  2. 生命週期統一管理,合理的分配系統資源

所以當一個子線程已經使用過,並且會set新的值到ThreadLocal中,那麼第二個task提交進來的時候則不能獲取父線程中的值,如果new Thread(多個)也會存在。
如果我們能夠,在使用完這個線程的時候清除所有的localMap,在submit新任務的時候在重新重父線程中copy所有的Entry。然後重新給當前線程的t.inhertableThreadLocal賦值。這樣就能夠保證在線程池中每一個新的任務都能夠獲得父線程中ThreadLocal中的值而不受其他任務的影響,因爲在每個線程生命週期完成的時候會自動clear所有的數據。
Alibaba的一個庫解決了這個問題github:alibaba/transmittable-thread-local

package com.zte.sunquan.demo.thread.local;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

/**
 * ThreadLocalTest class
 * 需要注意通過線程池來進行子父線程變量傳遞時,則會出現問題
 * @author 10184538
 * @date 2019/2/21
 */
public class InheritThreadLocal3Test {

    private static ExecutorService executorService = Executors.newFixedThreadPool(1);

    private static final ThreadLocal<String> nameThreadLocal = new InheritableThreadLocal<String>() {
        @Override
        protected String initialValue() {
            return "sunquan";
        }
    };

    public static void main(String[] args) throws InterruptedException {
        nameThreadLocal.set("abc");

        executorService.submit(()->{
            //abc(子線程中會引起父線程變更)
            System.out.println(Thread.currentThread().getName() + ":" + nameThreadLocal.get());
            //修改線程變量
            nameThreadLocal.set("efg");
        });

        TimeUnit.SECONDS.sleep(1);

        executorService.submit(()->{
            //居然打印efg,而不在是父線程中變量
            System.out.println(Thread.currentThread().getName() + ":" + nameThreadLocal.get());
        });
        //abc
        System.out.println(Thread.currentThread().getName() + ":" + nameThreadLocal.get());
        Thread.sleep(1000);
    }
}

打印
pool-1-thread-1:abc
main:abc
pool-1-thread-1:efg
可以看到第二次pool-1-thread-1的打印變成了efg,受到了之前線程設置的影響。如何避免該類影響:

  • 每次使用線程池時,都顯示創建關閉
package com.zte.sunquan.demo.thread.local;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

/**
 * ThreadLocalTest class
 * 需要注意通過線程池來進行子父線程變量傳遞時,則會出現問題
 * @author 10184538
 * @date 2019/2/21
 */
public class InheritThreadLocal3Test {

    private static ExecutorService executorService = Executors.newFixedThreadPool(1);

    private static final ThreadLocal<String> nameThreadLocal = new InheritableThreadLocal<String>() {
        @Override
        protected String initialValue() {
            return "sunquan";
        }
    };

    public static void main(String[] args) throws InterruptedException {
        nameThreadLocal.set("abc");

        executorService.submit(()->{
            //abc(子線程中會引起父線程變更)
            System.out.println(Thread.currentThread().getName() + ":" + nameThreadLocal.get());
            //修改線程變量
            nameThreadLocal.set("efg");
        });

        TimeUnit.SECONDS.sleep(1);

        executorService.shutdown();
        executorService = Executors.newFixedThreadPool(1);
        executorService.submit(()->{
            //居然打印efg,而不在是父線程中變量
            System.out.println(Thread.currentThread().getName() + ":" + nameThreadLocal.get());
        });
        //abc
        System.out.println(Thread.currentThread().getName() + ":" + nameThreadLocal.get());
        Thread.sleep(1000);
    }
}

上面的用例中進行線程關閉和重新創建,這樣打印都爲abc。
但這不僅會引起性能損耗,並且在併發中會造成不必要的上下文切換,還必須用信號量進行併發控制。

這裏不知你是否會想到在pool-1執行完成後,在main線程中使用remove將線程變更移除,這樣希望pool-1再次執行時,能夠重新拷內現在main線程變量。嘗試後,發現並不行,pool-1並不會再去main裏同步一次線程變更,此時只有main線程的中的線程就是變化了。

TransmittableThreadLocal實現

使用TransmittableThreadLocal則可以避免線程池中對於線程變更的相互影響,先看實現代碼:

package com.zte.sunquan.demo.thread.local;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

import com.alibaba.ttl.TransmittableThreadLocal;
import com.alibaba.ttl.TtlRunnable;

/**
 * TransmittableThreadLocalTest class
 *
 * @author 10184538
 * @date 2019/2/21
 */
public class TransmittableThreadLocal2Test {

    private static ExecutorService executorService = Executors.newFixedThreadPool(1);

    private static final ThreadLocal<String> nameThreadLocal = new TransmittableThreadLocal<String>();

    public static void main(String[] args) throws InterruptedException {
        nameThreadLocal.set("abc");

        executorService.submit(TtlRunnable.get(() -> {
            //abc(子線程中會引起父線程變更)
            System.out.println(Thread.currentThread().getName() + ":" + nameThreadLocal.get());
            //修改線程變量
            nameThreadLocal.set("efg");
        }));

        TimeUnit.SECONDS.sleep(1);

        executorService.submit(TtlRunnable.get(() -> {
            System.out.println(Thread.currentThread().getName() + ":" + nameThreadLocal.get());
        }));
        //abc
        System.out.println(Thread.currentThread().getName() + ":" + nameThreadLocal.get());
        Thread.sleep(2000);
    }
}

打印
pool-1-thread-1:abc
main:abc
pool-1-thread-1:abc

TransmittableThreadLocal原理

參考https://www.jianshu.com/p/e0774f965aa3]

 <dependency>
      <groupId>com.alibaba</groupId>
      <artifactId>transmittable-thread-local</artifactId>
      <version>2.8.1</version>
 </dependency>

InheritableThreadLocal缺陷利用

在組件的性能優化遇到一類場景,涉及大量消息的處理,處理後數據進行入庫(PG)存儲,其中數據批量存儲的性能遠高於單條入庫存儲。示意圖如下紅框所示:
在這裏插入圖片描述
在BatchCommit入口主線程中進行線程變量數據暫存區的設置,

    private ThreadLocal<Stage> stage = new TransmittableThreadLocal<Stage>() {
        @Override
        protected Stage initialValue() {
            return new Stage(Thread.currentThread().getName());
        }
    };
    @Override
    public void begin() {
        this.begin(DataState.Modified);
    }

    @Override
    public void begin(@NotNull DataState dataState) {
        stage.set(new Stage(Thread.currentThread().getName()));
        autoCommit.set(false);
        this.dataState.set(dataState);
    }

接下來處理消息時,利用多線程處理,由於是子父線程可繼承線程變量,子線程中拷貝stage,在多線程的處理中將最終數據存儲在各自的stage中(父線程中stage也會同步增加數據),在批量處理完畢後,進行數據的入庫處理,即從主線程中取出Stage中數據進行處理即可,此時的數據也是完整的。需要注意的是主線程如果一直在用,則需要在處理完對stage進行清空。若一次處理完,主線程會重建,則無須對stage手動清理,當然這仍是一個好習慣。

    @Override
    public void commit() {
        if (dataState.get() != null && dataState.get() == DataState.Insert) {
            saveDatasNotAutoMerge();
        } else {
            saveDatasAutoMerge();
        }
        autoCommit.set(true);
        stage.get().clear();
        stage.remove();
    }

總結

1、ThreadLocal 維持線程封閉性的一種方式,每個使用該變量的線程都存有一份獨立的副本,線程之間該變量的值相互獨立,更改互不影響,線程之間不可傳遞。
2、InheritableThreadLocal 適用於子線程想要獲取父線程中的變量的場景,不適用於在線程池中傳遞ThreadLocal的值。即線程池中修改線程變更會互相影響
3、TransmittableThreadLocal 適用於線程池中共享線程變量。

變量類型 是否獨立 說明
ThreadLocal 子父線程變量獨立 修改互不影響
InheritThreadLocal 子繼承父線程變量 子中修改線程變量會影響父中變量(非基本類型和String類型
TransmittableThreadLocal 子繼承父線程變量 通過TtlRunable.get能保證子父線程變量隔離
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章