Java Thread Local 線程本地變量

更多 Java 并發編程方面的文章,請參見文集《Java 并發編程》


Thread Local 線程本地變量

在每個線程中都創建一個變量的副本,線程內共享,線程間互斥。

ThreadLocal 是一個為線程提供線程局部變量的工具類。為線程提供一個線程私有的變量副本,這樣多個線程都可以隨意更改自己線程局部的變量,不會影響到其他線程。
不過需要注意的是,ThreadLocal 提供的只是一個淺拷貝,如果變量是一個引用類型,那么就要考慮它內部的狀態是否會被改變,想要解決這個問題可以通過重寫 ThreadLocal 的 initialValue() 函數來自己實現深拷貝,建議在使用 ThreadLocal 時一開始就重寫該函數。

ThreadLocal 與像 synchronized 這樣的鎖機制是不同的:

  • 鎖更強調的是如何同步多個線程去正確地共享一個變量,ThreadLocal 則是為了解決同一個變量如何不被多個線程共享
  • 從性能開銷的角度上來講,如果鎖機制是用時間換空間的話,那么 ThreadLocal 就是用空間換時間

ThreadLocal 中含有一個叫做 ThreadLocalMap 的內部類,該類為一個采用線性探測法實現的 HashMap。
它的 key 為 ThreadLocal 對象而且還使用了 WeakReference,ThreadLocalMap 正是用來存儲變量副本的。

static class ThreadLocalMap {

    /**
     * The entries in this hash map extend WeakReference, using
     * its main ref field as the key (which is always a
     * ThreadLocal object).  Note that null keys (i.e. entry.get()
     * == null) mean that the key is no longer referenced, so the
     * entry can be expunged from table.  Such entries are referred to
     * as "stale entries" in the code that follows.
     */
    static class Entry extends WeakReference<ThreadLocal<?>> {
        /** The value associated with this ThreadLocal. */
        Object value;

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

    /**
     * The initial capacity -- MUST be a power of two.
     */
    private static final int INITIAL_CAPACITY = 16;

    /**
     * The table, resized as necessary.
     * table.length MUST always be a power of two.
     */
    private Entry[] table;

ThreadLocal 中只含有三個成員變量,這三個變量都是與 ThreadLocalMap 的 hash 策略相關的。

public class ThreadLocal<T> {
    /**
     * ThreadLocals rely on per-thread linear-probe hash maps attached
     * to each thread (Thread.threadLocals and
     * inheritableThreadLocals).  The ThreadLocal objects act as keys,
     * searched via threadLocalHashCode.  This is a custom hash code
     * (useful only within ThreadLocalMaps) that eliminates collisions
     * in the common case where consecutively constructed ThreadLocals
     * are used by the same threads, while remaining well-behaved in
     * less common cases.
     */
    private final int threadLocalHashCode = nextHashCode();

    /**
     * The next hash code to be given out. Updated atomically. Starts at
     * zero.
     */
    private static AtomicInteger nextHashCode =
        new AtomicInteger();

    /**
     * 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;

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

唯一的實例變量 threadLocalHashCode 是用來進行尋址的 hashcode,它由函數 nextHashCode() 生成,該函數簡單地通過一個增量 HASH_INCREMENT 來生成 hashcode。至于為什么這個增量為 0x61c88647,主要是因為 ThreadLocalMap 的初始大小為16,每次擴容都會為原來的2倍,這樣它的容量永遠為2的n次方,該增量選為 0x61c88647 也是為了盡可能均勻地分布,減少碰撞沖突。

獲取當前線程中該變量的值 - get()

要獲得當前線程私有的變量副本需要調用 get() 函數。首先,它會調用 getMap() 函數去獲得當前線程的ThreadLocalMap,這個函數需要接收當前線程的實例作為參數。如果得到的 ThreadLocalMap 為 null,那么就去調用 setInitialValue() 函數來進行初始化,如果不為 null,就通過 map 來獲得變量副本并返回。

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

/**
 * Variant of set() to establish initialValue. Used instead
 * of set() in case user has overridden the set() method.
 *
 * @return the initial value
 */
private T setInitialValue() {
    T value = initialValue();
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
    return value;
}

設置當前線程中該變量的值 - 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);
}

移除當前線程中該變量的值 - remove()

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

getMap() 函數與 createMap() 函數的實現也十分簡單,但是通過觀察這兩個函數可以發現一個秘密:ThreadLocalMap 是存放在 Thread 中的

/**
 * Get the map associated with a ThreadLocal. Overridden in
 * InheritableThreadLocal.
 *
 * @param  t the current thread
 * @return the map
 */
ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}

/**
 * Create the map associated with a ThreadLocal. Overridden in
 * InheritableThreadLocal.
 *
 * @param t the current thread
 * @param firstValue value for the initial entry of the map
 */
void createMap(Thread t, T firstValue) {
    t.threadLocals = new ThreadLocalMap(this, firstValue);
}

