ThreadLocal的使用以及源碼分析

前言

當(dāng)多線程訪問共享可變數(shù)據(jù)時,涉及到線程間同步的問題,并不是所有時候,都要用到共享數(shù)據(jù),所以就需要線程封閉出場了。本文中的ThreadLocal就起到了線程封閉的作用。它提供了線程內(nèi)的局部變量,不同線程之間不會相互干擾,這種變量在線程的生命周期內(nèi)起作用,減少了同一個線程內(nèi)多個函數(shù)或組件之間一些公共變量傳遞的復(fù)雜度。

特點

通俗的說ThreadLocal具備三個特性:

  • 線程并發(fā): 在多線程并發(fā)的場景下使用
  • 傳遞數(shù)據(jù): 我們通過ThreadLocal在同一線程,不同組件中傳遞公共變量
  • 線程隔離: 每個線程的變量都是獨立的,不會相互影響

ThreadLocal用于保存每個線程獨享的對象,為每個線程創(chuàng)建一個副本,這樣每個線程都可以修改自己所擁有的副本,而不會影響其他線程的副本,從而確保了線程安全??偠灾琓hreadLocal的核心作用就是將變量在線程中隔離。

ThreadLocal的基本使用

常用方法

我們先看一下它的類圖中的方法:


方法申明 描述
ThreadLocal() 創(chuàng)建ThreadLocal對象
public void set(T value) 設(shè)置當(dāng)前線程綁定的局部變量
public T get() 獲取當(dāng)前線程綁定的局部變量
public void remove() 移除當(dāng)前線程綁定的局部變量

例子

線程之間的變量非獨立

/**
 * Description
 * 線程隔離例子
 * 在多線程并發(fā)場景下,每個線程中的變量都是相互獨立的
 * 線程A:設(shè)置(變量1)      獲?。ㄗ兞?)
 * 線程B:設(shè)置(變量2)      獲?。ㄗ兞?)
 * Date 2020/6/3 22:03
 * Created by kwz
 */
public class ThreadLocalExample1 {

    @Getter
    @Setter
    private String content;

    public static void main(String[] args) {
        ThreadLocalExample1 example = new ThreadLocalExample1();
        for (int i = 0; i < 5; i++) {
            Thread thread = new Thread(
                    () -> {
                        //每個線程存一個變量,過一會兒取這個變量
                        example.setContent(Thread.currentThread().getName() + "的數(shù)據(jù)");
                        System.out.println("----------------------------------------");
                        System.out.println(Thread.currentThread().getName() + "---->" + example.getContent());
                    }
            );
            //0->4一共有5個線程
            thread.setName("線程" + i);
            thread.start();
        }
    }
}

我們看它的控制臺輸出結(jié)果:

----------------------------------------
----------------------------------------
----------------------------------------
線程2---->線程2的數(shù)據(jù)
線程0---->線程2的數(shù)據(jù)
----------------------------------------
線程4---->線程4的數(shù)據(jù)
----------------------------------------
線程1---->線程2的數(shù)據(jù)
線程3---->線程4的數(shù)據(jù)

我們可以看到一個線程取到了其他線程的變量

線程之間的變量相互獨立

/**
 * Description(利用ThreadLocal)
 * 線程隔離例子
 * 在多線程并發(fā)場景下,每個線程中的變量都是相互獨立的
 * 線程A:設(shè)置(變量1)      獲?。ㄗ兞?)
 * 線程B:設(shè)置(變量2)      獲?。ㄗ兞?)
 *
 * ThreadLocal
 * 1.set(): 將變量綁定到
 * Date 2020/6/3 22:03
 * Created by kwz
 */
public class ThreadLocalExample2 {

    ThreadLocal<String> t1 = new ThreadLocal<>();

    private String content;

    private String getContent(){
        String s = t1.get();
        return s;
    }

    private void setContent(String content){
        //變量content綁定到當(dāng)前線程
        t1.set(content);
    }

    public static void main(String[] args) {
        ThreadLocalExample2 example = new ThreadLocalExample2();
        for (int i = 0; i < 5; i++) {
            Thread thread = new Thread(
                    () -> {
                        //每個線程存一個變量,過一會兒取這個變量
                        example.setContent(Thread.currentThread().getName() + "的數(shù)據(jù)");
                        System.out.println("----------------------------------------");
                        System.out.println(Thread.currentThread().getName() + "---->" + example.getContent());
                    }
            );
            //0->4一共有5個線程
            thread.setName("線程" + i);
            thread.start();
        }
    }
}

我們看它的控制臺輸出結(jié)果:

----------------------------------------
----------------------------------------
----------------------------------------
線程4---->線程4的數(shù)據(jù)
----------------------------------------
線程1---->線程1的數(shù)據(jù)
線程2---->線程2的數(shù)據(jù)
----------------------------------------
線程3---->線程3的數(shù)據(jù)
線程0---->線程0的數(shù)據(jù)

通過兩個例子可以看到,通過引入ThreadLocal可以做到不同線程之前訪問變量的相互獨立性。

ThreadLocal和Synchronized關(guān)鍵字

Synchronized的同步方式

線程之間共享變量的相互隔離,我們首先想到的其實是Synchronized,我們通過下面的Synchronized也能夠?qū)崿F(xiàn)

public class ThreadLocalExample3 {

    @Getter
    @Setter
    private String content;

    public static void main(String[] args) {
        ThreadLocalExample3 example = new ThreadLocalExample3();
        for (int i = 0; i < 5; i++) {
            Thread thread = new Thread(
                    () -> {
                        //每個線程存一個變量,過一會兒取這個變量
                        synchronized (ThreadLocalExample3.class) {
                            example.setContent(Thread.currentThread().getName() + "的數(shù)據(jù)");
                            System.out.println("----------------------------------------");
                            System.out.println(Thread.currentThread().getName() + "---->" + example.getContent());
                        }
                    }
            );
            //0->4一共有5個線程
            thread.setName("線程" + i);
            thread.start();
        }
    }
}

