亚洲激情专区-91九色丨porny丨老师-久久久久久久女国产乱让韩-国产精品午夜小视频观看

溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務條款》

Java多線程之ThreadLocal怎么使用

發布時間:2023-04-21 14:31:52 來源:億速云 閱讀:122 作者:iii 欄目:開發技術

這篇文章主要講解了“Java多線程之ThreadLocal怎么使用”,文中的講解內容簡單清晰,易于學習與理解,下面請大家跟著小編的思路慢慢深入,一起來研究和學習“Java多線程之ThreadLocal怎么使用”吧!

    介紹

    什么是ThreadLocal?

    ThreadLocal叫做線程變量,用于在多線程環境下創建線程本地變量。

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

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

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

    ThreadLocal 的作用是什么?

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

    如何使用ThreadLocal

    如何創建一個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

    創建ThreadLocal實例的方式非常簡單,只需要使用Java中的ThreadLocal類的構造函數即可。

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

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

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

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

    ThreadLocal與Synchronized的區別

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

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

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

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

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

    ThreadLocal的優點:

    • 線程安全:每個線程都擁有自己的變量副本,不會受到其他線程的影響,可以避免線程安全問題。

    • 性能高:ThreadLocal使用了空間換時間的方式,每個線程都有自己的變量副本,不需要進行加鎖和解鎖操作,因此性能更高。

    • 代碼簡潔:使用ThreadLocal可以避免復雜的同步控制邏輯。

    加鎖的優點:

    • 保證數據一致性:通過加鎖可以保證共享資源在多線程環境下的正確性,避免出現數據不一致的情況。

    • 線程同步:在加鎖過程中,線程會被阻塞,等待鎖的釋放,保證了線程同步。

    ThreadLocal的缺點:

    • 內存泄漏:ThreadLocal使用靜態的內部Map來存儲變量副本,如果不及時清理,會導致內存泄漏問題(后續展開介紹)。

    • 難以調試:由于每個線程都有自己的變量副本,因此在調試過程中,需要考慮多個線程的情況,會增加調試的難度。

    加鎖的缺點:

    • 性能問題:在高并發情況下,加鎖會導致線程的阻塞,從而影響系統的性能。

    • 容易導致死鎖:如果加鎖的操作不正確,可能會導致死鎖問題,需要謹慎使用。

    綜合來看,ThreadLocal適合處理線程私有的數據,而加鎖適合處理共享的資源,具體應該根據業務需求來選擇。

    ThreadLocal的實現原理

    ThreadLocal的內部數據結構

    Java多線程之ThreadLocal怎么使用

    直接查看源碼:

    Thread類:
    public  
    class Thread implements Runnable {
        //MAP
        ThreadLocal.ThreadLocalMap threadLocals = null;  
    
        //用于父子線程變量同步, 后續介紹
        ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
    }
    ThreadLocal的set()方法:
        public void set(T value) {  
            //獲取當前線程
            Thread t = Thread.currentThread();
            //封裝方法從獲取線程中的ThreadLocalMap
            //為什么封裝方法呢? 為了后面擴展inheritableThreadLocals
            ThreadLocalMap map = getMap(t);  
            //之前有創建過, 直接set
            if (map != null)  
                map.set(this, value);  
            else  
                //之前沒有創建, 新建Map并設置值
                createMap(t, value);  
        }
    
        ThreadLocalMap getMap(Thread t) {  
            return t.threadLocals;  
        }

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

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

    ThreadLocalMap和ThreadLocalMap.Entry的實現
    public class ThreadLocal<T> {
    
        static class ThreadLocalMap {  
            
            //繼承弱應用, 方便垃圾回收
            static class Entry extends WeakReference<ThreadLocal<?>> {  
    
                /** The value associated with this ThreadLocal. */  
                Object value;  
    
                Entry(ThreadLocal<?> k, Object v) {  
                    super(k);  
                    value = v;  
                }  
            }
            //數組, 用于存儲多組數據
            private Entry[] table;
        }
    }

    從代碼我們可以看到, threadLocalMap是ThreadLocal中的一個靜態內部類, 在threadLocalMap又維護了一個名叫table的Entry數組。

    Entry是什么呢?

    Entry是一組組數據對, 而且繼承的弱引用。在Entry內部使用ThreadLocal作為key,使用我們設置的value作為value。

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

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

    ThreadLocalMap.set()方法
    private void set(ThreadLocal<?> key, Object value) {
        Entry[] tab = table;
        int len = tab.length;
        //根據hashCode與長度計算索引位置
        int i = key.threadLocalHashCode & (len-1);
    
        for (Entry e = tab[i];
             e != null;
             // 如果下標沖突, 索引+1繼續查找
             e = tab[i = nextIndex(i, len)]) {
            ThreadLocal<?> k = e.get();
            
            //找到直接返回值
            if (k == key) {
                e.value = value;
                return;
            }
    
            if (k == null) {
                // key 為空, 說明 對應的 ThreadLocal 已經回收了.
                // 可以復用當前位置.
                // 有兩種情況: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方法與構造方法能看到以下代碼片段。

    • 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是斐波那契散列乘數,它的優點是通過它散列(hash)出來的結果分布會比較均勻,可以很大程度上避免hash沖突。

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

    總結一下:

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

    • 對于同一線程的不同ThreadLocal來講,這些ThreadLocal實例共享一個table數組,然后每個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;  
            }  
        }
        //設置初始值--null
        return setInitialValue();  
    }
    
    private Entry getEntry(ThreadLocal<?> key) {
        //計算下標, 通過下標從Entry數組中直接取值
        int i = key.threadLocalHashCode & (table.length - 1);  
        Entry e = table[i];  
        if (e != null && e.get() == key)  
            return e;  
        else
            //索引沖突導致沒有查找到, 繼續查找
            return getEntryAfterMiss(key, i, e);  
    }

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

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

    ThreadLocal的垃圾回收機制

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

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

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

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

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

    ThreadLocal的使用場景

    參數透傳

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

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

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

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

    在現在的系統設計中,前后端分離已基本成為常態,分離之后如何獲取用戶信息就成了一件麻煩事,通常在用戶登錄后, 用戶信息會保存在Session或者Token中。這個時候,我們如果使用常規的手段去獲取用戶信息會很費勁,拿Session來說,我們要在接口參數中加上HttpServletRequest對象,然后調用 getSession方法,且每一個需要用戶信息的接口都要加上這個參數,才能獲取Session,這樣實現就很麻煩了。

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

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

    解決線程安全問題

    ThreadLocal的設計天然就做到了線程隔離。所以就不會出現線程安全問題。

    感謝各位的閱讀,以上就是“Java多線程之ThreadLocal怎么使用”的內容了,經過本文的學習后,相信大家對Java多線程之ThreadLocal怎么使用這一問題有了更深刻的體會,具體使用情況還需要大家實踐驗證。這里是億速云,小編將為大家推送更多相關知識點的文章,歡迎關注!

    向AI問一下細節

    免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。

    AI

    家居| 奇台县| 龙胜| 鄂托克旗| 舒兰市| 尚义县| 壤塘县| 津市市| 石狮市| 青河县| 尉氏县| 息烽县| 汪清县| 奉节县| 晋江市| 竹山县| 龙陵县| 准格尔旗| 仙桃市| 镇原县| 南昌市| 吉首市| 石渠县| 达日县| 高雄市| 芷江| 鄯善县| 麟游县| 岐山县| 定结县| 班玛县| 怀化市| 徐汇区| 高尔夫| 泸州市| 长垣县| 克什克腾旗| 和林格尔县| 车险| 公安县| 房产|