欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

Java多線程之ThreadLocal淺析

 更新時間:2023年04月21日 11:21:29   作者:Mcsirv  
這篇文章主要分析了Java多線程ThreadLocal,ThreadLocal叫做線程變量,用于在多線程環(huán)境下創(chuàng)建線程本地變量。想了解更多的可以參考本文

介紹

什么是ThreadLocal?

ThreadLocal叫做線程變量,用于在多線程環(huán)境下創(chuàng)建線程本地變量。

通俗的講,ThreadLocal可以讓你在同一個線程中創(chuàng)建一個變量,并且這個變量對于該線程是唯一的,其他線程無法訪問到這個變量。

這種方式能夠有效地避免多線程之間的變量沖突問題,使得線程本地變量的訪問變得更加安全和高效。

例如,在一個線程池中,每個線程需要維護自己的狀態(tài),這時就可以使用ThreadLocal來創(chuàng)建線程本地變量來存儲狀態(tài)信息。

ThreadLocal 的作用是什么?

在多線程編程中,由于不同線程之間共享內(nèi)存,如果多個線程訪問同一個變量,就會發(fā)生競爭條件,可能會導(dǎo)致數(shù)據(jù)不一致或者死鎖等問題。使用ThreadLocal可以解決這個問題,因為它可以為每個線程創(chuàng)建一個獨立的變量副本,每個線程都可以訪問自己的變量副本,而不會影響其他線程的變量。這種方式可以有效地避免多線程之間的變量沖突問題,提高了程序的可靠性和性能。ThreadLocal常用于實現(xiàn)線程安全的單例模式,以及在多線程環(huán)境下對共享數(shù)據(jù)的緩存。

如何使用ThreadLocal

如何創(chuàng)建一個ThreadLocal實例

直接上代碼:

public class ThreadLocalDemo {  
    private static ThreadLocal<String> threadLocal = new ThreadLocal<>();  
  
    public static void main(String[] args) {  
        new Thread(() -> {  
            System.out.println("thread1 before set: " + threadLocal.get());  
            threadLocal.set("AAAAA");  
            System.out.println("thread1 after set: " + threadLocal.get());  
            threadLocal.remove();  
            System.out.println("thread1 after remove: " + threadLocal.get());  
        }).start();  

        new Thread(() -> {  
            System.out.println("thread1 before set: " + threadLocal.get());  
            threadLocal.set("BBBBB");  
            System.out.println("thread2 after set: " + threadLocal.get());  
            threadLocal.remove();  
            System.out.println("thread2 after remove: " + threadLocal.get());  
        }).start();  

        System.out.println("main thread before set: " + threadLocal.get());  
        threadLocal.set("Main");  
        System.out.println("main after set: " + threadLocal.get());  
        threadLocal.remove();  
        System.out.println("main after remove: " + threadLocal.get());  
    }  
}

程序輸出:

thread1 before set: null
main thread before set: null
main after set: Main
thread1 before set: null
thread1 after set: AAAAA
thread1 after remove: null
thread2 after set: BBBBB
thread2 after remove: null
main after remove: null

創(chuàng)建ThreadLocal實例的方式非常簡單,只需要使用Java中的ThreadLocal類的構(gòu)造函數(shù)即可。

上面的代碼創(chuàng)建了一個ThreadLocal實例,該實例可以存儲String類型的值。在使用ThreadLocal之前,需要先調(diào)用它的set()方法來初始化一個線程本地變量, 否則get()方法得到的值就是null。

從代碼中可以看到, 我們在main方法中分別創(chuàng)建了2個線程, 三個線程分表獲取了自己線程存放的變量,他們之間變量的獲取并不會錯亂。

如果在當前線程中尚未設(shè)置該值或者已經(jīng)調(diào)用remove()方法刪除值,則返回null。

需要注意的是,每個ThreadLocal對象只能存儲一個值,如果需要存儲多個值,則需要創(chuàng)建多個ThreadLocal對象。

ThreadLocal與Synchronized的區(qū)別

ThreadLocal和Synchronized都是Java中用于處理多線程并發(fā)訪問的工具,但它們的作用和實現(xiàn)方式有很大的區(qū)別。

