最新国产好看的视频,伊人天堂AV在线,国产Aaaaaa视频,蜜臀视频在线观看一区,人妻av色图,密臀久久久精品影片,青青视频免费观看毛片,久草在线观看视,国产三级精品色情在线

Java中ThreadLocal的用法及原理詳解

 更新時(shí)間:2023年09月26日 09:19:33   作者:你好世界wxx  
這篇文章主要介紹了Java中ThreadLocal的用法及原理詳解,在并發(fā)編程中,如果一個(gè)類變量被多個(gè)線程操作,會(huì)造成線程安全問(wèn)題,使用ThreadLocal可以讓每個(gè)線程擁有線程內(nèi)部的變量,防止多個(gè)線程操作一個(gè)類變量造成的線程安全問(wèn)題,需要的朋友可以參考下

1 ThreadLocal簡(jiǎn)介

ThreadLocal中文是:線程局部變量。

  • 為什么需要ThreadLocal呢?這是因?yàn)樵诓l(fā)編程中,如果一個(gè)類變量被多個(gè)線程操作,會(huì)造成線程安全問(wèn)題。例如多個(gè)線程使用同一個(gè) SimpleDateFormat 對(duì)象。使用ThreadLocal可以讓每個(gè)線程擁有線程內(nèi)部的變量,防止多個(gè)線程操作一個(gè)類變量造成的線程安全問(wèn)題。
  • 那是不是可以讓多線程中的每個(gè)任務(wù)都創(chuàng)建一個(gè)要用的對(duì)象呢?這樣做可以避免線程安全問(wèn)題,但是會(huì)造成資源的浪費(fèi)。例如我們要新建1000個(gè)格式化打印時(shí)間的任務(wù),每個(gè)任務(wù)中新建一個(gè) SimpleDateFormat 的對(duì)象:
    • 我們可以開(kāi)辟1000個(gè)線程分別執(zhí)行上述任務(wù),但這種做法太耗費(fèi)資源了,不可取;
    • 我們可以使用線程池,例如線程池中有10個(gè)線程,然后將這1000個(gè)任務(wù)放到線程池中執(zhí)行,這樣可以實(shí)現(xiàn)打印時(shí)間的目的,沒(méi)有線程安全問(wèn)題,但是新建1000個(gè) SimpleDateFormat 對(duì)象太浪費(fèi)了。
    • 最好的做法是每個(gè)線程中創(chuàng)建一個(gè) SimpleDateFormat 對(duì)象,這樣一共只需要?jiǎng)?chuàng)建10個(gè)該對(duì)象,即保證了線程安全,又節(jié)省了資源。

2 ThreadLocal用法

  • 用法一:每個(gè)線程需要一個(gè)獨(dú)享的對(duì)象。
  • 用法二:每個(gè)線程內(nèi)需要保存全局變量。

2.1 用法一:線程獨(dú)享對(duì)象

請(qǐng)創(chuàng)建1000個(gè)格式化打印時(shí)間的任務(wù)并執(zhí)行。

做法:使用線程池,線程池中開(kāi)辟10個(gè)線程,用這10個(gè)線程執(zhí)行這1000個(gè)任務(wù),為了防止出現(xiàn)線程安全問(wèn)題,使用 ThreadLocal 保證每個(gè)線程獨(dú)享一個(gè) SimpleDateFormat 對(duì)象,代碼如下:

/**
 * 典型場(chǎng)景1:每個(gè)線程需要一個(gè)獨(dú)享的對(duì)象
 * 利用ThreadLocal,給每個(gè)線程分配自己的dateFormat對(duì)象,保證了線程安全,高效利用了內(nèi)存
 */
public class Main1 {
    public static ExecutorService tp = Executors.newFixedThreadPool(10);
    public String date(int seconds) {
        SimpleDateFormat df = TSF.df.get();  // 獲取當(dāng)前線程擁有的 SimpleDateFormat 對(duì)象
        return df.format(new Date(1000 * seconds));
    }
    public static void main(String[] args) {
        for (int i = 0; i < 1000; i++) {
            int finalI = i;
            tp.submit(new Runnable() {
                @Override
                public void run() {
                    String date = new Main1().date(finalI);
                    System.out.println(date);
                }
            });
        }
        tp.shutdown();
    }
}
class TSF {  // ThreadSafeFormatter
    // 本類中定義的類變量都是線程內(nèi)部的,可以定義多個(gè)
    // 每個(gè)類變量的用法都是類似的,即:TSF.類變量名.get()    根據(jù)類變量名可以知道返回哪個(gè)對(duì)象
    // 底層map中存在鍵值對(duì):(UTSF.df, 該函數(shù)的返回值)
    public static ThreadLocal<SimpleDateFormat> df = new ThreadLocal<SimpleDateFormat>() {
        @Override
        protected SimpleDateFormat initialValue() {
            return new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
        }
    };
}

