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

線程局部變量的實現(xiàn)?ThreadLocal使用及場景介紹

 更新時間:2023年01月16日 09:43:57   作者:暮色妖嬈丶  
這篇文章主要為大家介紹了線程局部變量的實現(xiàn)?ThreadLocal使用及場景詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪

前言

回老家,實在太無聊,于是乎給自己整了一套臺式機配置,總價 1W+,本以為機器到位后可以打打游戲,學(xué)學(xué)技術(shù)打發(fā)無聊的時光。但是我早已不是從前那個少年了,打 Dota 已經(jīng)找不到大學(xué)時巔峰的自己,當(dāng)年我一手 SF 真的是打遍天下無敵手......,和朋友打 LOL 又沒有精力去學(xué)一個新的游戲,賊坑。。。

學(xué)技術(shù)又不想學(xué),太懶了!??!于是乎,寫文章吧...正好年后找工作用得上!今天我們來談一談 Java 中存儲線程局部變量的類 ThreadLocal 。

ThreadLocal 介紹

This class provides thread-local variables. These variables differ from their normal counterparts in that each thread that accesses one (via its {@code get} or {@code set} method) has its own, independently initialized copy of the variable. {@code ThreadLocal} instances are typically private static fields in classes that wish to associate state with a thread (e.g., a user ID or Transaction ID)

上面這段是該類的注釋:該提供線程局部變量。這些變量不同于它們正常的對應(yīng)變量,因為每個通過 get()、set() 訪問 ThreadLocal 變量的線程都有自己的、獨立初始化的變量副本。ThreadLocal 實例通常是類中的私有靜態(tài)字段,希望將狀態(tài)與線程關(guān)聯(lián)(例如,用戶ID或事務(wù)ID)。

簡單來說它的作用是作為一個數(shù)據(jù)結(jié)構(gòu),可以為每個線程分別存儲他們私有的數(shù)據(jù)。我們可以暫時簡單理解為下面這張圖(實際上這個圖是錯的)

后面我們會詳細(xì)介紹它的設(shè)計原理。

常用 API

方法作用
public ThreadLocal()實例化對象
ThreadLocal.withInitial(Supplier<? extends S> supplier )實例化對象并賦予它每個線程初始值
public void set(T value)設(shè)置當(dāng)前線程綁定的變量
public T get()獲取當(dāng)前線程綁定的變量
public void remove()移除當(dāng)前線程綁定的變量

ThreadLocal 使用場景

Spring 事務(wù)管理器

在 Spring 事務(wù)實現(xiàn)中,TransactionSynchronizationManager 類中聲明了多個 ThreadLocal 類型的成員變量用以將事務(wù)執(zhí)行過程中各種上下文信息綁定到當(dāng)前線程,包括當(dāng)前事務(wù)連接對象、是否可讀、事務(wù)名稱、隔離級別等

public abstract class TransactionSynchronizationManager {
   private static final ThreadLocal<Map<Object, Object>> resources = new NamedThreadLocal<>("Transactional resources");
   private static final ThreadLocal<Set<TransactionSynchronization>> synchronizations = new NamedThreadLocal<>("Transaction synchronizations");
   private static final ThreadLocal<String> currentTransactionName = new NamedThreadLocal<>("Current transaction name");
   private static final ThreadLocal<Boolean> currentTransactionReadOnly = new NamedThreadLocal<>("Current transaction read-only status");
   private static final ThreadLocal<Integer> currentTransactionIsolationLevel = new NamedThreadLocal<>("Current transaction isolation level");
   private static final ThreadLocal<Boolean> actualTransactionActive = new NamedThreadLocal<>("Actual transaction active");
   //......
  }

SpringMVC 存儲上下文 Request 數(shù)據(jù)

RequestContextHolder 這個類是 SpringMVC 中提供的持有上下文 Request 的一個類,內(nèi)部實現(xiàn)就是有兩個 ThreadLocal 屬性去存儲請求對象數(shù)據(jù)。

private static final ThreadLocal<RequestAttributes> requestAttributesHolder =
      new NamedThreadLocal<>("Request attributes");
private static final ThreadLocal<RequestAttributes> inheritableRequestAttributesHolder =
      new NamedInheritableThreadLocal<>("Request context");

以便于我們在業(yè)務(wù)代碼中在沒有 HttpServletRequest 對象的位置也可以通過 ThreadLocal 獲取請求頭等信息,比如我前面一篇關(guān)于 OpenFeign 向下游傳遞 header 的文章就用到了它。