作用不同:ThreadLocal主要是用來創(chuàng)建線程本地變量,解決多線程并發(fā)訪問時的變量沖突問題;而Synchronized則是一種同步機制,用于保護共享資源,防止多線程之間的競爭條件。

  • 實現(xiàn)方式不同:ThreadLocal通過為每個線程創(chuàng)建獨立的變量副本,使得每個線程之間互不干擾,從而解決多線程訪問共享變量時的線程安全問題。而Synchronized則是通過互斥訪問來實現(xiàn)同步的,即多個線程同時只能有一個線程訪問共享資源。

  • 應(yīng)用場景不同:ThreadLocal適用于需要在多個線程中使用獨立的變量的場景,如線程池中的線程狀態(tài)管理,以及Web應(yīng)用中的Session管理等;而Synchronized則適用于需要保護共享資源的場景,如多個線程同時訪問同一個數(shù)據(jù)結(jié)構(gòu),或者需要保證某個方法在同一時刻只能被一個線程訪問等。

  • 性能影響不同:ThreadLocal相對于Synchronized來說性能更好,因為它只涉及到線程本地變量的訪問和賦值操作,不需要進行鎖競爭和上下文切換等操作。而Synchronized則需要進行鎖競爭和上下文切換等操作,會對性能產(chǎn)生一定的影響。

ThreadLocal的優(yōu)點:

  • 線程安全:每個線程都擁有自己的變量副本,不會受到其他線程的影響,可以避免線程安全問題。
  • 性能高:ThreadLocal使用了空間換時間的方式,每個線程都有自己的變量副本,不需要進行加鎖和解鎖操作,因此性能更高。
  • 代碼簡潔:使用ThreadLocal可以避免復(fù)雜的同步控制邏輯。

加鎖的優(yōu)點:

  • 保證數(shù)據(jù)一致性:通過加鎖可以保證共享資源在多線程環(huán)境下的正確性,避免出現(xiàn)數(shù)據(jù)不一致的情況。
  • 線程同步:在加鎖過程中,線程會被阻塞,等待鎖的釋放,保證了線程同步。

ThreadLocal的缺點:

  • 內(nèi)存泄漏:ThreadLocal使用靜態(tài)的內(nèi)部Map來存儲變量副本,如果不及時清理,會導(dǎo)致內(nèi)存泄漏問題(后續(xù)展開介紹)。
  • 難以調(diào)試:由于每個線程都有自己的變量副本,因此在調(diào)試過程中,需要考慮多個線程的情況,會增加調(diào)試的難度。

加鎖的缺點:

  • 性能問題:在高并發(fā)情況下,加鎖會導(dǎo)致線程的阻塞,從而影響系統(tǒng)的性能。
  • 容易導(dǎo)致死鎖:如果加鎖的操作不正確,可能會導(dǎo)致死鎖問題,需要謹慎使用。

綜合來看,ThreadLocal適合處理線程私有的數(shù)據(jù),而加鎖適合處理共享的資源,具體應(yīng)該根據(jù)業(yè)務(wù)需求來選擇。

ThreadLocal的實現(xiàn)原理

ThreadLocal的內(nèi)部數(shù)據(jù)結(jié)構(gòu)

直接查看源碼:

Thread類:

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

    //用于父子線程變量同步, 后續(xù)介紹
    ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
}

ThreadLocal的set()方法:

    public void set(T value) {  
        //獲取當前線程
        Thread t = Thread.currentThread();
        //封裝方法從獲取線程中的ThreadLocalMap
        //為什么封裝方法呢? 為了后面擴展inheritableThreadLocals
        ThreadLocalMap map = getMap(t);  
        //之前有創(chuàng)建過, 直接set
        if (map != null)  
            map.set(this, value);  
        else  
            //之前沒有創(chuàng)建, 新建Map并設(shè)置值
            createMap(t, value);  
    }

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

