15.ThreadLocal線程持有對(duì)象

一、ThreadLocal兩大使用場(chǎng)景

  1. 每個(gè)線程需要一個(gè)獨(dú)享的對(duì)象
  2. 每個(gè)線程內(nèi)需要保存全局變量

1) 每個(gè)線程需要一個(gè)獨(dú)享的對(duì)象

  1. 通常是工具類(線程不安全),典型需要使用的類比如SimpleDateFormat和Random
  2. ThreadLocal定義為靜態(tài)變量
  3. 通過重寫initialValue()方法在本地線程第一次獲取對(duì)象時(shí)進(jìn)行創(chuàng)建。
  4. 本地線程通過threadLocal.get()獲取該對(duì)象。
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * @auth Hahadasheng
 * @since 2020/10/27
 */
public class ThreadLocalExclusiveObj {

    private static final ThreadLocal<SimpleDateFormat> dateFormatLocal =
            ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS"));

    public static void main(String[] args) {

        ExecutorService executor = Executors.newFixedThreadPool(10);
        for (int i = 0; i < 1000; i++) {
            final int index = i;
            executor.execute(() -> {
                Date date = new Date(1000 * index);
                String format = dateFormatLocal.get().format(date);
                System.out.println(format);
            });
        }

        executor.shutdown();
    }
}
  • 拓展:時(shí)間格式化

注意,h和H代表的含義是不一樣的
y = 年(yy或yyyy)
M = 月(MM)
d = 月中的天(dd)
h = 小時(shí)(0-12)(hh)
H = 小時(shí)(0-23)(HH)
m = 時(shí)分(mm)
s = 秒(ss)
S = 毫秒(SSS)
z = 時(shí)區(qū)文本(例如,太平洋標(biāo)準(zhǔn)時(shí)間…)
Z = 時(shí)區(qū),時(shí)間偏移量(例如-0800)
以下是一些模式示例,其中包含每個(gè)模式如何格式化或期望解析日期的示例:
yyyy-MM-dd(2009-12-31)
dd-MM-YYYY(31-12-2009)
yyyy-MM-dd HH:mm:ss(2009-12-31 23:59:59)

HH:mm:ss.SSS(23:59.59.999)
yyyy-MM-dd HH:mm:ss.SSS(2009-12-31 23:59:59.999)
yyyy-MM-dd HH:mm:ss.SSS Z(2009-12-31 23:59:59.999 +0100)

2) 每個(gè)線程內(nèi)需要保存全局變量

  1. 比如在攔截器中獲取用戶的信息,可以讓不同方法直接使用,避免參數(shù)傳遞的麻煩。
  2. 在本地線程生命周期內(nèi),通過set/get方法設(shè)置獲取線程獨(dú)占變量,避免參數(shù)到處傳遞。
  3. 強(qiáng)調(diào)的是同一個(gè)請(qǐng)求內(nèi)(同一個(gè)線程)不同方法間的共享。
  4. 不需要要重寫initialValue()方法

可以利用共享的Map:使用static的ConcurrentHashMap,把當(dāng)前線程的ID作為key,把user作為value來保存,這樣可以做到線程間的隔離,但是依然有性能影響。使用ThreadLocak就沒有性能影響,內(nèi)部沒有使用synchronized等同步機(jī)制,也無需層層傳遞參數(shù)。

import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;

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

/**
 * @auth Hahadasheng
 * @since 2020/10/29
 */
public class ThreadLocalShareInThread {

    public static void main(String[] args) {
        ExecutorService pool = Executors.newFixedThreadPool(10);
        for (int i = 0; i < 20; i++) {
            final int index = i;
            pool.execute(() -> {
                User user = new User();
                user.setId(String.format("NO.%s", index + 1));
                user.setName(String.format("HHDS-%s", index + 1));
                user.setGender(index & 1);
                user.setAge(index + 10);

                UserHolder.holder.set(user);
                otherMethod();
            });
        }

        pool.shutdown();

    }

    public static void otherMethod() {
        System.out.println(UserHolder.holder.get());
        UserHolder.holder.remove();
    }
}

@Getter
@Setter
class User {
    private String id;
    private String name;
    private int gender;
    private int age;

    @Override
    public String toString() {
        return "{" +
                "id='" + id + '\'' +
                ", name='" + name + '\'' +
                ", gender=" + gender +
                ", age=" + age +
                '}';
    }
}

class UserHolder {
    public static final ThreadLocal<User> holder = new ThreadLocal<>();
}

3) 總結(jié)

  1. 讓某個(gè)需要用到的對(duì)象在線程間隔離,每個(gè)線程都有自己獨(dú)立的對(duì)象
  2. 在任何方法中都能輕松獲取該對(duì)象。
  3. initialValue使用場(chǎng)景:
    1. 在ThreadLocal第一次get的時(shí)候吧對(duì)象給初始化,對(duì)象的初始化時(shí)機(jī)可以由我們控制
  4. set:
    1. 如果需要保存到ThreadLocal里面的對(duì)象的生成時(shí)機(jī)不由我們隨意控制,比如攔截器生成的用戶信息,用ThreadLocal.set直接放進(jìn)去即可