PageHelper 分頁的實現(xiàn)

之前流行的分頁插件之一 PageHelper 其分頁原理也是通過 ThreadLocal 實現(xiàn),我們使用它進行分頁時只需要在代碼中調(diào)用靜態(tài)方法

PageHelper.startPage(pageNum,pageSize);

接下來的第一條 SQL 就會自動進行分頁,其實原理就是它將分頁參數(shù)封裝到一個 Page 對象中,然后將 Page 放進 ThreadLocal 中以達(dá)到 web 環(huán)境中多個線程互相分頁不影響,后面就是都雷同的 SQL 拼接了。

存儲用戶身份信息

在很久之前我們用戶登錄信息的存儲通常都是在 Session 中,后來大多是逐漸用 ThreadLocal 去代替從 Session 獲取用戶登錄信息了。首先我們在用戶每次請求需要授權(quán)的接口時,會讓用戶攜帶請求頭 token ,后端在攔截器中拿到這個 tokenredis 查詢用戶信息,或者如果這個 tokenjwt 的話,直接解析它得到用戶信息然后放進 ThreadLocal。

@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
    String header = request.getHeader("x-auth-token");
    //如果你的實現(xiàn)是 token 唯一字符串,從 Redis 拿用戶信息
    User user = redisTemplate.opsForValue().get(header);
    //如果你的實現(xiàn)是 token 是jwt,那直接解析 jwt 拿到用戶信息
    //.......
    if (user != null) {
        CurrentUser.set(user);
        return true;
    }
    return false;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
    CurrentUser.clear();//請求結(jié)束之后不要忘記清除
}

CurrentUser

public class CurrentUser {
    public static final ThreadLocal<User> USER_THREAD_LOCAL = new ThreadLocal<>();
    public static void set(User user){
        USER_THREAD_LOCAL.set(user);
    }
    public static User get(){
        return USER_THREAD_LOCAL.get();
    }
    public static void clear(){
        USER_THREAD_LOCAL.remove();
    }
}

這樣我們在任何地方只要使用 CurrentUser.get() 就能輕松獲取到當(dāng)前登錄用戶。

以上就是幾個 ThreadLocal 常見的場景,其核心理念就是利用 ThreadLocal 的線程隔離特性。

ThreadLocal 和 synchronized

值得注意的是 ThreadLocal 在解決線程安全問題上提供了一種不同于傳統(tǒng)并發(fā)安全的解決思路,傳統(tǒng)的 synchronized 或者 Lock 類是出于并發(fā)操作時讓多個線程排隊去訪問共享數(shù)據(jù),但是這樣的弊端就是會造成鎖競爭,這是以時間換空間。

ThreadLocal 將這個問題換了一個角度看待,既然并發(fā)安全的問題原因是因為多個線程共享一份數(shù)據(jù),那么我現(xiàn)在就讓每個線程都擁有一份獨立數(shù)據(jù),它們各自操作自己私有的本地變量,這樣就不會有并發(fā)安全問題,也沒有鎖競爭.但是每個線程都要維護一份數(shù)據(jù),會有額外的內(nèi)存開銷,這是以空間換時間。

實際項目中我們應(yīng)該用哪種方式,最終還是取決于業(yè)務(wù)場景更適合哪一種。

線程隔離的原理

ThreadLocal.set(T value) 源碼解讀

前面說了一些使用場景,這里我們探究一下 ThreadLocal 是如何實現(xiàn)線程隔離的。這里我們寫個極致簡單的例子

ThreadLocal<User> local = ThreadLocal.withInitial(User::new);
new Thread(() -> local.set(new User())).start();

這個例子只有兩行代碼,我們來看 local.set(T value) 方法的源碼

public void set(T value) {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        map.set(this, value);
    } else {
        createMap(t, value);
    }
}

代碼很簡單,首先拿到當(dāng)前線程,然后根據(jù)當(dāng)前線程拿到一個 Map 數(shù)據(jù)結(jié)構(gòu),將我們傳進來的值設(shè)置到這個 Map 中,那么重點就在這個 getMap() 中,查看其源碼

ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}

我們發(fā)現(xiàn)這個代碼就更簡單了,直接返回了當(dāng)前線程的一個成員變量,Thread 類中是這樣定義的