從源碼中我們可以看到, 在set方法中, 我們先是獲取到當前線程, 然后以當前線程為入?yún)⒄{(diào)用getMap方法, 并獲取thread線程中的ThreadLocalMap屬性。如果map屬性不為空,則直接更新value值,如果map為空,則實例化threadLocalMap, 并將value值初始化。

那么threadLocalMap又是什么呢? 我們接著往下看。

ThreadLocalMap和ThreadLocalMap.Entry的實現(xiàn)

public class ThreadLocal<T> {

    static class ThreadLocalMap {  
        
        //繼承弱應(yīng)用, 方便垃圾回收
        static class Entry extends WeakReference<ThreadLocal<?>> {  

            /** The value associated with this ThreadLocal. */  
            Object value;  

            Entry(ThreadLocal<?> k, Object v) {  
                super(k);  
                value = v;  
            }  
        }
        //數(shù)組, 用于存儲多組數(shù)據(jù)
        private Entry[] table;
    }
}

從代碼我們可以看到, threadLocalMap是ThreadLocal中的一個靜態(tài)內(nèi)部類, 在threadLocalMap又維護了一個名叫table的Entry數(shù)組。

Entry是什么呢?

Entry是一組組數(shù)據(jù)對, 而且繼承的弱引用。在Entry內(nèi)部使用ThreadLocal作為key,使用我們設(shè)置的value作為value。

key 就是 ThreadLocal,肯定不為空,但也是弱引用的。

也就是說,當 key 為 null 時,說明 ThreadLocal 已經(jīng)被回收了,對應(yīng)的 Entry 就應(yīng)該被清除了。

ThreadLocalMap.set()方法

private void set(ThreadLocal<?> key, Object value) {
    Entry[] tab = table;
    int len = tab.length;
    //根據(jù)hashCode與長度計算索引位置
    int i = key.threadLocalHashCode & (len-1);

    for (Entry e = tab[i];
         e != null;
         // 如果下標沖突, 索引+1繼續(xù)查找
         e = tab[i = nextIndex(i, len)]) {
        ThreadLocal<?> k = e.get();
        
        //找到直接返回值
        if (k == key) {
            e.value = value;
            return;
        }

        if (k == null) {
            // key 為空, 說明 對應(yīng)的 ThreadLocal 已經(jīng)回收了.
            // 可以復(fù)用當前位置.
            // 有兩種情況:1\. entry 存在, 在這個過時位置的后面. 所以需要置換到這個位置
            // 2.不存在, 直接放到這個位置
            replaceStaleEntry(key, value, i);
            // 因為是替換, 所以size 要么不變,要么減少。
            return;
        }
    }

    // 沒找到已存在的, 也沒找到可以替換的過時. 則直接新建
    tab[i] = new Entry(key, value);
    int sz = ++size;
    if (!cleanSomeSlots(i, sz) && sz >= threshold)
        // 如果沒有清除過時 entry, 并且超過閾值. 則進行先嘗試縮小,不行則擴容
        rehash();
}

在ThreadLocalMap中的set方法與構(gòu)造方法能看到以下代碼片段。

  • int i = key.threadLocalHashCode & (len-1)
  • int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1)

簡而言之就是將threadLocalHashCode進行一個位運算(取模)得到索引i,threadLocalHashCode代碼如下。

public class ThreadLocal<T> {  
    private final int threadLocalHashCode = nextHashCode();  

    private static AtomicInteger nextHashCode =  new AtomicInteger(); 
    private static final int HASH_INCREMENT = 0x61c88647;  

    /**  
    * Returns the next hash code.  
    */  
    private static int nextHashCode() {
        //自增
        return nextHashCode.getAndAdd(HASH_INCREMENT);  
    }
}

因為static的原因,在每次new ThreadLocal()時因為threadLocalHashCode的初始化,會使threadLocalHashCode值自增一次,增量為0x61c88647。

0x61c88647是斐波那契散列乘數(shù),它的優(yōu)點是通過它散列(hash)出來的結(jié)果分布會比較均勻,可以很大程度上避免hash沖突。

有興趣可以深入研究下去, 這里就不過多贅述了, 這里這樣運算就是為了避免索引下標沖突。

總結(jié)一下:

  • 對于某一ThreadLocal來講,他的索引值i是確定的,在不同線程之間訪問時訪問的是不同的table數(shù)組的同一位置即都為table[i],只不過這個不同線程之間的table是獨立的。

  • 對于同一線程的不同ThreadLocal來講,這些ThreadLocal實例共享一個table數(shù)組,然后每個ThreadLocal實例在table中的索引i是不同的。

ThreadLocalMap.get()方法

public T get() {  
    //獲取當前線程
    Thread t = Thread.currentThread();
    //獲取ThreadLocalMap
    ThreadLocalMap map = getMap(t);  
    if (map != null) {
        //通過ThreadLocal獲取Entry
        ThreadLocalMap.Entry e = map.getEntry(this);
        //返回值
        if (e != null) {  
            @SuppressWarnings("unchecked")  
            T result = (T)e.value;  
            return result;  
        }  
    }
    //設(shè)置初始值--null
    return setInitialValue();  
}