4) ThreadLocal帶來的好處

  1. 達(dá)到線程安全
  2. 不需要加鎖,提高執(zhí)行效率
  3. 更高效地利用內(nèi)存,節(jié)省開銷(例如每個(gè)線程持有一個(gè)SimpleDateFormat)。
  4. 免去傳參的繁瑣

二、ThreadLocal原理

1) Thread與ThreadLocal以及ThreadLocalMap之間的關(guān)系

  1. 每個(gè)Thread實(shí)例都會(huì)有一個(gè)獨(dú)立的ThreadLocalMap對(duì)象
  2. ThreadLocalMap中的Entry的key為ThreadLocal對(duì)應(yīng)的引用(弱引用),value則是線程獨(dú)享的對(duì)象

Thread與TL和TLM之間的關(guān)系.png

2) 重要方法

1> T initialValue():該方法會(huì)返回當(dāng)前線程對(duì)應(yīng)的“初始值”,延遲加載的方法,只有在調(diào)用get的時(shí)候才會(huì)觸發(fā)。

  1. 當(dāng)線程第一次使用get方法訪問變量時(shí),將調(diào)用此方法,除非線程先前調(diào)用了set方法,在這種相框下,不會(huì)為線程調(diào)用本initialValue方法。

get內(nèi)部實(shí)現(xiàn)是檢查對(duì)象是否為null,如果為null則執(zhí)行initialValue()方法<重寫該方法后執(zhí)行重寫的方法>,否則直接返回對(duì)象。

3.如果調(diào)用了remove()后,再調(diào)用get(),則可以再次調(diào)用initialValue()方法。

  1. initialValue()方法默認(rèn)實(shí)現(xiàn)是直接返回一個(gè)null,如果需要獨(dú)享對(duì)象,一般使用匿名內(nèi)部類的方式重寫該方法。
    • ThreadLocal.withInitial(() -> {... return ...})

2> void set(T t)

  1. 為這個(gè)線程設(shè)置一個(gè)新值

3> T get()

  1. 得到線程對(duì)應(yīng)的value。如果是首次調(diào)用get()<之前沒有調(diào)用void set(T t)>,則會(huì)調(diào)用initialValue來得到這個(gè)值。

4> void remoe()

  1. 刪除線程對(duì)應(yīng)的值。

3) 源碼分析

  1. get方法是先取出當(dāng)前線程的ThreadLocalMap,然后調(diào)用map.getEntry方法,把本ThreadLocal的引用作為參數(shù)傳入,取出map中屬于本ThreadLocal的value
  2. 注意,這個(gè)map以及map中的key和value都是保存在線程中的,而不是保存在ThreadLocal中
  3. initialValue方法:默認(rèn)返回null,可以自定義實(shí)現(xiàn)
  4. remove方法,只刪除ThreadLocalMap對(duì)應(yīng)本ThreadLocal引用的Entry

4) ThreadLocalMap類

  1. 在Thread中以threadLocals作為成員變量

  2. ThreadLocalMap類是每個(gè)線程Thread類里面的變量,里面最重要的是一個(gè)鍵值對(duì)數(shù)組Entry[] table,可以認(rèn)為是一個(gè)Map鍵值對(duì)

    1. 鍵:這個(gè)ThreadLocal
    2. 值:實(shí)際需要的成員變量
  3. ThreadLocalMap類使用上類似HashMap,但是在實(shí)現(xiàn)上略有不同,

    1. 并沒有實(shí)現(xiàn)Map接口
  4. 解決沖突

    1. HashMap解決Hash沖突的思路是鏈表+紅黑樹
    2. ThreadLocalMap采用的是線性探測(cè)法:如果發(fā)生沖突,就繼續(xù)找下一個(gè)空位置,而不是用拉鏈或者紅黑樹
  5. 可以當(dāng)做為一個(gè)Map去理解

5) 兩種使用場(chǎng)景殊途同歸

  1. setInitialValue和直接set最后都是利用map.set()方法來設(shè)置值。最后都會(huì)對(duì)應(yīng)到ThreadLocalMap的一個(gè)Entry,只不過起點(diǎn)和入口不一樣。

三、ThreadLocal注意點(diǎn)

1) 內(nèi)存泄露

