天天看點

面試必備之ThreadLocal

概述

通常情況下,一個類的變量是可以被任何一個線程通路并修改的。而使用ThreadLocal建立的變量隻能被目前線程通路,其他線程則無法通路和修改。

采用空間換時間,用于線程間的資料隔離,為每一個使用該變量的線程提供一個副本,每個線程都可以獨立地改變自己的副本,而不會和其他線程的副本沖突。資料都被封閉在各自的線程之中,就不需要同步,這種通過将資料封閉線上程中而避免使用同步的技術稱為線程封閉。

ThreadLocal類中維護一個Map,用于存儲每一個線程的變量副本,Map中元素的鍵為線程對象,而值為對應線程的變量副本(局部變量)。

執行個體

public class ThreadLocalTest {
    public static class MyRunnable implements Runnable {
        private ThreadLocal<Integer> threadLocal = new ThreadLocal<>();
        @Override
        public void run() {
            threadLocal.set((int) (Math.random() * 100D));
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(threadLocal.get());
            threadLocal.remove();
        }
    }

    public static void main(String[] args) {
        MyRunnable sharedRunnableInstance = new MyRunnable();
        Thread thread1 = new Thread(sharedRunnableInstance);
        Thread thread2 = new Thread(sharedRunnableInstance);
        thread1.start();
        thread2.start();
    }
}      

代碼非常簡單,啟動兩個線程,分别設定一個小于100的随機數,然後取出來。

分析

ThreadLocal是一個泛型類,重要屬性包括:

// 目前 ThreadLocal的hashCode,由 nextHashCode() 計算而來,用于計算目前 ThreadLocal 在 ThreadLocalMap 中的索引位置
private final int threadLocalHashCode = nextHashCode();
// 哈希魔數,主要與斐波那契散列法以及黃金分割有關
private static final int HASH_INCREMENT = 0x61c88647;
// 傳回計算出的下一個哈希值,其值為 i * HASH_INCREMENT,其中 i 代表調用次數
private static int nextHashCode() {
    return nextHashCode.getAndAdd(HASH_INCREMENT);
}
// 保證在一台機器中每個 ThreadLocal 的 threadLocalHashCode 是唯一的
private static AtomicInteger nextHashCode = new AtomicInteger();      

其提供的主要方法如下:

// 擷取值,如果沒有傳回 null
public T get()
public void set(T)
// 提供初始值,當調用 get 方法時如果之前沒有設定過則會調用該方法擷取初始值,預設為 null
protected T initialValue()
// 删掉目前線程對應的值,如果删掉後再次調用get,則會再調用initialValue擷取初值
public void remove()      

其中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);
}      

ThreadLocal類中的getMap方法

ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}
// Thread類中聲明的threadLocals變量
ThreadLocal.ThreadLocalMap threadLocals = null;      

其中,ThreadLocalMap

每個線程維護一個ThreadLocalMap映射表,映射表的key是ThreadLocal執行個體,使用的是ThreadLocal的弱引用 ,value是具體需要存儲的Object。有ThreadLocalMap的内部類,該類為一個采用線性探測法實作的HashMap。它的key為ThreadLocal對象而且還使用WeakReference,ThreadLocalMap正是用來存儲變量副本的。

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

問題

ThreadLocal沒有外部強引用,當發生垃圾回收時,這個ThreadLocal一定會被回收(弱引用,不管目前記憶體空間足夠與否,GC時都會被回收),這樣就會導緻ThreadLocalMap中出現key為null的Entry,外部将不能擷取這些key為null的Entry的value,并且如果目前線程一直存活,那麼就會存在一條強引用鍊:​​

​Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value​

​,導緻value對應的Object一直無法被回收,産生記憶體洩露。ThreadLocal的get、set和remove方法都實作對所有key為null的value的清除,但仍可能會發生記憶體洩露,因為可能使用ThreadLocal的get或set方法後發生GC,此後不調用get、set或remove方法,為null的value就不會被清除。

解決方法:

  1. 在使用完後,沒有remove
  2. private static,這樣就一直存在ThreadLocal的強引用,也就能保證任何時候都能通過ThreadLocal的弱引用通路到Entry的value值,進而清除掉

其中 static 是為了確定全局隻有一個儲存 String 對象的 ThreadLocal 執行個體;final 確定 ThreadLocal 的執行個體不可更改,防止被意外改變,導緻放入的值和取出來的不一緻,另外還能防止 ThreadLocal 的記憶體洩漏。

用途

  1. 實作單個線程單例以及單個線程上下文資訊存儲。ThreadLocal在Spring中處處可見:管理Request作用域中的Bean、事務管理、任務排程、AOP等子產品。Spring中絕大部分Bean都可以聲明成Singleton作用域,采用ThreadLocal進行封裝,是以有狀态的Bean就能夠以singleton的方式在多線程中正常工作。又,常用于使用者登入控制,如記錄session資訊。
  2. 非線程安全的對象使用ThreadLocal之後就會變得線程安全。如SimpleDateFormat,SDF,在多線程并發的情況下是線程不安全的,可能會抛出NumberFormatException或其它異常。使用ThreadLocal包裝,直接建立一個共享執行個體對象,每個線程都有自己的SDF執行個體對象。
