Java并發(fā)——ThreadLocal詳解

引言

ThreadLocal的官方API解釋為:
“該類提供了線程局部 (thread-local) 變量。這些變量不同于它們的普通對(duì)應(yīng)物,因?yàn)樵L問(wèn)某個(gè)變量(通過(guò)其 get 或 set 方法)的每個(gè)線程都有自己的局部變量,它獨(dú)立于變量的初始化副本。ThreadLocal 實(shí)例通常是類中的 private static 字段,它們希望將狀態(tài)與某一個(gè)線程(例如,用戶 ID 或事務(wù) ID)相關(guān)聯(lián)。”

大概的意思有兩點(diǎn):

  • 1、ThreadLocal提供了一種訪問(wèn)某個(gè)變量的特殊方式:訪問(wèn)到的變量屬于當(dāng)前線程,即保證每個(gè)線程的變量不一樣,而同一個(gè)線程在任何地方拿到的變量都是一致的,這就是所謂的線程隔離。
  • 2、如果要使用ThreadLocal,通常定義為private static類型,在我看來(lái)最好是定義為private static final類型。

什么是ThreadLocal變量

ThreadLoal 變量,線程局部變量,同一個(gè) ThreadLocal 所包含的對(duì)象,在不同的 Thread 中有不同的副本。這里有幾點(diǎn)需要注意:

因?yàn)槊總€(gè) Thread 內(nèi)有自己的實(shí)例副本,且該副本只能由當(dāng)前 Thread 使用。這是也是 ThreadLocal 命名的由來(lái)。
既然每個(gè) Thread 有自己的實(shí)例副本,且其它 Thread 不可訪問(wèn),那就不存在多線程間共享的問(wèn)題。
ThreadLocal 提供了線程本地的實(shí)例。它與普通變量的區(qū)別在于,每個(gè)使用該變量的線程都會(huì)初始化一個(gè)完全獨(dú)立的實(shí)例副本。ThreadLocal 變量通常被private static修飾。當(dāng)一個(gè)線程結(jié)束時(shí),它所使用的所有 ThreadLocal 相對(duì)的實(shí)例副本都可被回收。

總的來(lái)說(shuō),ThreadLocal 適用于每個(gè)線程需要自己獨(dú)立的實(shí)例且該實(shí)例需要在多個(gè)方法中被使用,也即變量在線程間隔離而在方法或類間共享的場(chǎng)景。

兩大使用場(chǎng)景——ThreadLocal的用途

  • 1、每個(gè)線程需要一個(gè)獨(dú)享的對(duì)象(通常是工具類,典型需要使用的類有SimpleDateFormat和Random)
  • 2、每個(gè)線程內(nèi)需要保存全局變量(例如在攔截器中獲取用戶信息),可以讓不同方法直接使用,避免參數(shù)傳遞的麻煩

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

  • 每個(gè)Thread內(nèi)有自己的實(shí)例副本,不共享
/**
 * @Description: 1000個(gè)打印日期的任務(wù),用線程池來(lái)執(zhí)行 使用ThreadLocal來(lái)解決線程安全問(wèn)題
 */
public class ThreadLocalTest6 {

    public static ExecutorService executorService = Executors.newFixedThreadPool(10);

    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < 1000; i++) {
            final int seconds = i;
            executorService.submit(new Runnable() {
                public void run() {
                    String date = new ThreadLocalTest6().date(seconds);
                    System.out.println(date);
                }
            });
//            executorService.shutdown();
        }
    }

    public String date(int seconds){
        //參數(shù)的單位是毫秒,從1970.1.1 00:00:00開始計(jì)時(shí)
        Date date = new Date(1000 * seconds);
        SimpleDateFormat simpleDateFormat = ThreadSafeFormatter.dateFormatThreadLocal2.get();
        return simpleDateFormat.format(date);
    }
}

class ThreadSafeFormatter{
    public static ThreadLocal<SimpleDateFormat> dateFormatThreadLocal = new ThreadLocal<SimpleDateFormat>(){
        @Override
        protected SimpleDateFormat initialValue() {
            //初始化SimpleDateFormat
            return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        }
    };

    public static ThreadLocal<SimpleDateFormat> dateFormatThreadLocal2 =
            ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
}

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

當(dāng)前用戶信息需要被線程內(nèi)所有方法共享

  • 比較繁瑣的解決方案是把user作為參數(shù)層層傳遞,從service-1()傳遞到service-2(),在從service-2()傳遞到service-3(),以此類推,但是這樣做會(huì)導(dǎo)致代碼冗余且不容易維護(hù)