private Entry getEntry(ThreadLocal<?> key) {
    //計算下標, 通過下標從Entry數(shù)組中直接取值
    int i = key.threadLocalHashCode & (table.length - 1);  
    Entry e = table[i];  
    if (e != null && e.get() == key)  
        return e;  
    else
        //索引沖突導(dǎo)致沒有查找到, 繼續(xù)查找
        return getEntryAfterMiss(key, i, e);  
}

理解了set方法,get方法也就清楚明了,直接通過計算出索引直接從數(shù)組對應(yīng)位置讀取即可。

ThreadLocalMap.remove()方法

public void remove() {  
    ThreadLocalMap m = getMap(Thread.currentThread());  
    if (m != null)  
        m.remove(this);  
}

ThreadLocal的垃圾回收機制

ThreadLocal對象的垃圾回收機制比較特殊,主要涉及到兩個對象:ThreadLocal對象和ThreadLocalMap對象。

每個ThreadLocal對象都會在當前線程的ThreadLocalMap中創(chuàng)建一個Entry對象,這個Entry對象包含了ThreadLocal對象和其對應(yīng)的值。當ThreadLocal對象沒有被其他對象引用,并且當前線程結(jié)束時,這個ThreadLocal對象會被標記為可回收的,并且被添加到一個特殊的ReferenceQueue中。

當垃圾回收器掃描到ReferenceQueue中的ThreadLocal對象時,它會將ThreadLocal對象對應(yīng)的Entry對象從ThreadLocalMap中刪除,并且清除Entry對象中對ThreadLocal對象和值的引用,從而使得ThreadLocal對象和值都能夠被回收。

需要注意的是,雖然ThreadLocal對象被回收了,但是它在ThreadLocalMap中對應(yīng)的Entry對象并沒有被立即清除,只有在下一次調(diào)用ThreadLocalMap的set()、get()或remove()方法時才會觸發(fā)Entry對象的清除操作。這是因為ThreadLocalMap中的Entry對象使用了弱引用,只有在下一次調(diào)用ThreadLocalMap時才會被垃圾回收器掃描到并被清除。

因此,使用ThreadLocal對象時需要注意,在不再需要使用ThreadLocal對象時,應(yīng)該及時調(diào)用remove()方法,以便及時清除ThreadLocalMap中對應(yīng)的Entry對象,從而避免內(nèi)存泄漏。

ThreadLocal的使用場景

參數(shù)透傳

當我們在寫API接口的時候,通常Controller層會接受來自前端的入?yún)?,當這個接口功能比較復(fù)雜的時候,可能我們調(diào)用的Service層內(nèi)部還調(diào)用了很多其他的很多方法,通常情況下,我們會在每個調(diào)用的方法上加上需要傳遞的參數(shù)。

但是如果我們將參數(shù)存入ThreadLocal中,那么就不用顯式的傳遞參數(shù)了,而是只需要ThreadLocal中獲取即可。

這個場景其實使用的比較少,一方面顯式傳參比較容易理解,另一方面我們可以將多個參數(shù)封裝為對象去傳遞。

全局存儲用戶信息(項目中用到)

在現(xiàn)在的系統(tǒng)設(shè)計中,前后端分離已基本成為常態(tài),分離之后如何獲取用戶信息就成了一件麻煩事,通常在用戶登錄后, 用戶信息會保存在Session或者Token中。這個時候,我們?nèi)绻褂贸R?guī)的手段去獲取用戶信息會很費勁,拿Session來說,我們要在接口參數(shù)中加上HttpServletRequest對象,然后調(diào)用 getSession方法,且每一個需要用戶信息的接口都要加上這個參數(shù),才能獲取Session,這樣實現(xiàn)就很麻煩了。

在實際的系統(tǒng)設(shè)計中,我們肯定不會采用上面所說的這種方式,而是使用ThreadLocal,我們會選擇在攔截器的業(yè)務(wù)中, 獲取到保存的用戶信息,然后存入ThreadLocal,那么當前線程在任何地方如果需要拿到用戶信息都可以使用ThreadLocal的get()方法 (異步程序中ThreadLocal是不可靠的, 后續(xù)會出文章詳解)。

當用戶登錄后,會將用戶信息存入Token中返回前端,當用戶調(diào)用需要授權(quán)的接口時,需要在header中攜帶 Token,然后攔截器中解析Token,獲取用戶信息,調(diào)用自定義的類存入ThreadLocal中,當請求結(jié)束的時候,將ThreadLocal存儲數(shù)據(jù)清空(這一點很重要,否則會產(chǎn)生內(nèi)存泄漏), 中間的過程無需再關(guān)注如何獲取用戶信息,只需要使用工具類的get方法即可。

