一文了解ThreadLocal用法

微信公衆號:Java流水賬
本號記錄國服安琪拉日常編程流水帳,歡迎後臺留言

爲啥寫ThreadLocal

背景:發現很多博客關於ThreadLocal的說明寫錯了,ThreadLocal不是維護了key爲Thread對象的Map,而是Thread對象維護了一個key爲ThreadLocal的Map。
下面截取的源碼說明了這個問題,如果覺得晦澀,可以先看後面的實例,再回過頭來看源碼。

//類Thread
public class Thread implements Runnable {
    /***Thread類內部維護了一個ThreadLocalMap變量***/
    ThreadLocal.ThreadLocalMap threadLocals = null;
}

//類ThreadLocal
public class ThreadLocal<T> {
    public void set(T value) {
        Thread t = Thread.currentThread(); //獲取當前線程對象
        ThreadLocalMap map = getMap(t);  //拿到線程私有的ThreadLocalMap
        if (map != null)
            map.set(this, value);  //ThreadLocal對象爲key 
        else
            createMap(t, value);  
    }

   public T get() {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t); //獲取Thread對象私有ThreadLocalMap對象
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);//拿到節點
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        return setInitialValue(); //設置初始值
    }
   //設置Thread對象的ThreadLocalMap
   void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);  
    }

   ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
            table = new Entry[INITIAL_CAPACITY];
            int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1); //hashcode & 運算去除高位  等同於取餘
            table[i] = new Entry(firstKey, firstValue);
            size = 1;
            setThreshold(INITIAL_CAPACITY);
        }
}

一般像網絡請求,使用線程池技術的,Tomcat/Jetty等容器,一個請求一個線程來處理的,如果有一些信息是線程共享的,有二種方式保證線程安全:

  • 一種就是常規的加鎖,當然也包括cas這種;
  • 一種就是使用ThreadLocal做線程隔離,線程訪問的是私有的變量,修改的也是線程對象Map自己的那份。

這種思想是典型的用空間換取降低線程安全風險和加鎖耗時的做法。
看一個多線程處理請求的例子,這張圖可以結合源碼看:
在這裏插入圖片描述
Request對象是Handler的成員變量,多線程訪問有線程安全問題,使用ThredLocal對Request對象做線程隔離,可以讓多線程執行handle()線程安全,再次說明:線程內部Map key爲ThreadLocal對象,value爲Request對象,因此實際上是每個線程有自己的線程局部變量,省去了鎖的開銷。

下面是一段簡單用法,只是爲了演示寫的。

public static void main(String[] args) {

        ThreadLocal<Person> threadLocal = new ThreadLocal<>(); //每個線程局部變量都需要創建一個ThreadLocal對象
        ThreadLocal<String> threadLocal2 = new ThreadLocal<>();
        AtomicInteger atomicInteger = new AtomicInteger(1000);

        ExecutorService executorService = Executors.newCachedThreadPool();

     //for(int i 0->10)
            executorService.submit(() -> {
                Person person = new Person();
                person.setIdcard(atomicInteger.getAndIncrement());
                person.setName(Thread.currentThread().getName());
                threadLocal.set(person);
                threadLocal2.set("haha");
                System.out.println(threadLocal.get());
                System.out.println(threadLocal2.get());

                System.out.println();

                threadLocal.remove();
            });
        executorService.shutdown();
    }

我的實際應用

舉一個我的項目中使用場景:
 背景:上一個項目在優化整體代碼,把項目我負責的主體的實現方式做了調整,ThreadLocal在其中扮演了非常重要的角色。需求(使用場景):客戶端有一個請求過來,Java程序根據請求報文的參數決定調用某個服務提供數據。相信大家都會有類似的需求,拿我們業務來說,請求報文如下:
{ “appid”: “88888888”, “userid”: 80, “datatype”: “***”, “data”:"{“dataids”:[146,147,148]}" }
一個用戶請求過來,需要根據報文中的datatype字段決定調取不同的數據提供服務。大體如下圖所示:
在這裏插入圖片描述

我在基類定義了一個ThreadLocal 對象來隔離PullDataNotification(封裝請求信息)這個類成員變量,因爲之前項目由於線程安全問題,每一個請求都是new 一個全新的Provider 處理來規避這個問題,看Cat上GC新生代很頻繁,爲了性能提升使用ThreadLocal 只創建一個對象,降低對象的頻繁創建和銷燬。

public abstract class CommonCreditProvider<E extends CreditArgument> implements IDataProvider{
    //基類定義ThreadLocal對象,如果有多個變量做線程隔離可以初始化多個對象
    protected ThreadLocal<PullDataNotification> notificationThreadLocal = new ThreadLocal<>()

    //主要處理函數  實際處理邏輯在process,由子類完成
    @Override
    PullDataResult getData(PullDataNotification notification) throws Exception {
        PullDataResult pullDataResult = null
        try{
            notificationThreadLocal.set(notification)
            getThreeElements()
            E creditArgument = initArguments()

            pullDataResult = process(creditArgument)
        }catch (Exception ex){
            throw ex
        }finally{
            //*****使用完手動remove******
            notificationThreadLocal.remove()
        }

        return pullDataResult
    }
}

重點:關注一個細節,我finally 代碼塊手動remove把線程變量從Thread對象的ThreadLocalMap中移除,因爲如果不這麼做可能有內存泄漏風險。
原因:看源碼,引用鏈,Thread -> ThreadLoalMap -> Entry[ WeakRefrence, Object] 因爲線程對象(Tomcat請求線程池等)存在,線程維護的Map的Entry數組如果不手動清除也還存在,根據JVM GC的根路徑定位,那數組中Entry的key,value是變量都在鏈上,所以會有內容泄漏的風險,所以要在使用完之後手動調用remove()函數清除。

後記

網上的很多文章,講ThreadLocal中的ThreadLocalMap存放的key是線程對象,value是設置的線程局部變量,乍一看覺得挺有道理的,也算實現了線程數據隔離。但是仔細想不合理,如果有多個變量都要線程隔離,key又是線程對象,那key不夠用啊!看源碼,發現網上博客很多寫的是錯的,所以還是以後遇到有疑問的多想想What? How?Why?這東西是什麼?怎麼用? 爲什麼這麼實現?合理嗎?


這裏沒有敷衍的複製粘貼,博眼球的面試資料分享,有的只是儘可能清晰的講清個人開發中遇到的一個個問題和總結。歡迎大家關注Java流水賬,純粹的個人技術公衆號。


在這裏插入圖片描述

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