public class Thread implements Runnable {
    //...
    ThreadLocal.ThreadLocalMap threadLocals = null;
    //...
 }

從這里就可以明白它是如何實現(xiàn)線程隔離的,我們設(shè)置的值全都放進了當(dāng)前線程對象的一個成員變量中存著呢,那當(dāng)然是線程隔離的,你在哪個線程中去 set(),那就會保存到哪個線程對象的成員變量中。

接著我們再看這行代碼 map.set(this, value);,很重要,這里的 this 是什么?是當(dāng)前的 ThreadLocal<Integer> local; 對象,也就是說我們 set() 的值實際上是以當(dāng)前方法的調(diào)用者 localkey,傳入的值為 value 保存起來的鍵值對。簡單的理解為下圖

我們對于 ThreadLocal 的操作其實是對 Thread 的成員變量 threadLocals 進行操作。那么這個時候我們就要改變一下固有的思維,因為在正常的思維中,我們看到這行代碼 local.set(new User()); 腦海中浮現(xiàn)的第一印象都是向 local 的成員變量中進行一個數(shù)據(jù)的賦值,然而在 ThreadLocal 的實現(xiàn)中,這行代碼的意思是將 ThreadLocal 作為 key,傳入的值作為 value 存入到當(dāng)前 Thread 對象的一個成員變量中。

ThreadLocalMap

上面我們看到了 Thread 類中的成員變量是 ThreadLocalMap 類型的,ThreadLocal、Thread、ThreadLocalMap 三者的類圖關(guān)系為

首先由于我們程序中可能會聲明多個 ThreadLocal 對象,那么自然用于存放的數(shù)據(jù)結(jié)構(gòu)就需要類似集合,需要一個線程可以存儲多個以 ThreadLocalkey 數(shù)據(jù),考慮到查詢的時間復(fù)雜度以及各方面綜合考慮,Map 結(jié)構(gòu)再適合不過。

ThreadLocalMapThreadLocal 的一個靜態(tài)內(nèi)部類,它的內(nèi)部又聲明了一個靜態(tài)內(nèi)部類 Entry 來實現(xiàn) K/V,這一點類似于 HashMap。

static class Entry extends WeakReference<ThreadLocal<?>> {
    /** The value associated with this ThreadLocal. */
    Object value;
    Entry(ThreadLocal<?> k, Object v) {
        super(k);
        value = v;
    }
}

不同的是這里的 Entry 是 弱引用 WeakReference 的子類,那么在了解弱引用之后我們會發(fā)現(xiàn)在特定的場景下,如果不這么設(shè)計可能造成內(nèi)存泄漏。

弱引用

在分析內(nèi)存泄漏之前我們必須知道 Jvm 中幾種引用類型以及它們的特點。這里不詳細(xì)介紹,只說結(jié)論

引用類型回收機制
強引用我們程序中聲明的對象其引用都是強引用,只要其不指向 null ,GC 時候就不會被回收,即使內(nèi)存溢出
軟引用使用 SoftReference 類構(gòu)造一個軟引用,與強引用的區(qū)別是當(dāng)內(nèi)存不足,GC 會回收軟引用指向的對象
弱引用使用 WeakReference 類構(gòu)造一個弱引用,與軟引用的區(qū)別是,只要觸發(fā) GC 就會回收弱引用指向的對象

上面的結(jié)論都可以通過簡單的代碼來驗證,這里我們主要介紹結(jié)論。

內(nèi)存泄漏

內(nèi)存泄漏與內(nèi)存溢出

  • 內(nèi)存溢出 —— 程序中真的內(nèi)存不夠用了。
  • 內(nèi)存泄漏 —— 由于代碼問題導(dǎo)致程序中本該被釋放的內(nèi)存沒有被釋放,最終造成 “內(nèi)存不夠用” 的假象。

內(nèi)存圖

這里我們通過上面的兩行樣例代碼。

ThreadLocal<User> local = ThreadLocal.withInitial(User::new);
new Thread(() -> local.set(new User())).start();

結(jié)合分析 ThreadLocalMap、Thread、ThreadLocal 的源碼可以得到一張完整的內(nèi)存圖。

值得注意的是我們是沒有辦法直接聲明弱引用的,必須通過 WeakReference 去包裹一個對象持有弱引用,以下面代碼為例

WeakReference<User> wr = new WeakReference<>(new User());

它在內(nèi)存中是這樣的