解決線程安全問題

ThreadLocal的設(shè)計天然就做到了線程隔離。所以就不會出現(xiàn)線程安全問題。

以上就是Java多線程之ThreadLocal淺析的詳細內(nèi)容,更多關(guān)于Java多線程ThreadLocal的資料請關(guān)注腳本之家其它相關(guān)文章!

相關(guān)文章

  • Springboot基于assembly的服務(wù)化打包方案及spring boot部署方式

    Springboot基于assembly的服務(wù)化打包方案及spring boot部署方式

    這篇文章主要介紹了Springboot基于assembly的服務(wù)化打包方案及springboot項目的幾種常見的部署方式,本文主要針對第二種部署方式提供一種更加友好的打包方案,需要的朋友可以參考下
    2017-12-12
  • 詳解Java 自動裝箱與拆箱的實現(xiàn)原理

    詳解Java 自動裝箱與拆箱的實現(xiàn)原理

    本篇文章主要介紹了詳解Java 自動裝箱與拆箱的實現(xiàn)原理,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧
    2017-04-04
  • SpringSecurity基于散列加密方案實現(xiàn)自動登錄

    SpringSecurity基于散列加密方案實現(xiàn)自動登錄

    本文主要介紹了SpringSecurity基于散列加密方案實現(xiàn)自動登錄,文中通過示例代碼介紹的非常詳細,具有一定的參考價值,感興趣的小伙伴們可以參考一下
    2021-09-09
  • Java窗體居中顯示的2種方法(實例講解)

    Java窗體居中顯示的2種方法(實例講解)

    下面小編就為大家?guī)硪黄狫ava窗體居中顯示的2種方法(實例講解)。小編覺得挺不錯的,現(xiàn)在就分享給大家,也給大家做個參考。一起跟隨小編過來看看吧
    2017-07-07
  • java.sql.SQLException:?connection?holder?is?null錯誤解決辦法

    java.sql.SQLException:?connection?holder?is?null錯誤解決辦法

    這篇文章主要給大家介紹了關(guān)于java.sql.SQLException:?connection?holder?is?null錯誤的解決辦法,這個錯誤通常是由于連接對象為空或未正確初始化導(dǎo)致的,文中通過代碼介紹的非常詳細,需要的朋友可以參考下
    2024-02-02
  • Maven 倉庫國內(nèi)鏡像源收藏(小結(jié))

    Maven 倉庫國內(nèi)鏡像源收藏(小結(jié))

    這篇文章主要介紹了Maven 倉庫國內(nèi)鏡像源收藏(小結(jié)),文中通過示例代碼介紹的非常詳細,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧
    2020-12-12
  • Spring Boot Actuator監(jiān)控的簡單使用方法示例代碼詳解

    Spring Boot Actuator監(jiān)控的簡單使用方法示例代碼詳解

    這篇文章主要介紹了Spring Boot Actuator監(jiān)控的簡單使用,本文通過實例代碼圖文相結(jié)合給大家介紹的非常詳細,對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下
    2020-06-06
  • maven 在執(zhí)行package,install,deploy時使用clean與不使用clean的不同之處

    maven 在執(zhí)行package,install,deploy時使用clean與不使用clean的不同之處

    有時候用mvn install后,新改的內(nèi)容不生效,一定要后來使用mvn clean install 才生效,由于之前沒有做記錄,以及記不清是什么情況下才會出現(xiàn)的問題,于是想看看clean和不clean的區(qū)別,感興趣的朋友跟隨小編一起看看吧
    2021-08-08
  • Java進階之Object類及常用方法詳解

    Java進階之Object類及常用方法詳解

    Object?類是?Java?默認提供的一個類,是所有?Java?類的祖先類,每個類都使用?Object?作為父類。本文就來和大家聊聊Object類的常用方法,希望對大家有所幫助
    2023-01-01
  • java中的Io(input與output)操作總結(jié)(一)

    java中的Io(input與output)操作總結(jié)(一)

    所謂IO,也就是Input與Output的縮寫。在java中,IO涉及的范圍比較大,這里主要討論針對文件內(nèi)容的讀寫,感興趣的朋友可以了解下
    2013-01-01

最新評論