Java面試題必備知識之ThreadLocal

来源:https://www.cnblogs.com/javaadu/archive/2019/07/21/11222891.html
-Advertisement-
Play Games

老套路,先列舉下關於ThreadLocal常見的疑問,希望可以通過這篇學習筆記來解決這幾個問題: 1. ThreadLocal是用來解決什麼問題的? 2. 如何使用ThreadLocal? 3. ThreadLocal的實現原理是什麼? 4. 可否舉幾個實際項目中使用ThreadLocal的案例? ...


image.png

老套路,先列舉下關於ThreadLocal常見的疑問,希望可以通過這篇學習筆記來解決這幾個問題:

  1. ThreadLocal是用來解決什麼問題的?
  2. 如何使用ThreadLocal?
  3. ThreadLocal的實現原理是什麼?
  4. 可否舉幾個實際項目中使用ThreadLocal的案例?

基礎知識

ThreadLocal是線程局部變數,和普通變數的不同在於:每個線程持有這個變數的一個副本,可以獨立修改(set方法)和訪問(get方法)這個變數,並且線程之間不會發生衝突。

類中定義的ThreadLocal實例一般會被private static修飾,這樣可以讓ThreadLocal實例的狀態和Thread綁定在一起,業務上,一般用ThreadLocal包裝一些業務ID(user ID或事務ID)——不同的線程使用的ID是不相同的。

如何使用

case1

從某個角度來看,ThreadLocal為Java併發編程提供了額外的思路——避免併發,如果某個對象本身是非線程安全的,但是你想實現多線程同步訪問的效果,例如SimpleDateFormat,你可以使用ThreadLocal變數。

public class Foo
{
    // SimpleDateFormat is not thread-safe, so give one to each thread
    private static final ThreadLocal<SimpleDateFormat> formatter = new ThreadLocal<SimpleDateFormat>(){
        @Override
        protected SimpleDateFormat initialValue()
        {
            return new SimpleDateFormat("yyyyMMdd HHmm");
        }
    };

    public String formatIt(Date date)
    {
        return formatter.get().format(date);
    }
}

註意,這裡針對每個線程只需要初始化一次SimpleDateFormat對象,其實跟在自定義線程中定義一個SimpleDateFormat成員變數,併在線程初始化的時候new這個對象,效果是一樣的,只是這樣看起來代碼更規整。

case2

之前在yunos做酷盤項目的數據遷移時,我們需要按照用戶維度去加鎖,每個線程在處理遷移之前,都需要先獲取當前用戶的鎖,每個鎖的key是帶著用戶信息的,因此也可以使用ThreadLocal變數實現:image.png

case3

下麵這個例子,我們定義了一個MyRunnable對象,這個MyRunnable對象會被線程1和線程2使用,但是通過內部的ThreadLocal變數,每個線程訪問到的整數都是自己單獨的一份。

package org.java.learn.concurrent.threadlocal;

/**
 * @author duqi
 * @createTime 2018-12-29 23:25
 **/
public class ThreadLocalExample {
    public static class MyRunnable implements Runnable {

        private ThreadLocal<Integer> threadLocal =
                new ThreadLocal<Integer>();

        @Override
        public void run() {
            threadLocal.set((int) (Math.random() * 100D));

            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
            }

            System.out.println(threadLocal.get());
        }
    }


    public static void main(String[] args) throws InterruptedException {
        MyRunnable sharedRunnableInstance = new MyRunnable();

        Thread thread1 = new Thread(sharedRunnableInstance);
        Thread thread2 = new Thread(sharedRunnableInstance);

        thread1.start();
        thread2.start();

        thread1.join(); //wait for thread 1 to terminate
        thread2.join(); //wait for thread 2 to terminate
    }
}

ThreadLocal關鍵知識點

源碼分析

ThreadLocal是如何被線程使用的?原理如下圖所示:Thread引用和ThreadLocal引用都在棧上,Thread引用會引用一個ThreadLocalMap對象,這個map中的key是ThreadLocal對象(使用WeakReference包裝),value是業務上變數的值。image.png

首先看java.lang.Thread中的代碼:

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

    /*
     * InheritableThreadLocal values pertaining to this thread. This map is maintained by the InheritableThreadLocal class.
     */
    ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
    //......其他源碼