// SDF非線程安全的原因:CalendarBuilder類的establish方法部分代碼
Calendar establish(Calendar cal) {
    // 省略部分不重要代碼
    cal.clear();
    // 省略部分不重要代碼
    return cal;
}      
  1. 承載一些線程相關的資料,避免在方法中來回傳遞參數

ThreadLocal vs Synchronized

都能實作多線程環境下的共享變量的線程安全,差別呢?

Synchronized通過鎖(同步)來實作記憶體共享,ThreadLocal為每個線程維護一個本地變量,即通過避免對象的共享來實作。鎖更強調的是如何同步多個線程去正确地共享一個變量,ThreadLocal則是為了解決同一個變量如何不被多個線程共享。如果鎖機制是用時間換空間的話,那麼ThreadLocal就是用空間換時間。

同步機制是為了同步多線程對相同資源的并發通路,是為了線程間資料共享的問題,而 ThreadLocal 是隔離多線程資料共享,從根本上就不在多個線程之間共享資源,這樣自然就不需要多線程的同步機制。

拓展

基于ThreadLocal思想的拓展類有很多。并且在其他語言裡面也有這種思想的實作。

線程局部存儲

ThreadLocal底層的技術。TLS,thread local storage。全局變量與函數内定義的靜态變量,是各個線程都可以通路的共享變量。

​​線程局部存儲-百科​​線程局部存儲TLS

線程局部握手

Thread-Local Handshakes,JDK 10引入的技術,也叫線程本地握手,不執行全局VM安全點也能對線程執行回調,同時實作單線程停止回調。利用所謂的非對稱Dekker同步技術,通過與Java線程握手來消除一些記憶體障礙(memory barrier)。不再依賴記憶體屏障實作同步,直接使用Dekker算法。

Handshakes和普通的safepoint機制有所不同:

A handshake operation is a callback that is executed for each JavaThread while that thread is in a safepoint safe state. The callback is executed either by the thread itself or by the VM thread while keeping the thread in a blocked state. The big difference between safepointing and handshaking is that the per thread operation will be performed on all threads as soon as possible and they will continue to execute as soon as it’s own operation is completed. If a JavaThread is known to be running, then a handshake can be performed with that single JavaThread as well.

In the initial implementation there will be a limitation of at most one handshake operation in flight at a given time. The operation can, however, involve any subset of all JavaThreads. The VM thread will coordinate the handshake operation through a VM operation which will in effect prevent global safepoints from occurring during the handshake operation.

The current safepointing scheme is modified to perform an indirection through a per-thread pointer which will allow a single thread’s execution to be forced to trap on the guard page. Essentially, at all times there will be two polling pages: One which is always guarded, and one which is always unguarded. In order to force a thread to yield, the VM updates the per-thread pointer for the corresponding thread to point to the guarded page.

Thread-local handshakes will be implemented initially on x64 and SPARC. Other platforms will fall back to normal safepoints. A new product option, -XX:ThreadLocalHandshakes (default value true), allows users to select normal safepoints on supported platforms.

ThreadLocalRandom

待學習

InheritableThreadLocal

在子線程中擷取主線程threadLocal中set方法設定的值,如何實作?

使用InheritableThreadLocal。通過​​

​ThreadLocal threadLocal = new InheritableThreadLocal()​

​​,在子線程中就可以通過get方法擷取到主線程set方法設定的值。

InheritableThreadLocal繼承ThreadLocal重寫​​

​childValue​

​​、​

​getMap​

​​和​

​createMap​

​方法:

/**
 * 在父線程建立子線程,向子線程複制InheritableThreadLocal變量時使用
 */
protected T childValue(T parentValue) {
    return parentValue;
}
/**
 * 重寫getMap,操作InheritableThreadLocal時,将隻影響Thread類中的inheritableThreadLocals變量,與threadLocals變量不再有關系
 */
ThreadLocalMap getMap(Thread t) {
    return t.inheritableThreadLocals;
}
/**
 * 類似于getMap,操作InheritableThreadLocal時,将隻影響Thread類中的inheritableThreadLocals變量,
 * 與threadLocals變量不再有關系
 */
void createMap(Thread t, T firstValue) {
    t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
}      

當使用InheritableThreadLocal建立執行個體對象時,目前線程Thread對象中維護一個inheritableThreadLocals變量,也是ThreadLocalMap類型,在建立子線程的過程中,将主線程維護的inheritableThreadLocals變量的值複制到子線程維護的inheritableThreadLocals變量中,這樣子線程就可以擷取到主線程設定的值。

應用

調用鍊追蹤:在調用鍊系統設計中,為了優化系統運作速度,會使用多線程程式設計,為了保證調用鍊ID能夠自然的在多線程間傳遞,需要考慮ThreadLocal傳遞問題。

TransmittableThreadLocal

阿裡開源,​​GitHub​​

​​TransmittableThreadLocal詳解​​增強版的ThreadLocal-TransmittableThreadLocal

FastThreadLocal

參考