所以完整的內(nèi)存圖應(yīng)該能夠理解。

為什么需要弱引用

使用反證法,假設(shè)我們的 Entry 不用弱引用,那么會出現(xiàn)這樣的情況,我們聲明出來的 ThreadLocal 對象,如果我們不想用它了或者說在程序中它的生命周期結(jié)束了(實際上這種場景很少,一般來說我們的 ThreadLocal 對象都是以 static final 的形式定義在全局,這里只是存在這個可能),想讓 GC 回收掉它占用的內(nèi)存,那么我們只需要讓沒有引用指向它即可 ,也就是將 1號線 干掉。

但是由于 ThreadLocalMap 里面也有持有我們聲明的 ThreadLocal 對象的強引用,如果我們想要回收的話就必須把這里的強引用也干掉,最好的方法是使用 remove() 方法移除。否則就需要干掉線程里面的 ThreadLocal.ThreadLocalMap threadLocals = null; 這個屬性,想要干掉這個屬性就得等線程銷毀,然而實際業(yè)務(wù)中有的線程是 24h 不間斷執(zhí)行的,也有的線程是位于線程池要被復(fù)用的,所以只要有一個線程不銷毀,這個 ThreadLocal 對象就不會被回收,這就會產(chǎn)生內(nèi)存泄漏。

但是如果這里 Entrykey 是弱引用,只要我們將 1號線 干掉,下次 GC 的時候發(fā)現(xiàn)這個 ThreadLocal 對象只有一個 2號線 弱引用指向它,就會將它回收掉。

public static void function1() {
    ThreadLocal<User> local = ThreadLocal.withInitial(User::new);
    local.set(new User());
    local.get();
    new Thread(() -> {
        local.set(new User());
        User user = local.get();
        while (true) {
            Thread.sleep(1000);
            System.out.println("測試");
        }
    }).start();
}

上面這段代碼如果 Entry 是強引用,當(dāng) function1() 結(jié)束之后 local 指向的內(nèi)存不會被回收,如果是弱引用,就會被回收。

remove() 防止內(nèi)存泄漏

ThreadLocal 為了防止內(nèi)存泄漏,已經(jīng)用弱引用幫我們解決了一大隱患,難道使用弱引用就能完全避免內(nèi)存泄漏嗎?并不是,還有一種情況,接著上面的章節(jié),當(dāng)我們 GC 將弱引用指向的 ThreadLocal 內(nèi)存回收之后, ThreadLocalMap 里面的 Entrykey 就變成 null 了,這樣我們就無法訪問到它原先對應(yīng)的 value,所以這個 value 將不會被回收,這才是實際場景中真正的內(nèi)存泄漏問題。

所以我們在用完之后一定需要手動的調(diào)用 remove() 清除當(dāng)前線程的局部變量值,也就是將對應(yīng)的 Entry(K/V) 刪掉,這樣即使后來 ThreadLocal 對象被回收,也不會造成內(nèi)存泄漏問題。

值得注意的是我們觀察 set()、get() 源碼會發(fā)現(xiàn)它其實都調(diào)用了一個方法

private int expungeStaleEntry(int staleSlot) {
//......
    if (k == null) {
        e.value = null;
        tab[i] = null;
        size--;
    }
//......
}

在每次操作的時候都會判斷是否存在 keynull 的鍵值對,如果存在就會刪掉,以此來盡量的避免內(nèi)存泄漏的問題,那這是不是意味著即使我們不手動 remove() 也可以呢?其實不然,因為實際業(yè)務(wù)中可能會出現(xiàn)長時間不調(diào)用 set()、get() 方法的情況,所以當(dāng)后面的流程里不再需要使用這個值得時候,手動 remove() 是一個好習(xí)慣,也是阿里巴巴規(guī)范里面的一個強制規(guī)定。

remove() 防止數(shù)據(jù)錯亂

實際 Web 項目中我們很多場景都會用到線程池,當(dāng)用完之后將線程對象歸還到線程池,如果沒有 remove() ,下個請求到來,這個線程被復(fù)用時發(fā)現(xiàn)這個數(shù)據(jù)已經(jīng)存在了,就直接拿過來用了,這個問題是很嚴(yán)重的,因為相當(dāng)于一個線程用了另一個線程的數(shù)據(jù),這會造成嚴(yán)重的業(yè)務(wù) bug 。