Thread中的threadLocals變數指向的是一個map,這個map就是ThreadLocal.ThreadLocalMap,裡面存放的是跟當前線程綁定的ThreadLocal變數;inheritableThreadLocals的作用相同,裡面也是存放的ThreadLocal變數,但是存放的是從當前線程的父線程繼承過來的ThreadLocal變數。

在看java.lang.ThreadLocal類,主要的成員和介面如下:image.png

  1. withInitial方法,Java 8以後用於初始化ThreadLocal的一種方法,在外部調用get()方法的時候,會通過Supplier確定變數的初始值;

    public static <S> ThreadLocal<S> withInitial(Supplier<? extends S> supplier) {
        return new SuppliedThreadLocal<>(supplier);
    }
  2. get方法,獲取當前線程的變數副本,如果當前線程還沒有創建該變數的副本,則需要通過調用initialValue方法來設置初始值;get方法的源代碼如下,首先通過當前線程獲取當前線程對應的map,如果map不為空,則從map中取出對應的Entry,然後取出對應的值;如果map為空,則調用setInitialValue設置初始值;如果map不為空,當前ThreadLocal實例對應的Entry為空,則也需要設置初始值。

    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();
    }
  3. set方法,跟get方法一樣,先獲取當前線程對應的map,如果map為空,則調用createMap創建map,否則將變數的值放入map——key為當前這個ThreadLocal對象,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);
    }
  4. remove方法,刪除當前線程綁定的這個副本

         public void remove() {
             ThreadLocalMap m = getMap(Thread.currentThread());
             if (m != null)
                 m.remove(this);
         }
  5. 數字0x61c88647,這個值是HASH_INCREMENT的值,普通的hashmap是使用鏈表來處理衝突的,但是ThreadLocalMap是使用線性探測法來處理衝突的,HASH_INCREMENT就是每次增加的步長,根據參考資料1所說,選擇這個數字是為了讓衝突概率最小。

    /**
     * The difference between successively generated hash codes - turns
     * implicit sequential thread-local IDs into near-optimally spread
     * multiplicative hash values for power-of-two-sized tables.
     */
    private static final int HASH_INCREMENT = 0x61c88647;

父子進程數據共用

InheritableThreadLocal主要用於子線程創建時,需要自動繼承父線程的ThreadLocal變數,實現子線程訪問父線程的threadlocal變數。InheritableThreadLocal繼承了ThreadLocal,並重寫了childValue、getMap、createMap三個方法。

public class InheritableThreadLocal<T> extends ThreadLocal<T> {
    /**
     * 創建線程的時候,如果需要繼承且父線程中Thread-Local變數,則需要將父線程中的ThreadLocal變數一次拷貝過來。
     */
    protected T childValue(T parentValue) {
        return parentValue;
    }

    /**
    * 由於重寫了getMap,所以在操作InheritableThreadLocal變數的時候,將只操作Thread類中的inheritableThreadLocals變數,與threadLocals變數沒有關係
    **/
    ThreadLocalMap getMap(Thread t) {
       return t.inheritableThreadLocals;
    }

    /**
     * 跟getMap類似,set或getInheritableThreadLocal變數的時候,將只操作Thread類中的inheritableThreadLocals變數
     */
    void createMap(Thread t, T firstValue) {
        t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
    }
}

關於childValue多說兩句,拷貝是如何發生的?
首先看Thread.init方法,

    private void init(ThreadGroup g, Runnable target, String name, long stackSize, AccessControlContext acc, boolean inheritThreadLocals) {
        //其他源碼
        if (inheritThreadLocals && parent.inheritableThreadLocals != null)
            this.inheritableThreadLocals =
                ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
        /* Stash the specified stack size in case the VM cares */
        this.stackSize = stackSize;

        /* Set thread ID */
        tid = nextThreadID();
    }

然後看ThreadLocal.createInheritedMap方法,最終會調用到newThreadLocalMap方法,這裡InheritableThreadLocal對childValue做了重寫,可以看出,這裡確實是將父線程關聯的ThreadLocalMap中的內容依次拷貝到子線程的ThreadLocalMap中了。

       private ThreadLocalMap(ThreadLocalMap parentMap) {
            Entry[] parentTable = parentMap.table;
            int len = parentTable.length;
            setThreshold(len);
            table = new Entry[len];

            for (int j = 0; j < len; j++) {
                Entry e = parentTable[j];
                if (e != null) {
                    @SuppressWarnings("unchecked")
                    ThreadLocal<Object> key = (ThreadLocal<Object>) e.get();
                    if (key != null) {
                        Object value = key.childValue(e.value);
                        Entry c = new Entry(key, value);
                        int h = key.threadLocalHashCode & (len - 1);
                        while (table[h] != null)
                            h = nextIndex(h, len);
                        table[h] = c;
                        size++;
                    }
                }
            }
        }