結(jié)果會(huì)打印出1000個(gè)不同的時(shí)間。

2.2 用法二:線程全局變量

每個(gè)線程都會(huì)牽涉到三個(gè)服務(wù)類:Service1、Service2、Service3,這三個(gè)類中都會(huì)使用到同一個(gè)對(duì)象。同一個(gè)進(jìn)程內(nèi)部這是一個(gè)對(duì)象,不同進(jìn)程之間對(duì)象不同,請(qǐng)實(shí)現(xiàn)該需求。

  • 一種簡(jiǎn)單的做法是:我們可以在相應(yīng)的函數(shù)中進(jìn)行參數(shù)傳遞但是這樣會(huì)導(dǎo)致代碼冗余且不易維護(hù),不可取。
  • 做法應(yīng)該是:使用ThreadLocal保存屬于每個(gè)線程的對(duì)象,然后通過(guò)ThreadLocal的 get 方法獲取屬于本線程的對(duì)象。
/**
 * 每個(gè)線程內(nèi)需要保存全局變量
 * 同一個(gè)線程內(nèi)該全局信息相同,不同線程間該全局信息不同
 * 如下兩個(gè)線程,線程1保存全局用戶"wxx",線程2保存全局用戶"she"
 */
public class Main2 {
    public static void main(String[] args) throws Exception {
        new Thread(() -> new Service1().process("wxx")).start();
        Thread.sleep(100);
        new Thread(() -> new Service1().process("she")).start();
    }
}
class Service1 {  // Service1 調(diào)用 Service2
    public void process(String name) {
        User user = new User(name);
        UserContextHolder.holder.set(user);  // 底層map中存在鍵值對(duì):(UserContextHolder.holder, user)
        System.out.println("Service1:" + user.name);
        new Service2().process();
    }
}
class Service2 {  // Service2 調(diào)用 Service3
    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);
    }
}
class UserContextHolder {  // 本類中定義的類變量都是線程內(nèi)部的,可以定義多個(gè)
    public static ThreadLocal<User> holder = new ThreadLocal<>();
}
class User {
    String name;
    public User(String name) {
        this.name = name;
    }
}

結(jié)果:

Service1:wxx
Service2:wxx
Service3:wxx
Service1:she
Service2:she
Service3:she

3 ThreadLocal原理

  • 首先我們應(yīng)該明確如下類之間的關(guān)系:ThreadLocal、ThreadLocalMap、Thread。
  • ThreadLocalMap 是 ThreadLocal的內(nèi)部類。ThreadLocalMap是一個(gè)存儲(chǔ)鍵值對(duì)Map容器,ThreadLocalMap中還有內(nèi)部類Entry,用于存儲(chǔ)每個(gè)鍵值對(duì),其中鍵為 ThreadLocal 變量,值為用戶傳入的對(duì)象。關(guān)系如下:

在這里插入圖片描述

現(xiàn)在搞清楚了ThreadLocal、ThreadLocalMap之間的關(guān)系,那這兩個(gè)和Thread是什么關(guān)系呢?答案是:Thread中有一個(gè) ThreadLocal.ThreadLocalMap 的變量。如下圖:

在這里插入圖片描述

public class Thread implements Runnable {
    // ...
    /* ThreadLocal values pertaining to this thread. This map is maintained
     * by the ThreadLocal class. */
    ThreadLocal.ThreadLocalMap threadLocals = null;
    // ....
}

接下來(lái)我們就可以探究ThreadLocal到底是如何獲取屬于線程內(nèi)部的變量的,關(guān)鍵在于探究ThreadLocal的 get() 方法。該函數(shù)如下:

public class ThreadLocal<T> {
    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();
    }
}

該函數(shù)中使用到了 getMap 和 setInitialValue 兩個(gè)函數(shù),這兩個(gè)函數(shù)的定義如下:

public class ThreadLocal<T> {
    private T setInitialValue() {
        T value = initialValue();  // 用法一 重寫(xiě)了該方法,由多態(tài)可知,返回重寫(xiě)的該函數(shù)的返回值
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);  // 得到當(dāng)前線程t的成員變量 threadLocals
        if (map != null)
            map.set(this, value);  // 向 threadLocals 中放入鍵值對(duì), 關(guān)鍵!!!
        else
            createMap(t, value);
        return value;
    }
    public void set(T value) {  // 用法二調(diào)用了該方法
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);  // 向 threadLocals 中放入鍵值對(duì), 關(guān)鍵!!!
        else
            createMap(t, value);
    }
    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }
    void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }
}

分析 get() 函數(shù)的執(zhí)行流程:

(1)獲取當(dāng)前線程 t ,然后調(diào)用 getMap(t) ,從而得到屬于當(dāng)前線程 t 的ThreadLocalMap變量 map ;