以上就是線程局部變量的實現(xiàn) ThreadLocal使用及場景介紹的詳細(xì)內(nèi)容,更多關(guān)于線程局部變量ThreadLocal的資料請關(guān)注腳本之家其它相關(guān)文章!

相關(guān)文章

  • Java中EnumMap的使用解析

    Java中EnumMap的使用解析

    這篇文章主要介紹了Java中EnumMap的使用解析,EnumMap?是一種特殊的?Map,它要求自身所有的鍵來自某個枚舉類型,EnumMap?的內(nèi)部可以作為一個數(shù)組來實現(xiàn),因此它們的性能非常好,你可以放心地用?EnumMap?來實現(xiàn)基于枚舉的查詢,需要的朋友可以參考下
    2023-11-11
  • spring boot參數(shù)驗證注解@NotNull、@NotBlank和@NotEmpty區(qū)別解析

    spring boot參數(shù)驗證注解@NotNull、@NotBlank和@NotEmpty區(qū)別解析

    使用spring boot參數(shù)驗證是常常會使用@NotNull、@NotBlank和@NotEmpty三個判斷是否不為空的注解,中文都有不能為空的意思,大部分使用者都傻傻分清它們之間到底有什么區(qū)別,今天就讓咱們來一起探索它們之間的不同吧,感興趣的朋友一起看看吧
    2024-05-05
  • 微信跳一跳刷分java代碼實現(xiàn)

    微信跳一跳刷分java代碼實現(xiàn)

    這篇文章主要介紹了微信跳一跳刷分java代碼實現(xiàn),還為大家分享了java刷微信跳一跳問題集,具有一定的參考價值,感興趣的小伙伴們可以參考一下
    2018-01-01
  • 實例解析Java關(guān)于static的作用

    實例解析Java關(guān)于static的作用

    只要是有學(xué)過Java的都一定知道static,也一定能多多少少說出一些作用和注意事項。文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧
    2019-04-04
  • 教你利用JAVA實現(xiàn)可以自行關(guān)閉服務(wù)器的方法

    教你利用JAVA實現(xiàn)可以自行關(guān)閉服務(wù)器的方法

    今天給大家?guī)淼氖顷P(guān)于Java的相關(guān)知識,文章圍繞著利用JAVA實現(xiàn)可以自行關(guān)閉服務(wù)器的方法展開,文中有非常詳細(xì)的介紹及代碼示例,需要的朋友可以參考下
    2021-06-06
  • Java實現(xiàn)對視頻進行截圖的方法【附ffmpeg下載】

    Java實現(xiàn)對視頻進行截圖的方法【附ffmpeg下載】

    這篇文章主要介紹了Java實現(xiàn)對視頻進行截圖的方法,結(jié)合實例形式分析了Java使用ffmpeg針對視頻進行截圖的相關(guān)操作技巧,并附帶ffmpeg.exe文件供讀者下載使用,需要的朋友可以參考下
    2018-01-01
  • Java自動取款機ATM案例實現(xiàn)

    Java自動取款機ATM案例實現(xiàn)

    本文主要介紹了Java自動取款機ATM案例實現(xiàn),整個過程可以分為三部分:登錄賬戶和執(zhí)行取款操作,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧
    2023-08-08
  • java多線程-讀寫鎖原理

    java多線程-讀寫鎖原理

    本文主要介紹java多線程的知識,這里整理了相關(guān)資料及簡單示例代碼,有興趣的小伙伴可以參考下
    2016-09-09
  • springboot學(xué)習(xí)之Thymeleaf模板引擎及原理介紹

    springboot學(xué)習(xí)之Thymeleaf模板引擎及原理介紹

    本文主要介紹一下SpringBoot給我們推薦的Thymeleaf模板引擎,這模板引擎呢,是一個高級語言的模板引擎,他的這個語法更簡單而且功能更強大,對springboot?Thymeleaf模板引擎相關(guān)知識感興趣的朋友一起看看吧
    2022-02-02
  • 關(guān)于SpringBoot配置文件加載位置的優(yōu)先級

    關(guān)于SpringBoot配置文件加載位置的優(yōu)先級

    這篇文章主要介紹了關(guān)于SpringBoot配置文件加載位置的優(yōu)先級,我們也可以通過spring.config.location來改變默認(rèn)的配置文件位置,項目打包好后,我們可以通過命令行的方式在啟動時指定配置文件的位置,需要的朋友可以參考下
    2023-10-10

最新評論