聊聊ThreadLocal那些事

前言

這篇文章聊聊 ThreadLocal,我們經常會在一些開源中間件的源碼中見到它的身影,比較常見的用途是保存上下文信息,還有就是保證了線程安全。

實際上,ThreadLocal 爲每個線程提供一個單獨的變量,確是一種保證線程安全的手段,ThreadLocal 創建的變量只能被當前線程訪問,其他線程不得干涉。

ThreadLocal API

使用 ThreadLocal 其實非常簡單,直接看下面的示例:


public class ThreadLocalSimpleDateFormat {
    private static final ThreadLocal<SimpleDateFormat> formatter = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyyMMss HHmm"));

    public String formatDate(Date date) {
        return formatter.get().format(date);
    }
}    
  • 使用 withIntial() 靜態方法 初始化變量
  • 調用 get() 方法獲取當前線程的對象值

我們都知道 SimpleDateFormat 不是線程安全,所以上面使用 ThreadLocal 的方式來保證線程安全,保證每個線程都有特定的 SimpleDateFormat。

當然不只這兩個方法,還有常見的 set()、initialValue()方法,使用起來都比較簡單。

什麼時候使用TheadLocal

有時候我們會遇到將某個變量和線程進行綁定起來的場景,一種方法是定義一個Map,其中key是線程ID,value是對象,這樣每個線程都有屬於自己的對象,同一個線程只能對應一個對象。

比如,定義一個計數器,每個線程需要有一個屬於自己的計數器,保證線程安全。


public class Counter {
    private AtomicLong counter = new AtomicLong();

    private static Map<Long, Counter> counterMap = new ConcurrentHashMap<>();

    public static Counter getCounter() {
        long id = Thread.currentThread().getId();
        counterMap.putIfAbsent(id, new Counter());
        return counterMap.get(id);
    }

    public long incrementAndGet() {
        return counter.incrementAndGet();
    }
}

而 Java 提供了更簡單的方法,也就是 ThreadLocal 工具類,使用 ThreadLocal 類可以直接給每個線程提供單獨的計數器


public class ThreadLocal_Counter {
    private ThreadLocal<AtomicLong> threadLocal = new ThreadLocal<>();
    
    public long incrementAndGet() {
        return threadLocal.get().incrementAndGet();
    }
}

顯然使用 ThreadLocal 來得更簡潔方便,之前提到使用 ThreadLocal 構建線程獨享的 SimpleDateFormat 也是同樣的道理,主要是保證線程安全。

ThreadLocal如何實現

探究ThreadLocal是如何實現的,從它的set()方法可以看出端倪,如下:

public void set(T value) {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
}
  • 獲取當前線程
  • 根據當前線程作爲參數獲取一個 ThreadLocalMap 的對象
  • 如果 ThreadLocalMap 對象不爲空,則將 ThreadLocal 本身作爲 key,value 爲值,否則直接創建這個 ThreadLocalMap 對象並設置值

重點在於這個 ThreadLocalMap 對象,走進 getMap 方法 :

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

實際上是獲取的 Thread 對象的 threadLocals 變量,也就是:

class Thread implements Runnable {

    ThreadLocal.ThreadLocalMap threadLocals = null;
}

每個線程都有一個 ThreadLocalMap 變量,將 value 值放入其中,自然是只能在本線程中訪問。

那這 ThreadLocalMap 究竟是怎樣一種構造,它的作用與 Map 有些類似,請繼續往下看:

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;
            }
        }
        // 其他代碼省略
}

ThreadLocalMap 是 Thread 的靜態內部類, 而它的內部還有一個靜態內部類 (禁止套娃) ,Entry類就是 ThreadLocalMap 用來進行 key-value 存儲的, key是 ThreadLocal,value 是值。

Entry 繼承自 WeakReference,WeakReference 表示弱引用,那麼 Entry 的 key (ThreadLocal) 就是 一個弱引用。弱引用的意思是:如果在發生垃圾回收時,若這個對象只被弱引用指向,那麼就會被回收。key 被回收了,而value 值卻沒有被回收,導致value一直存在,從而引發內存泄漏,這就是 ThreadLocal內存泄漏的問題,所以我們需要手動調用 remove() 方法去清除value值。

值得注意的是,如果 key有被強引用指向,那麼在垃圾回收的時候是不會被回收的。

ThreadLocal的注意事項

static修飾ThreadLocal

在《阿里巴巴Java開發手冊》中給出了使用 ThreadLocal 的建議:

【參考】ThreadLocal無法解決共享對象的更新問題,ThreadLocal對象建議使用static修飾。這個變量是針對一個線程內所有操作共享的,所以設置爲靜態變量,所有此類實例共享此靜態變量 ,也就是說在類第一次被使用時裝載,只分配一塊存儲空間,所有此類的對象(只要是這個線程內定義的)都可以操控這個變量。

前面分析過 ThreadLocal 是 ThreadLocalMap 中 Entry 的 key,而用 static 修飾 ThreadLocal,保證了 ThreadLocal 有強引用在,也就是 Entry 的 key有被強引用指向,會一直存在,垃圾回收的時候不會被回收,這樣就不容易導致內存泄漏的問題

ThreadLocal結合線程池的問題

當 ThreadLocal 配合線程池使用的時候,我們需要及時對 ThreadLocal 進行清理,清除與本線程綁定的 value 值,否則會出現意料之外的結果。

下面來看一個代碼示例,來看看沒有調用remove方法和有調用remove下的結果差異。

private static ThreadLocal<Integer> threadLocal = ThreadLocal.withInitial(() -> 0);

public static void main(String[] args) {
    ExecutorService executorService = Executors.newFixedThreadPool(2);
    for (int i = 0; i < 5; i++) {
        executorService.execute(()->{
            Integer before = threadLocal.get();
            threadLocal.set(before + 1);
            Integer after = threadLocal.get();
            System.out.println("before: " + before + ",after: " + after);
        });
    }
    executorService.shutdown();
}

首先我們沒有調用 remove 方法進行清理,它的打印結果是:

before: 0,after: 1
before: 0,after: 1
before: 1,after: 2
before: 2,after: 3
before: 3,after: 4

可以看到出現了 before 不爲0的情況,這是因爲線程在執行完任務被複用了,被複用的線程使用了上一個線程操作的value對象,從而導致不符合預期。

然後我們加上調用remove方法的邏輯:

try {
    Integer before = threadLocal.get();
    threadLocal.set(before + 1);
    Integer after = threadLocal.get();
    System.out.println("before: " + before + ",after: " + after);
} finally {
    threadLocal.remove();
}

這次輸出的結果迴歸正常了:

before: 0,after: 1
before: 0,after: 1
before: 0,after: 1
before: 0,after: 1
before: 0,after: 1

總結來說,使用 ThreadLocal 的時候要及時調用 remove() 方法進行清理。

ThreadLocal的應用

文章最開頭就提到,ThreadLocal 被頻繁運用到開源中間件中,比如RocketMQ、Dubbo、Zuul等等,下面就來學習下開源中間件是如何使用 ThreadLocal的。

Zuul

最近有研究 API 網關的實現原理,Zuul 1.x 算的上一款比較優秀的網關,在它的源碼實現中的RequestContext類就用到了 ThreadLocal,保存線程上下文信息。

public class RequestContext extends ConcurrentHashMap<String, Object> {

    protected static Class<? extends RequestContext> contextClass = RequestContext.class;
    
    private static RequestContext testContext = null;
    // 使用 ThreadLocal 保存線程上下文信息
    protected static final ThreadLocal<? extends RequestContext> threadLocal = new ThreadLocal<RequestContext>() {
        @Override
        protected RequestContext initialValue() {
            try {
                return contextClass.newInstance();
            } catch (Throwable e) {
                throw new RuntimeException(e);
            }
        }
    };
    
    public static RequestContext getCurrentContext() {
        if (testContext != null) return testContext;

        RequestContext context = threadLocal.get();
        return context;
    }
    ...

通過 ThreadLocal 保存上下文信息,在任意地方調用getCurrentContext()方法就可以獲取當前線程的RequestContext,然後再從 RequestContext 獲取 Request或者Response進行相應處理。

RocketMQ

在 RocketMQ的源碼實現中也有用到 ThreadLocal,代碼如下:

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;
    }
}

ThreadLocalIndex 主要用於生產者發送消息的時候,熟悉 RocketMQ 的小夥伴都知道,生產者首先拉取 Topic 的路由信息,一個 Topic 有多個 MessageQueue (消息隊列),發送消息時需要選擇一個消息隊列進行發送,一般採用輪詢的方式選擇,此時不同的生產者線程需要有自己負責的輪詢順序,所以使用 ThreadLocalIndex來保證。

小結

本文介紹了 ThreadLocal 的一些常見知識點,再次總結一點:爲了保證安全和結果的準確性,我們需要在使用 ThreadLocal 後及時調用 remove()方法進行清理工作。

同時,歡迎關注我新開的公衆號,定期分享Java後端知識!

pjmike

參考資料

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章