(2)然后判斷屬于當(dāng)前線程 t 的 map 是否為空,不空的話從 map 中取出當(dāng)前鍵值對(duì),這里的鍵是this,也就是說(shuō)調(diào)用get()方法的變量。對(duì)應(yīng)于用法一的 TSF.df ,對(duì)應(yīng)于用法二的 UserContextHolder.holder 。為空的話則調(diào)用 setInitialValue() ,該函數(shù)會(huì)將this作為鍵,重寫(xiě)的 initialValue() 返回值作為值存入到 map 中。

(3)返回 this 對(duì)象對(duì)應(yīng)的值。

無(wú)論是用法一,還是用法二,其實(shí)本質(zhì)上都在操縱 當(dāng)前線程 t 的成員變量 threadLocals 。

根據(jù)上述 get() 分析的第(2)點(diǎn),當(dāng)我們 new ThreadLocal<>(); 時(shí)并沒(méi)有向 ThreadLocalMap 中存入鍵值對(duì),只有當(dāng)調(diào)用 get()、set() 方法時(shí)才放入鍵值對(duì),這是懶加載的一種體現(xiàn)。

4 ThreadLocal注意點(diǎn)

ThreadLocalMap

  • ThreadLocalMap 和 HashMap 類似,關(guān)于 HashMap 的詳細(xì)分析,可以參考:HashMap源碼分析。
  • 兩者也有不少區(qū)別:
    • 兩者解決哈希沖突的方式不同;
    • ThreadLocalMap中的鍵值對(duì),其中鍵為軟引用,值為強(qiáng)引用,但HashMap中鍵值都為強(qiáng)引用。

解決哈希沖突

  • ThreadLocalMap采用的是線性探測(cè)法,也就是如果發(fā)生沖突,就繼續(xù)找下一個(gè)空位置;
  • HashMap采用拉鏈法(鏈表+紅黑樹(shù))。

ThreadLocalMap中節(jié)點(diǎn)的鍵值對(duì)

如果弱引用對(duì)象只與弱引用關(guān)聯(lián),則這個(gè)弱引用對(duì)象可以被回收。

ThreadLocalMap中的Entry繼承自WeakReference,是弱引用;

每一個(gè)Entry都是對(duì)key的弱引用;

每個(gè)Entry都包含了一個(gè)對(duì)value的強(qiáng)引用;

value為強(qiáng)引用的原因:因?yàn)镴VM認(rèn)為這個(gè)引用十分重要,是程序員定義的,不能隨意回收,回收之后可能發(fā)生異響不到的錯(cuò)誤;

因?yàn)橹祐alue是強(qiáng)引用,所以可能導(dǎo)致內(nèi)存泄露,最終導(dǎo)致OOM,這是因?yàn)椋喝绻€程不終止(比如線程需要保持很久),那么key對(duì)應(yīng)的value就不能被回收,存在以下調(diào)用鏈:Thread---->ThreadLocalMap---->Entry(key為null)---->value。導(dǎo)致value無(wú)法回收,日積月累可能造成OOM。

JDK已經(jīng)考慮到了這個(gè)問(wèn)題,所以在Entry的set,remove,rehash方法中會(huì)掃描key為null的Entry,并把對(duì)應(yīng)的value設(shè)置為null,這樣value對(duì)象就可以被回收。但是這樣做還不足夠,因?yàn)槲覀儽仨氄{(diào)用這些方法才能達(dá)到上述效果。

為了避免產(chǎn)生內(nèi)存泄露問(wèn)題,我們?cè)谑褂猛闠hreadLocal之后,就應(yīng)該調(diào)用remove方法(阿里規(guī)約)。例如用法二中 Service3 應(yīng)該改為:

class Service3 {
    public void process() {
        User user = UserContextHolder.holder.get();
        System.out.println("Service3:" + user.name);
        UserContextHolder.holder.remove();  // 防止內(nèi)存泄露
    }
}

我們可不可以在新建ThreadLocal并在沒(méi)有重寫(xiě)initialValue()方法后,直接調(diào)用 ThreadLocal 的 get()方法?

可以,只不過(guò)會(huì)返回 null 。

如下代碼演示了上述描述的問(wèn)題:

public class ThreadLocalNPE {
    ThreadLocal<Long> tl = new ThreadLocal<>();
//    public void set() {
//        tl.set(Thread.currentThread().getId());
//    }
    public long get() {  // 返回值改為 Long 就沒(méi)有NPE異常了
        return tl.get();  // tl.get() 為 null
    }
    public static void main(String[] args) {
        ThreadLocalNPE main = new ThreadLocalNPE();
        // 不進(jìn)行set,直接get
        main.get();
    }
}

上述代碼會(huì)拋出java.lang.NullPointerException異常,這不是因?yàn)間et()的原因,而是因?yàn)椋翰鹣鋾r(shí)null不能轉(zhuǎn)為基本類型。當(dāng)返回值改為 Long 就沒(méi)有NPE異常了。