弱引用:如果一個(gè)對(duì)象被弱引用關(guān)聯(lián)(沒有任務(wù)強(qiáng)引用),那這個(gè)對(duì)象可以被GC垃圾回收

  1. 內(nèi)存泄露:某個(gè)對(duì)象不再有用,但是占用的內(nèi)存卻不能被回收。
  2. ThreadLocalMap中Entry的key的是弱引用,不會(huì)導(dǎo)致泄露問題。
  3. 每個(gè)Entry都包含一個(gè)對(duì)value的強(qiáng)引用。
  4. 正常情況下,當(dāng)線程終止,保存在ThreadLocalMap里的key, value會(huì)被垃圾回收,沒有任何強(qiáng)引用。
  5. 如果線程不終止(比如線程需要保持很久),key對(duì)應(yīng)的value就不能被回收,存在如下調(diào)用鏈
    • Thread->ThreadLocalMap->Entry(key為null)->Value
    • value無法回收,就可能出現(xiàn)OOM
  6. JDK已經(jīng)考慮到這個(gè)問題,所以在set,remove,rehash方法中會(huì)掃描key為null的Entry并發(fā)對(duì)應(yīng)的value設(shè)置為null,這樣value對(duì)象就可以被回收了。
  7. 如果一個(gè)ThreadLocal不被使用,那么實(shí)際上set,remove,rehash方法也不會(huì)被調(diào)用,如果同時(shí)
    線程又不停止,那么調(diào)用鏈就一直存在,就導(dǎo)致了value的內(nèi)存泄露

2) 避免內(nèi)存泄露(阿里規(guī)約)

  1. 使用完ThreadLocal之后主動(dòng)調(diào)用remove方法,刪除Entry對(duì)象,避免內(nèi)存泄露。

3) ThreadLocal空指針異常

  1. 在進(jìn)行g(shù)et之前,必須先set,否則可能會(huì)報(bào)空指針異常?
    • 可能是寫的代碼缺陷:包裝類拆箱導(dǎo)致
/**
 * @auth Hahadasheng
 * @since 2020/10/30
 */
public class ThreadLocalNPE {
    private static final ThreadLocal<Long> localId = new ThreadLocal<>();

    /**
     * 這里在沒有調(diào)用initializeValue以及set的前提下直接調(diào)用get方法,
     * 似乎直接返回null,但是卻報(bào)java.lang.NullPointerException
     * 是因?yàn)門hreadLocal定義的泛型為包裝類的Long,在方法返回時(shí)拆箱
     * 發(fā)現(xiàn)是null,所以報(bào)空指針
     */
    public static long get() {
        return localId.get();
    }

    /**
     * 而這個(gè)方法則不會(huì)報(bào)錯(cuò)
     */
    public static Long get2() {
        return localId.get();
    }

    public static void main(String[] args) {
        System.out.println(get2());
        System.out.println(get());
    }
}

4) 共享對(duì)象

  1. 如果在每個(gè)線程中ThreadLocal.set進(jìn)去的本來就是多線程共享的同一個(gè)對(duì)象,比如static對(duì)象,那么多個(gè)線程的ThreadLocal.get取得的還是這個(gè)共享對(duì)象本身,還是有并發(fā)問題。

如果可以不需要使用ThreadLocal,則不要進(jìn)行強(qiáng)行使用。

5) 優(yōu)先使用框架的支持,而不是自己創(chuàng)造

  1. 例如在Spring中,如果可以使用RequestContextHolder,那么就不需要自己維護(hù)ThreadLocal,因?yàn)樽约嚎赡軙?huì)忘記調(diào)用remove()方法等,造成內(nèi)存泄露。
  2. Spring中DateTimeContextHolder類,使用了ThreadLocal

6)關(guān)于弱引用被GC清理是否可用的疑惑解答

  1. 引用的關(guān)系是:Thread -> ThreadLocalMap -> Entity -> 弱引用ThreadLocal 和 數(shù)據(jù),所以:

  2. 雖然是弱引用,但是只要其他地方還有普通引用,就不會(huì)被清理,會(huì)一直存在(1.一般在使用的時(shí)候都是定義為靜態(tài)類屬性常量... static final ThreadLocal<?> ...,為強(qiáng)引用,只要此類不被虛擬機(jī)卸載,則GC不會(huì)回收該對(duì)象,相關(guān)弱引用也不會(huì)被清理;2.線程執(zhí)行產(chǎn)生的棧幀中局部變量表中可能也會(huì)存在該強(qiáng)引用)。

提示:GC Roots:虛擬機(jī)棧(棧幀中的本地變量表)中引用的對(duì)象;方法區(qū)中類靜態(tài)屬性引用的對(duì)象;方法區(qū)中常量引用的對(duì)象;本地方法棧中JNI(即一般說的native方法)中引用的對(duì)象

  1. 如果不是弱引用,而且用戶已經(jīng)不再持有這個(gè)ThreadLocal的引用并且沒有調(diào)用remove方法,那么只要線程還在,ThreadLocal和數(shù)據(jù)就會(huì)一直被引用無法回收,就是內(nèi)存泄漏了,所以這里用弱引用一定程度上是幫助忘記調(diào)用remove方法的用戶做清理工作…
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容