每個(gè)線程內(nèi)需要保存全局變量,可以讓不同方法直接使用,避免參數(shù)傳遞的麻煩

  • 在此基礎(chǔ)上可以演進(jìn),使用UserMap


  • 當(dāng)多線程同時(shí)工作時(shí),我們需要保證線程安全,可以使用synchronized,也可以使用ConcurrentHashMap,但無(wú)論使用什么,都會(huì)對(duì)性能有所影響


  • 更好的辦法就是使用ThreadLocal,這樣無(wú)需synchronized,可以在不影響性能的情況下,也無(wú)需層層傳遞參數(shù),就可以達(dá)到保存當(dāng)前線程對(duì)應(yīng)的用戶信息的目的

方法

  • 使用ThreadLocal保存一些業(yè)務(wù)內(nèi)容(用戶權(quán)限信息,從用戶系統(tǒng)獲取到的用戶名、userID等)
  • 這些信息在同一個(gè)線程內(nèi)相同,但是不同的線程使用的業(yè)務(wù)內(nèi)容是不同的
  • 在線程生命周期內(nèi),都通過(guò)這個(gè)靜態(tài)ThreadLocal實(shí)例的get()方法取得自己set進(jìn)去的那個(gè)對(duì)象,從而避免了將這個(gè)對(duì)象(例如user對(duì)象)作為參數(shù)傳遞的麻煩
  • 強(qiáng)調(diào)的是同一個(gè)請(qǐng)求(同一個(gè)線程內(nèi))不同方法間的共享
  • 不需要重寫initialValue()方法,但是必須手動(dòng)調(diào)用set()方法


/**
 * @author: huangyibo
 * @Date: 2020/2/15 16:03
 * @Description: 演示ThreadLocal用法2,避免傳遞參數(shù)的麻煩
 */
public class ThreadLocalDemo1 {

    public static void main(String[] args) {
        Service1 service1 = new Service1();
        service1.process();
    }
}

class Service1{
    public void process(){
        User user = new User("小明");
        UserContextHolder.holder.set(user);
        new Service2().process();
    }
}

class Service2{
    public void process(){
        User user = UserContextHolder.holder.get();
        System.out.println("service2:"+user.name);
        new Service3().process();
    }
}

class Service3{
    public void process(){
        User user = UserContextHolder.holder.get();
        System.out.println("service3:"+user.name);
        UserContextHolder.holder.remove();//使用完ThreadLocal之后調(diào)用remove()方法,避免內(nèi)存泄漏
    }
}

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

class User{
    String name;

    public User(String name) {
        this.name = name;
    }
}

ThreadLocal的兩個(gè)作用總結(jié)

  • 1、讓某個(gè)需要用到的對(duì)象在線程間隔離(每個(gè)線程都有自己獨(dú)立的對(duì)象)
  • 2、在任何方法中都可以輕松獲取到該對(duì)象
  • 3、根據(jù)共享對(duì)象的生成時(shí)機(jī)不同,選擇initialValue或set來(lái)保存對(duì)象

場(chǎng)景一:initialValue

  • 在ThreadLocal第一次get的時(shí)候把對(duì)象給初始化出來(lái),對(duì)象的初始化時(shí)機(jī)可以由我們控制

場(chǎng)景二:set

  • 如果需要保存到ThreadLocal里的對(duì)象的生成時(shí)機(jī)不由我們隨意控制,例如攔截器生成的用戶信息,用ThreadLocal.set直接放到我們的ThreadLocal中去,以便后續(xù)使用

使用ThreadLocal帶來(lái)的好處

  • 線程安全
  • 不需要加鎖,提高執(zhí)行效率
  • 更高效的利用內(nèi)存,節(jié)省開銷。相比與每個(gè)任務(wù)都新建一個(gè)SimpleDateFormat,顯然ThreadLocal可以節(jié)省內(nèi)存和開銷
  • 避免傳參的繁瑣。無(wú)論是場(chǎng)景一的工具類,還是場(chǎng)景二的用戶名,都可以在任何地方直接通過(guò)ThreadLocal拿到,再也不需要每次都傳同樣的參數(shù),ThreadLocal使得代碼耦合度更低,更優(yōu)雅

ThreadLocal原理

首先 ThreadLocal 是一個(gè)泛型類,保證可以接受任何類型的對(duì)象。