到此這篇關(guān)于Java中ThreadLocal的用法及原理詳解的文章就介紹到這了,更多相關(guān)ThreadLocal的用法及原理內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!

相關(guān)文章

  • 使用stream的Collectors.toMap()方法常見(jiàn)的問(wèn)題及解決

    使用stream的Collectors.toMap()方法常見(jiàn)的問(wèn)題及解決

    這篇文章主要介紹了使用stream的Collectors.toMap()方法常見(jiàn)的問(wèn)題及解決方案,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教
    2023-03-03
  • 實(shí)例講解JAVA設(shè)計(jì)模式之備忘錄模式

    實(shí)例講解JAVA設(shè)計(jì)模式之備忘錄模式

    這篇文章主要介紹了JAVA設(shè)計(jì)模式之備忘錄模式的的相關(guān)資料,文中示例代碼非常詳細(xì),供大家參考和學(xué)習(xí),感興趣的朋友可以了解下
    2020-06-06
  • Java中Objects.equals踩坑記錄

    Java中Objects.equals踩坑記錄

    最近在工作中發(fā)現(xiàn)一個(gè)問(wèn)題,覺(jué)著還是挺有必要記錄下的,這篇文章主要給大家介紹了關(guān)于Java中Objects.equals踩坑的相關(guān)資料,需要的朋友可以參考下
    2022-03-03
  • 詳解Java異常處理最佳實(shí)踐及陷阱防范

    詳解Java異常處理最佳實(shí)踐及陷阱防范

    這篇文章主要介紹了Java異常處理最佳實(shí)踐及陷阱防范,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧
    2019-04-04
  • Spring IOC簡(jiǎn)單理解及創(chuàng)建對(duì)象的方式

    Spring IOC簡(jiǎn)單理解及創(chuàng)建對(duì)象的方式

    這篇文章主要介紹了Spring IOC簡(jiǎn)單理解及創(chuàng)建對(duì)象的方式,本文通過(guò)兩種方式給大家介紹創(chuàng)建對(duì)象的方法,通過(guò)實(shí)例代碼給大家介紹的非常詳細(xì),需要的朋友可以參考下
    2021-09-09
  • Springboot+Mybatis實(shí)現(xiàn)分頁(yè)加條件查詢功能

    Springboot+Mybatis實(shí)現(xiàn)分頁(yè)加條件查詢功能

    這篇文章主要為大家詳細(xì)介紹了Springboot+Mybatis實(shí)現(xiàn)分頁(yè)加條件查詢,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下
    2022-04-04
  • idea如何關(guān)閉頁(yè)面顯示的瀏覽器圖標(biāo)

    idea如何關(guān)閉頁(yè)面顯示的瀏覽器圖標(biāo)

    這篇文章主要介紹了idea如何關(guān)閉頁(yè)面顯示的瀏覽器圖標(biāo)問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教
    2023-07-07
  • Nexus存儲(chǔ)庫(kù)管理器搭建-Maven私服詳解

    Nexus存儲(chǔ)庫(kù)管理器搭建-Maven私服詳解

    這篇文章主要介紹了Nexus存儲(chǔ)庫(kù)管理器搭建-Maven私服,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教
    2025-06-06
  • 在SpringBoot接口中正確地序列化時(shí)間字段的方法

    在SpringBoot接口中正確地序列化時(shí)間字段的方法

    文章主要介紹在 Spring Boot 接口中正確序列化時(shí)間字段的方法,包括 Java 中Date和LocalDateTime類型的區(qū)別,JSON 序列化和請(qǐng)求參數(shù)中時(shí)間字段的處理,如時(shí)間字符串的格式配置、時(shí)間戳的使用及相關(guān)配置,還提到了在 Swagger UI 中的類型設(shè)置,需要的朋友可以參考下
    2024-11-11
  • Java RPC框架熔斷降級(jí)機(jī)制原理解析

    Java RPC框架熔斷降級(jí)機(jī)制原理解析

    這篇文章主要介紹了Java RPC框架熔斷降級(jí)機(jī)制原理解析,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下
    2020-02-02

最新評(píng)論

中江县| 黎城县| 静海县| 内江市| 吴旗县| 偏关县| 邵阳市| 昌宁县| 贵阳市| 台湾省| 博客| 潞城市| 石景山区| 克山县| 屯门区| 株洲县| 萍乡市| 新蔡县| 常州市| 贵溪市| 岳普湖县| 平谷区| 精河县| 沾化县| 万载县| 仪征市| 澜沧| 依安县| 荔浦县| 双辽市| 垫江县| 通河县| 新源县| 马公市| 昌邑市| 德钦县| 青浦区| 宜兰县| 灵川县| 岐山县| 福安市|