上面的這段代碼也能起到線程對于共享變量的隔離效果,但是需要線程挨個排隊去實現(xiàn)業(yè)務(wù),這樣就失去了多線程并發(fā)執(zhí)行的意義了。

ThreadLocal和Synchronized的區(qū)別

雖然ThreadLocal模式和Synchronized關(guān)鍵字都用于多線程并發(fā)訪問變量的問題,不過兩者處理問題的角度和思路的不同的
|Synchronized|ThreadLocal|
---|----|--|--|
原理|同步機(jī)制采用了以時間換空間的方式,只提供了一份變量,讓不同線程排隊訪問|ThreadLocal采用了以空間換時間的方式,為每個線程都提供了一份變量的副本,從而實現(xiàn)同時訪問而互不干擾
側(cè)重點|多個線程之間訪問資源的同步|多個線程中讓每個線程之間的數(shù)據(jù)相互隔離

ThreadLocal和Synchronized都能解決問題,但是使用ThreadLocal更為合適,因為這樣可以讓程序擁有更高的并發(fā)性

ThreadLocal的使用場景與優(yōu)勢

使用場景

  • 如在銀行證券相互轉(zhuǎn)賬時,我們手動開啟事務(wù),直接獲取當(dāng)前線程綁定的連接對象,如果連接對象是空的再去連接池中獲取連接,將此連接對象跟當(dāng)前線程進(jìn)行綁定。

優(yōu)勢

  • 傳遞數(shù)據(jù): 保存每個線程綁定的數(shù)據(jù),在需要的地方可以直接獲取,避免參數(shù)直接傳遞帶來的代碼耦合性問題
  • 線程隔離: 各線程之間的數(shù)據(jù)相互隔離卻又具有并發(fā)性,避免同步方式帶來的性能損失

ThreadLocal的設(shè)計及源碼分析

設(shè)計

JDK1.8中ThreadLocal的設(shè)計原則是:每個Thread維護(hù)一個ThreadLocalMap,這個Map的key是ThreadLocal實例本身,value才是真正要存儲的值object,比如存儲的user對象等,如下圖所示。

過程如下:

  • 每個Thread線程內(nèi)部都有一個Map(ThreadLocalMap)
  • Map里面存儲ThreadLocal對象(key) 和線程的變量副本(value)
  • Thread內(nèi)部的Map是由ThreadLocal維護(hù)的,由ThreadLocal負(fù)責(zé)向map獲取和設(shè)置線程的變量值
  • 對于不同的線程,每次獲取副本值時,別的線程并不能獲取到當(dāng)前線程的副本值,形成副本的隔離,互不干擾

源碼分析

get 方法

public T get() {
    //獲取到當(dāng)前線程
    Thread t = Thread.currentThread();
    //每個線程內(nèi)都有一個ThreadLocalMap對象,獲取到當(dāng)前線程內(nèi)的 ThreadLocalMap 對象,
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        //獲取 ThreadLocalMap 中的 Entry 對象并拿到 Value值
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result;
        }
    }
    //如果這個線程之前沒創(chuàng)建過 ThreadLocalMap,就初始化一個ThreadLocalMap
    return setInitialValue();
}

getMap方法

//上面?zhèn)魅胍粋€Thread.currentThread(),這個方法就是獲取當(dāng)前線程內(nèi)的ThreadLocalMap對象
ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}

threadLocals

//這個對象的名字叫threadLocals
ThreadLocal.ThreadLocalMap threadLocals = null;

set方法

//獲取當(dāng)前線程的引用,把值給set進(jìn)去
public void set(T value) {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
    //這個線程之前沒創(chuàng)建過 ThreadLocalMap就創(chuàng)建一個
        createMap(t, value);
}

ThreadLocal使用注意事項

一個線程池,中間有個線程1號工作了一段時間,它的ThreadLocalMap里面裝了許多東西,裝完了之后如果沒有被清理掉,然后直接還到線程池里面,
任務(wù)不停的過來,然后都沒清理就會產(chǎn)生很大問題。因此在讀線程池源碼的時候,發(fā)現(xiàn)任務(wù)執(zhí)行完之后首先清理ThreadLocals,首先清理Map。

項目中使用遇到的問題

思考

  • ThreadLocal是不是用來解決共享資源的多線程訪問問題的?
    不是,ThreadLocal雖然可以用于解決多線程情況下的線程安全問題,但其資源不是共享的,而是每個線程獨享的。它解決并發(fā)資源的思路是在initialValue中new出自己線程獨享的資源,而多個線程之間,它們所訪問的對象本身是不共享的,自然就不存在任何并發(fā)問題。
  • ThreadLocal什么情況下會發(fā)生內(nèi)存泄漏?如何避免的?
    ThreadLocal內(nèi)存泄漏主要體現(xiàn)在key和value的內(nèi)存泄漏,ThreadLocal的消亡是伴隨著線程的,單純的將ThreadLocal置為空它底層的ThreadLocalMap
    的key會存在一個引用而不能釋放,因此會發(fā)生內(nèi)存泄漏。
    解決方法:
    1.key采用弱引用(ThreadLocal內(nèi)部解決)
    2.通過.remove的方式避免了value值的內(nèi)存泄漏

小結(jié)

本文主要介紹ThreadLocal的一些基本用法,以及它的設(shè)計和源碼分享,又介紹了它可能產(chǎn)生內(nèi)存泄漏體現(xiàn)的兩個方面,
因此每次在使用完之后都要做.remove操作。

本文由mdnice多平臺發(fā)布

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

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

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