因?yàn)橐粋€(gè)線程內(nèi)可以存在多個(gè) ThreadLocal 對(duì)象,所以其實(shí)是 Thread 內(nèi)部維護(hù)了一個(gè) Map ,這個(gè) Map 不是直接使用的 HashMap ,而是 ThreadLocal 實(shí)現(xiàn)的一個(gè)叫做 ThreadLocalMap 的靜態(tài)內(nèi)部類。而我們使用的 get()、set() 方法其實(shí)都是調(diào)用了這個(gè)ThreadLocalMap類對(duì)應(yīng)的 get()、set() 方法。例如下面的 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);
}

ThreadLocal主要方法

  • protected T initialValue():初始化
    • 該方法會(huì)返回當(dāng)前線程對(duì)應(yīng)的“初始值”,這是一個(gè)延遲加載的方法,只有在調(diào)用get的時(shí)候才會(huì)觸發(fā)
    • 當(dāng)線程第一次調(diào)用get方法訪問(wèn)變量時(shí),調(diào)用此方法,如果線程先前調(diào)用了set方法,在這種請(qǐng)求下,不會(huì)為線程調(diào)用本initialValue方法
    • 通常,每個(gè)線程最多調(diào)用一次此方法,但如果已經(jīng)調(diào)用了remove()后,再調(diào)用get(),則可以再次調(diào)用此方法
    • 如果不重寫該方法,這個(gè)方法會(huì)返回null,一般使用匿名內(nèi)部類的方法來(lái)重寫initialValue()方法,以便在后續(xù)使用中可以初始化副本對(duì)象
  • public void set(T value) :為這個(gè)線程設(shè)置一個(gè)新值
  • public T get() :得到這個(gè)線程對(duì)應(yīng)的value。如果是首次調(diào)用get(),則會(huì)調(diào)用initialValue()方法來(lái)得到這個(gè)值
  • public void remove():刪除對(duì)應(yīng)線程的值

ThreadLocal主要方法源碼解析

  • public T get() :get方法是先取出當(dāng)前線程的ThreadLocalMap,然后調(diào)用map.getEntry方法,把本ThreadLocal的引用作為參數(shù)傳入,取出map中屬于本ThreadLocal的value
  • 注意這個(gè)map以及map中的key和value都是保存在線程中的,而不是保存在ThreadLocal中
public T get() {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result;
        }
    }
    return setInitialValue();
}

ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}
  • public void set(T value) :和setInitialValue()方法很類似
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);
}
  • public void remove()方法
public void remove() {
    ThreadLocalMap m = getMap(Thread.currentThread());
    if (m != null)
     m.remove(this);
}

private void remove(ThreadLocal<?> key) {
    Entry[] tab = table;
    int len = tab.length;
    int i = key.threadLocalHashCode & (len-1);
    for (Entry e = tab[i];
         e != null;
         e = tab[i = nextIndex(i, len)]) {
        if (e.get() == key) {
            e.clear();
            expungeStaleEntry(i);
            return;
        }
    }
}

ThreadLocalMap

  • ThreadLocalMap類,也就是Thread.threadLocals
  • ThreadLocalMap類是每個(gè)線程Thread類里面的變量,里面最重要的是一個(gè)鍵值對(duì)數(shù)組Entry[] table,可以認(rèn)為是一個(gè)Map,鍵值對(duì):
    • 鍵:這個(gè)ThreadLocal
    • 值:實(shí)際需要的成員變量,比如User或者SimpleDateFormat
  • ThreadLocalMap雖然類似HashMap但是處理處理Hash沖突略有不同
    • HashMap


    • ThreadLocalMap采用的是線性探測(cè)法,也就是如果發(fā)生Hash沖突,就繼續(xù)尋找下一個(gè)空位,而不是采用鏈表拉鏈

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

  • 通過(guò)源碼分析可以看出,setInitialValue和直接set最后都是利用map.set()方法來(lái)設(shè)置值
  • 也就是說(shuō),最后都會(huì)對(duì)應(yīng)到ThreadLocalMap的一個(gè)Entry,只是起點(diǎn)和入口不一樣

ThreadLocal注意點(diǎn)

內(nèi)存泄漏問(wèn)題

內(nèi)存泄漏:某個(gè)對(duì)象不再有用,但是占用的內(nèi)存確不能被回收。

Key的泄漏
ThreadLocalMap中的Entry繼承自WeakReference,是弱引用,弱引用的特點(diǎn)是,如果這個(gè)對(duì)象只被弱引用關(guān)聯(lián)(沒(méi)有任何強(qiáng)引用關(guān)聯(lián)),那么在下一次垃圾回收的時(shí)候必然會(huì)被清理掉。

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

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