ThreadLocal對象何時被回收?

ThreadLocalMap中的key是ThreadLocal對象,然後ThreadLocal對象時被WeakReference包裝的,這樣當沒有強引用指向該ThreadLocal對象之後,或者說Map中的ThreadLocal對象被判定為弱引用可達時,就會在垃圾收集中被回收掉。看下Entry的定義:

 static class Entry extends WeakReference<ThreadLocal<?>> {
     /** The value associated with this ThreadLocal. */
     Object value;

     Entry(ThreadLocal<?> k, Object v) {
         super(k);
         value = v;
     }
}

ThreadLocal和線程池一起使用?

ThreadLocal對象的生命周期跟線程的生命周期一樣長,那麼如果將ThreadLocal對象和線程池一起使用,就可能會遇到這種情況:一個線程的ThreadLocal對象會和其他線程的ThreadLocal對象串掉,一般不建議將兩者一起使用。

案例學習

Dubbo中對ThreadLocal的使用

我從Dubbo中找到了ThreadLocal的例子,它主要是用在請求緩存的場景,具體代碼如下:

@Activate(group = {Constants.CONSUMER, Constants.PROVIDER}, value = Constants.CACHE_KEY)
public class CacheFilter implements Filter {

    private CacheFactory cacheFactory;

    public void setCacheFactory(CacheFactory cacheFactory) {
        this.cacheFactory = cacheFactory;
    }

    @Override
    public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {
        if (cacheFactory != null && ConfigUtils.isNotEmpty(invoker.getUrl().getMethodParameter(invocation.getMethodName(), Constants.CACHE_KEY))) {
            Cache cache = cacheFactory.getCache(invoker.getUrl(), invocation);
            if (cache != null) {
                String key = StringUtils.toArgumentString(invocation.getArguments());
                Object value = cache.get(key);
                if (value != null) {
                    if (value instanceof ValueWrapper) {
                        return new RpcResult(((ValueWrapper)value).get());
                    } else {
                        return new RpcResult(value);
                    }
                }
                Result result = invoker.invoke(invocation);
                if (!result.hasException()) {
                    cache.put(key, new ValueWrapper(result.getValue()));
                }
                return result;
            }
        }
        return invoker.invoke(invocation);
    }

可以看出,在RPC調用(invoke)的鏈路上,會先使用請求參數判斷當前線程是否剛剛發起過同樣參數的調用——這個調用會使用ThreadLocalCache保存起來。具體的看,ThreadLocalCache的實現如下:

package org.apache.dubbo.cache.support.threadlocal;

import org.apache.dubbo.cache.Cache;
import org.apache.dubbo.common.URL;

import java.util.HashMap;
import java.util.Map;

/**
 * ThreadLocalCache
 */
public class ThreadLocalCache implements Cache {

    //ThreadLocal里存放的是參數到結果的映射
    private final ThreadLocal<Map<Object, Object>> store;

    public ThreadLocalCache(URL url) {
        this.store = new ThreadLocal<Map<Object, Object>>() {
            @Override
            protected Map<Object, Object> initialValue() {
                return new HashMap<Object, Object>();
            }
        };
    }

    @Override
    public void put(Object key, Object value) {
        store.get().put(key, value);
    }

    @Override
    public Object get(Object key) {
        return store.get().get(key);
    }

}

RocketMQ

在RocketMQ中,我也找到了ThreadLocal的身影,它是用在消息發送的場景,MQClientAPIImpl是RMQ中負責將消息發送到服務端的實現,其中有一個步驟需要選擇一個具體的隊列,選擇具體的隊列的時候,不同的線程有自己負責的index值,這裡使用了ThreadLocal的機制,可以看下ThreadLocalIndex的實現:

package org.apache.rocketmq.client.common;

import java.util.Random;

public class ThreadLocalIndex {
    private final ThreadLocal<Integer> threadLocalIndex = new ThreadLocal<Integer>();
    private final Random random = new Random();