Thread 類中包括:

/* ThreadLocal values pertaining to this thread. This map is maintained
 * by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;

仔細想想其實就能夠理解這種設計的思想。有一種普遍的方法是通過一個全局的線程安全的 Map 來存儲各個線程的變量副本,但是這種做法已經完全違背了 ThreadLocal 的本意,設計 ThreadLocal 的初衷就是為了避免多個線程去并發訪問同一個對象,盡管它是線程安全的。
而在每個 Thread 中存放與它關聯的 ThreadLocalMap 是完全符合 ThreadLocal 的思想的,當想要對線程局部變量進行操作時,只需要把 Thread 作為 key 來獲得 Thread 中的 ThreadLocalMap 即可。這種設計相比采用一個全局 Map 的方法會多占用很多內存空間,但也因此不需要額外的采取鎖等線程同步方法而節省了時間上的消耗。

使用示例

提供的方法:

  • T initialValue():設置初始值
  • public T get():獲取當前線程中該變量的值
  • public void set(T value):設置當前線程中該變量的值
  • public void remove():移除當前線程中該變量的值

示例:
我們希望每一個線程維護一個 Unique ID。

public class ThreadLocal_Test {
    private static final AtomicInteger nextId = new AtomicInteger(0);

    public static ThreadLocal<Integer> threadLocal = new ThreadLocal<Integer>() {
        @Override
        protected Integer initialValue() {
            return nextId.getAndIncrement();
        }
    };

    public static void main(String[] args) {
        for (int i = 0; i < 5; i++) {
            new MyThread().start();
        }
    }

    static class MyThread extends Thread {
        public void run() {
            System.out.println(Thread.currentThread().getName() + " Unique ID: " + threadLocal.get());
        }
    }
}

輸出如下:

Thread-0 Unique ID: 0
Thread-1 Unique ID: 1
Thread-2 Unique ID: 2
Thread-3 Unique ID: 3
Thread-4 Unique ID: 4

ThreadLocal 中的內存泄漏

如果 ThreadLocal 被設置為 null 后,而且沒有任何強引用指向它,根據垃圾回收的可達性分析算法,ThreadLocal 將會被回收。這樣一來,ThreadLocalMap 中就會含有 key 為 null 的 Entry,而且ThreadLocalMap 是在 Thread 中的,只要線程遲遲不結束,這些無法訪問到的 value 會形成內存泄漏。為了解決這個問題,ThreadLocalMap 中的 getEntry()、set() 和 remove() 函數都會清理 key 為 null 的 Entry。

在上文中我們發現了 ThreadLocalMap 的 key 是一個弱引用,那么為什么使用弱引用呢?使用強引用key與弱引用key的差別如下:

  • 強引用 key:ThreadLocal 被設置為 null,由于 ThreadLocalMap 持有 ThreadLocal 的強引用,如果不手動刪除,那么 ThreadLocal 將不會回收,產生內存泄漏。
  • 弱引用 key:ThreadLocal 被設置為 null,由于 ThreadLocalMap 持有 ThreadLocal 的弱引用,即便不手動刪除,ThreadLocal 仍會被回收,ThreadLocalMap 在之后調用 set()、getEntry() 和 remove() 函數時會清除所有 key 為 null 的 Entry。

但要注意的是,ThreadLocalMap僅僅含有這些被動措施來補救內存泄漏問題。如果你在之后沒有調用ThreadLocalMap 的 set()、getEntry() 和 remove() 函數的話,那么仍然會存在內存泄漏問題。
在使用線程池的情況下,如果不及時進行清理,內存泄漏問題事小,甚至還會產生程序邏輯上的問題。所以,為了安全地使用 ThreadLocal,必須要像每次使用完鎖就解鎖一樣,在每次使用完 ThreadLocal 后都要調用 remove() 來清理無用的Entry。


引用:
聊一聊 Spring 中的線程安全性

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • Spring Cloud為開發人員提供了快速構建分布式系統中一些常見模式的工具(例如配置管理,服務發現,斷路器,智...
    卡卡羅2017閱讀 134,923評論 18 139
  • 1. Java基礎部分 基礎部分的順序:基本語法,類相關的語法,內部類的語法,繼承相關的語法,異常的語法,線程的語...
    子非魚_t_閱讀 31,765評論 18 399
  • 文章來源:http://www.54tianzhisheng.cn/2017/06/04/Java-Thread/...
    beneke閱讀 1,522評論 0 1
  • 本文主要講了java中多線程的使用方法、線程同步、線程數據傳遞、線程狀態及相應的一些線程函數用法、概述等。 首先講...
    李欣陽閱讀 2,493評論 1 15
  • 6月--畢業 在學校上學時,多么希望早點畢業,擺脫學校無用的課程和考試。畢業晚會最后一杯酒碰撞之后,我知道,我終于...
    o翻滾的牛寶寶o閱讀 386評論 1 0