Value的泄漏

  • ThreadLocalMap的每個(gè)Entry都是一個(gè)對(duì)Key的弱引用,同時(shí)每個(gè)Entry都包含了一個(gè)對(duì)Value的強(qiáng)引用
  • 正常情況下,當(dāng)線程終止,保存在ThreadLocal里的Value會(huì)被垃圾回收,因?yàn)闆](méi)有任何強(qiáng)引用關(guān)聯(lián)
  • 但是,如果線程始終不終止(比如線程需要保持很久或者使用線程池的時(shí)候),那么Key對(duì)應(yīng)的Value就不能被回收,因?yàn)橛幸韵碌恼{(diào)用鏈:


  • 因?yàn)閂alue和Thread之間還存在這個(gè)強(qiáng)引用調(diào)用鏈,所以導(dǎo)致value無(wú)法被回收,就可會(huì)出現(xiàn)OOM
  • JDK已經(jīng)考慮到了這個(gè)問(wèn)題,所以在set()、remove()、rehash()方法中會(huì)掃描key為null的Entry,并把對(duì)應(yīng)的value設(shè)置為null,這樣value對(duì)象就可以被回收
  • 但是如果一個(gè)ThreadLocal不被使用,那么實(shí)際上set()、remove()、rehash()等方法也不會(huì)被調(diào)用,并且線程又不停止,那么調(diào)用鏈就一直存在,那么也就導(dǎo)致了value的內(nèi)存泄漏

如何避免內(nèi)存泄漏(阿里代碼規(guī)約)

  • 調(diào)用remove()方法,就會(huì)刪除對(duì)應(yīng)的Entry對(duì)象,可以避免內(nèi)存泄漏,所以使用完ThreadLocal之后,應(yīng)該調(diào)用remove()方法
  • 如果使用攔截器獲取用戶信息,那么同樣應(yīng)該使用攔截器在線程請(qǐng)求退出之前將之前保存過(guò)得信息清除掉

空指針異常

  • 在進(jìn)行g(shù)et()之前,必須先進(jìn)行set(),否則可能會(huì)報(bào)空指針異常
public class ThreadLocalNPE {

    ThreadLocal<Long> threadLocal = new ThreadLocal<Long>();

    public void set(){
        threadLocal.set(Thread.currentThread().getId());
    }

    /**
     * 這里的返回值使用long的時(shí)候,如果沒(méi)有set()就調(diào)用get()那么會(huì)報(bào)空指針異常,因?yàn)闋可娴讲鹣滢D(zhuǎn)換(將對(duì)象類型轉(zhuǎn)換為基本類型)
     * @return
     */
    public Long get(){
        return threadLocal.get();
    }

    public static void main(String[] args) {
        ThreadLocalNPE threadLocalNPE = new ThreadLocalNPE();
        System.out.println(threadLocalNPE.get());
        new Thread(() -> {
            threadLocalNPE.set();
            long threadId = threadLocalNPE.get();
            System.out.println(threadId);
        }).start();

    }
}

共享對(duì)象

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

如果不使用ThreadLocal就可以解決問(wèn)題,那么就不要強(qiáng)行使用

  • 例如在任務(wù)很少的時(shí)候,在局部變量中可以新建對(duì)象就可以解決問(wèn)題,那么就不需要使用到ThreadLocal

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

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

ThreadLocal在Spring中的實(shí)例分析

  • RequestContextHolder和DateTimeContextHolder中,看到里面使用了ThreadLocal
  • 每次HTTP請(qǐng)求都對(duì)應(yīng)一個(gè)線程,線程之間相互隔離,這就是ThreadLocal的典型應(yīng)用場(chǎng)景

參考:
https://www.cnblogs.com/luxiaoxun/p/8744826.html

http://ifeve.com/tag/threadlocal/

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,363評(píng)論 6 532
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,497評(píng)論 3 416
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人,你說(shuō)我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,305評(píng)論 0 374
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我,道長(zhǎng),這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,962評(píng)論 1 311
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 71,727評(píng)論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,193評(píng)論 1 324
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,257評(píng)論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,411評(píng)論 0 288
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,945評(píng)論 1 335
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 40,777評(píng)論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 42,978評(píng)論 1 369
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,519評(píng)論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,216評(píng)論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,642評(píng)論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,878評(píng)論 1 286
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 51,657評(píng)論 3 391
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 47,960評(píng)論 2 373