亚洲乱码中文字幕综合,中国熟女仑乱hd,亚洲精品乱拍国产一区二区三区,一本大道卡一卡二卡三乱码全集资源,又粗又黄又硬又爽的免费视频

Java中ThreadLocal的用法及原理詳解

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

1 ThreadLocal簡介

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

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

2 ThreadLocal用法

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

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

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

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

/**
 * 典型場景1:每個(gè)線程需要一個(gè)獨(dú)享的對象
 * 利用ThreadLocal,給每個(gè)線程分配自己的dateFormat對象,保證了線程安全,高效利用了內(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 對象
        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è)對象
    // 底層map中存在鍵值對:(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é)果會打印出1000個(gè)不同的時(shí)間。

2.2 用法二:線程全局變量

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

  • 一種簡單的做法是:我們可以在相應(yīng)的函數(shù)中進(jìn)行參數(shù)傳遞但是這樣會導(dǎo)致代碼冗余且不易維護(hù),不可取。
  • 做法應(yīng)該是:使用ThreadLocal保存屬于每個(gè)線程的對象,然后通過ThreadLocal的 get 方法獲取屬于本線程的對象。
/**
 * 每個(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中存在鍵值對:(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è)存儲鍵值對Map容器,ThreadLocalMap中還有內(nèi)部類Entry,用于存儲每個(gè)鍵值對,其中鍵為 ThreadLocal 變量,值為用戶傳入的對象。關(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;
    // ....
}

接下來我們就可以探究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();  // 用法一 重寫了該方法,由多態(tài)可知,返回重寫的該函數(shù)的返回值
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);  // 得到當(dāng)前線程t的成員變量 threadLocals
        if (map != null)
            map.set(this, value);  // 向 threadLocals 中放入鍵值對, 關(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 中放入鍵值對, 關(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)前鍵值對,這里的鍵是this,也就是說調(diào)用get()方法的變量。對應(yīng)于用法一的 TSF.df ,對應(yīng)于用法二的 UserContextHolder.holder 。為空的話則調(diào)用 setInitialValue() ,該函數(shù)會將this作為鍵,重寫的 initialValue() 返回值作為值存入到 map 中。

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

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

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

4 ThreadLocal注意點(diǎn)

ThreadLocalMap

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

解決哈希沖突

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

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

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

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

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

每個(gè)Entry都包含了一個(gè)對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對應(yīng)的value就不能被回收,存在以下調(diào)用鏈:Thread---->ThreadLocalMap---->Entry(key為null)---->value。導(dǎo)致value無法回收,日積月累可能造成OOM。

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

為了避免產(chǎn)生內(nèi)存泄露問題,我們在使用完ThreadLocal之后,就應(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并在沒有重寫initialValue()方法后,直接調(diào)用 ThreadLocal 的 get()方法?

可以,只不過會返回 null 。

如下代碼演示了上述描述的問題:

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

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

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

相關(guān)文章

  • 詳解Spring boot使用Redis集群替換mybatis二級緩存

    詳解Spring boot使用Redis集群替換mybatis二級緩存

    本篇文章主要介紹了詳解Spring boot使用Redis集群替換mybatis二級緩存,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下
    2017-05-05
  • Spring?Boot項(xiàng)目傳參校驗(yàn)的最佳實(shí)踐指南

    Spring?Boot項(xiàng)目傳參校驗(yàn)的最佳實(shí)踐指南

    有參數(shù)傳遞的地方都少不了參數(shù)校驗(yàn),在web開發(fā)中前端的參數(shù)校驗(yàn)是為了用戶體驗(yàn),后端的參數(shù)校驗(yàn)是為了安全,下面這篇文章主要給大家介紹了關(guān)于Spring?Boot項(xiàng)目傳參校驗(yàn)的最佳實(shí)踐,需要的朋友可以參考下
    2022-04-04
  • Spring mvc Controller和RestFul原理解析

    Spring mvc Controller和RestFul原理解析

    這篇文章主要介紹了Spring mvc Controller和RestFul原理解析,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下
    2020-03-03
  • java中this的n種使用方法

    java中this的n種使用方法

    this可能是幾乎所有有一點(diǎn)面向?qū)ο笏枷氲恼Z言都會引用到的變量,this有多少種用法。下面小編給大家?guī)砹薺ava中this的n種使用方法,感興趣的朋友一起看看吧
    2018-08-08
  • Java動(dòng)態(tài)代理模式的深入揭秘

    Java動(dòng)態(tài)代理模式的深入揭秘

    這篇文章主要給大家介紹了關(guān)于Java動(dòng)態(tài)代理模式的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),對大家學(xué)習(xí)或者使用Java具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面來一起學(xué)習(xí)學(xué)習(xí)吧
    2019-08-08
  • Java基本語法之內(nèi)部類示例詳解

    Java基本語法之內(nèi)部類示例詳解

    本文帶大家認(rèn)識Java基本語法——內(nèi)部類,將一個(gè)類定義放在另一類的定義的內(nèi)部,這個(gè)就是內(nèi)部類,內(nèi)部類允許將一些邏輯相關(guān)的類組織在一起,并能夠控制位于內(nèi)部的類的可視性,感興趣的可以了解一下
    2022-03-03
  • SpringBoot服務(wù)端數(shù)據(jù)校驗(yàn)過程詳解

    SpringBoot服務(wù)端數(shù)據(jù)校驗(yàn)過程詳解

    這篇文章主要介紹了SpringBoot服務(wù)端數(shù)據(jù)校驗(yàn)過程詳解,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下
    2020-02-02
  • 淺析Java驗(yàn)證碼生成庫JCaptcha

    淺析Java驗(yàn)證碼生成庫JCaptcha

    JCaptcha 是一個(gè)用來生成驗(yàn)證碼的開源Java類庫,使用起來也是非常的簡單方便。本文通過代碼實(shí)例介紹了JCaptcha類庫。
    2016-07-07
  • SpringBoot實(shí)現(xiàn)接口返回?cái)?shù)據(jù)脫敏的代碼示例

    SpringBoot實(shí)現(xiàn)接口返回?cái)?shù)據(jù)脫敏的代碼示例

    在當(dāng)今的信息化時(shí)代,數(shù)據(jù)安全尤為重要,接口返回?cái)?shù)據(jù)脫敏是一種重要的數(shù)據(jù)保護(hù)手段,可以防止敏感信息通過接口返回給客戶端,本文旨在探討如何在SpringBoot應(yīng)用程序中實(shí)現(xiàn)接口返回?cái)?shù)據(jù)脫敏,需要的朋友可以參考下
    2024-07-07
  • java將指定目錄下文件復(fù)制到目標(biāo)文件夾的幾種小方法

    java將指定目錄下文件復(fù)制到目標(biāo)文件夾的幾種小方法

    在Java中有多種方法可以實(shí)現(xiàn)文件的復(fù)制,這篇文章主要給大家介紹了關(guān)于java將指定目錄下文件復(fù)制到目標(biāo)文件夾的幾種小方法,文中通過代碼介紹的非常詳細(xì),需要的朋友可以參考下
    2024-01-01

最新評論