    public int getAndIncrement() {
        Integer index = this.threadLocalIndex.get();
        if (null == index) {
            index = Math.abs(random.nextInt());
            if (index < 0)
                index = 0;
            this.threadLocalIndex.set(index);
        }

        index = Math.abs(index + 1);
        if (index < 0)
            index = 0;

        this.threadLocalIndex.set(index);
        return index;
    }

    @Override
    public String toString() {
        return "ThreadLocalIndex{" +
            "threadLocalIndex=" + threadLocalIndex.get() +
            '}';
    }
}

總結

這篇文章主要是解決了關於ThreadLocal的幾個問題:(1)具體的概念是啥?(2)在Java開發中的什麼場景下使用?(3)ThreadLocal的實現原理是怎樣的?(4)開源項目中有哪些案例可以參考?不知道你是否對這幾個問題有了一定的瞭解呢?如果還有疑問,歡迎交流。

參考資料

  1. Why 0x61c88647?
  2. Java ThreadLocal
  3. When and how should I use a ThreadLocal variable?
  4. 技術小黑屋:理解Java中的ThreadLocal
  5. 深入分析ThreadLocal的記憶體泄漏問題
  6. 《Java併發編程實戰》
  7. InheritableThreadLocal詳解
  8. ThreadLocal詳解
  9. ThreadLocal的使用場景
  10. 數據結構:哈希表

本號專註於後端技術、JVM問題排查和優化、Java面試題、個人成長和自我管理等主題,為讀者提供一線開發者的工作和成長經驗,期待你能在這裡有所收穫。
javaadu


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 第七章 父 子關係文檔 打虎親兄弟,上陣父子兵。 本章作為 複雜搜索 的鋪墊,介紹父子文檔是為了更好的介紹複雜場景下的ES操作。 在非關係型資料庫資料庫中,我們常常會有表與表的關聯查詢。例如學生表和成績表的關聯查詢就能查出學會的信息和成績信息。在ES中,父子關係文檔就類似於表的關聯查詢。 背景 ES ...
  • 列表列表用中括弧[ ]把各種數據框起來,每一個數據叫作“元素”。每個元素之間都要用英文逗號隔開各種類型的數據(整數/浮點數/字元串)————————————————————————————從列表提取單個元素每個元素都有自己的位置編號(即偏移量) 1.偏移量是從0開始的2.列表名後加帶偏移量的中括弧, ...
  • 一、抽象工廠模式 1、生活場景 汽車生產根據用戶選擇的汽車類型,指定不同的工廠進行生產,選擇紅旗轎車,就要使用中國工廠,選擇奧迪轎車,就要使用德國工廠。 2、抽象工廠模式 1) 抽象工廠模式:定義了一個interface用於創建相關對象或相互依賴的對象,而無需指明具體的類; 2) 抽象工廠模式可以將 ...
  • 一.首先程式啟動,顯示下麵內容供用戶選擇: 1.請登錄 2.請註冊 3.進入文章頁面 4.進入評論頁面 5.進入日記頁面 6.進入收藏頁面 7.註銷賬號 8.退出整個程式 二.必須實現的功能: 1.註冊功能要求: a.用戶名、密碼要記錄在文件中。 b.用戶名要求:只能含有字母或者數字不能含有特殊字元 ...
  • 我喜歡直接了當, 這次主要是推薦蟒營大媽的 Python 入門課(https://py.101.camp), 還有不到一周就要開課了, 歡迎轉發推薦~ 點擊"夏日大作戰:從小白到小能手的 Python 學習之旅", 直接訪問課程的詳細信息, 以及課程的由來故事DM2: call back 下文為曾經 ...
  • 運用input函數搜集信息 input()函數結果的賦值name = input('請輸入你的名字:') #將input()函數的執行結果(收集的信息)賦值給變數name input()函數的使用場景1.函數結果賦值 name=input()2.搜集信息 name=input(xxx)3.輸出結果 ...
  • Goroutine 和 Channel 是 Go 語言併發編程的兩大基石,本文深度解密 channel 的底層原理。 ...
  • Java 8曾經與Docker無法很好地相容性,現在問題已消失。 請註意:我在本文中使用採用GNU GPL v2許可證的OpenJDK官方docker映像。在Oracle Java SE中,這裡描述的docker支持功能在更新191中引入。Oracle在2019年4月更改了Java 8更新的許可證, ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...