天天看點

阿裡面試官問我ThreadLocal,我一口氣給他說了四種!

什麼是ThreadLocal

ThreadLocal

類顧名思義可以了解為線程本地變量。也就是說如果定義了一個

ThreadLocal

每個線程往這個

ThreadLocal

中讀寫是線程隔離,互相之間不會影響的。它提供了一種将可變資料通過每個線程有自己的獨立副本進而實作線程封閉的機制。

實際應用

實際開發中我們真正使用

ThreadLocal

的場景還是比較少的,大多數使用都是在架構裡面。最常見的使用場景的話就是用它來解決資料庫連接配接、

Session

管理等保證每一個線程中使用的資料庫連接配接是同一個。還有一個用的比較多的場景就是用來解決

SimpleDateFormat

解決線程不安全的問題,不過現在

java8

提供了

DateTimeFormatter

它是線程安全的,感興趣的同學可以去看看。還可以利用它進行優雅的傳遞參數,傳遞參數的時候,如果父線程生成的變量或者參數直接通過

ThreadLocal

傳遞到子線程參數就會丢失,這個後面會介紹一個其他的

ThreadLocal

來專門解決這個問題的。

ThreadLocal api介紹

ThreadLocal的API還是比較少的就幾個api

阿裡面試官問我ThreadLocal,我一口氣給他說了四種!

我們看下這幾個

api

的使用,使用起來也超級簡單

private static ThreadLocal<String> threadLocal = ThreadLocal.withInitial(()->"java金融");
public static void main(String[] args) {
    System.out.println("擷取初始值:"+threadLocal.get());
    threadLocal.set("關注:【java金融】");
    System.out.println("擷取修改後的值:"+threadLocal.get());
    threadLocal.remove();
}
           

輸出結果:

擷取初始值:java金融

擷取修改後的值:關注:【java金融】

是不是炒雞簡單,就幾行代碼就把所有

api

都覆寫了。下面我們就來簡單看看這幾個

api

的源碼吧。

成員變量

/**初始容量,必須為2的幂
     * The initial capacity -- MUST be a power of two.
     */
    private static final int INITIAL_CAPACITY = 16;

    /** Entry表,大小必須為2的幂
     * The table, resized as necessary.
     * table.length MUST always be a power of two.
     */
    private Entry[] table;

    /**
     * The number of entries in the table.
     */
    private int size = 0;

    /**
     * The next size value at which to resize.
     */
    private int threshold; // Default to 0
           

這裡會有一個面試經常問到的問題:為什麼

entry

數組的大小,以及初始容量都必須是

2

的幂?對于

firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);

以及很多源碼裡面都是使用

hashCode &( $2^n$-1) 來代替hashCode% $2^n$。

這種寫法好處如下:

  • 使用位運算替代取模,提升計算效率。
  • 為了使不同

    hash

    值發生碰撞的機率更小,盡可能促使元素在哈希表中均勻地散列。

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

set

方法還是比較簡單的,我們可以重點看下這個方法裡面的ThreadLocalMap,它既然是個map(注意不要與

java.util.map

混為一談,這裡指的是概念上的

map

),肯定是有自己的key和value組成,我們根據源碼可以看出它的

key

是其實可以把它簡單看成是

ThreadLocal

,但是實際上ThreadLocal中存放的是

ThreadLocal

的弱引用,而它的

value

的話是我們實際

set

的值

static class Entry extends WeakReference> {

/** The value associated with this ThreadLocal. */
        Object value; // 實際存放的值

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

Entry

就是是

ThreadLocalMap

裡定義的節點,它繼承了

WeakReference

類,定義了一個類型為

Object

value

,用于存放塞到

ThreadLocal

裡的值。我們再來看下這個

ThreadLocalMap

是位于哪裡的?我們看到

ThreadLocalMap

是位于

Thread

裡面的一個變量,而我們的值又是放在

ThreadLocalMap

,這樣的話我們就實作了每個線程間的隔離。下面兩張圖的基本就把

ThreadLocal

的結構給介紹清楚了。

阿裡面試官問我ThreadLocal,我一口氣給他說了四種!
阿裡面試官問我ThreadLocal,我一口氣給他說了四種!

接下來我們再看下

ThreadLocalMap

裡面的資料結構,我們知道

HaseMap

解決

hash

沖突是由連結清單和紅黑樹(

jdk1.8

)來解決的,但是這個我們看到

ThreadLocalMap

隻有一個數組,它是怎麼來解決

hash

沖突呢?

ThreadLocalMap

采用線性探測的方式,什麼是線性探測呢?就是根據初始

key

的hashcode值确定元素在

table

數組中的位置,如果發現這個位置上已經有其他

key

值的元素被占用,則利用固定的算法尋找一定步長的下個位置,依次判斷,直至找到能夠存放的位置。

ThreadLocalMap

Hash

沖突的方式就是簡單的步長加

1

或減

1

,尋找下一個相鄰的位置。

/**
     * Increment i modulo len.
     */
    private static int nextIndex(int i, int len) {
        return ((i + 1 < len) ? i + 1 : 0);
    }

    /**
     * Decrement i modulo len.
     */
    private static int prevIndex(int i, int len) {
        return ((i - 1 >= 0) ? i - 1 : len - 1);
    }
           

這種方式的話如果一個線程裡面有大量的

ThreadLocal

就會産生性能問題,因為每次都需要對這個

table

進行周遊,清空無效的值。是以我們在使用的時候盡可能的使用少的

ThreadLocal

,不要線上程裡面建立大量的

ThreadLocal

,如果需要設定不同的參數類型我們可以通過

ThreadLocal

來存放一個

Object

Map

這樣的話,可以大大減少建立

ThreadLocal

的數量。

僞代碼如下:

public final class HttpContext {

private HttpContext() {
}
private static final ThreadLocal<Map<String, Object>> CONTEXT = ThreadLocal.withInitial(() -> new ConcurrentHashMap(64));
public static <T> void add(String key, T value) {
    if(StringUtils.isEmpty(key) || Objects.isNull(value)) {
        throw new IllegalArgumentException("key or value is null");
    }
    CONTEXT.get().put(key, value);
}
public static <T> T get(String key) {
    return (T) get().get(key);
}
public static Map<String, Object> get() {
    return CONTEXT.get();
}
public static void remove() {
    CONTEXT.remove();
}           

}

這樣的話我們如果需要傳遞不同的參數,可以直接使用一個

ThreadLocal

就可以代替多個

ThreadLocal

了。

如果覺得不想這麼玩,我就是要建立多個

ThreadLocal

,我的需求就是這樣,而且性能還得要好,這個能不能實作列?可以使用

netty

FastThreadLocal

可以解決這個問題,不過要配合使

FastThreadLocalThread

或者它子類的線程線程效率才會更高,更多關于它的使用可以自行查閱資料哦。

下面我們先來看下它的這個哈希函數

// 生成hash code間隙為這個魔數,可以讓生成出來的值或者說ThreadLocal的ID較為均勻地分布在2的幂大小的數組中。
private static final int HASH_INCREMENT = 0x61c88647;

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

可以看出,它是在上一個被構造出的

ThreadLocal

ID/threadLocalHashCode

的基礎上加上一個魔數

0x61c88647

的。這個魔數的選取與斐波那契散列有關,

0x61c88647

對應的十進制為

1640531527

.當我們使用

0x61c88647

這個魔數累加對每個

ThreadLocal

配置設定各自的

ID

也就是

threadLocalHashCode

再與

2

的幂(數組的長度)取模,得到的結果分布很均勻。我們可以來也示範下通過這個魔數

public class MagicHashCode {

private static final int HASH_INCREMENT = 0x61c88647;

public static void main(String[] args) {
    hashCode(16); //初始化16
    hashCode(32); //後續2倍擴容
    hashCode(64);
}

private static void hashCode(Integer length) {
    int hashCode = 0;
    for (int i = 0; i < length; i++) {
        hashCode = i * HASH_INCREMENT + HASH_INCREMENT;//每次遞增HASH_INCREMENT
        System.out.print(hashCode & (length - 1));
        System.out.print(" ");
    }
    System.out.println();
}           

運作結果:

7 14 5 12 3 10 1 8 15 6 13 4 11 2 9 0

7 14 21 28 3 10 17 24 31 6 13 20 27 2 9 16 23 30 5 12 19 26 1 8 15 22 29 4 11 18 25 0

7 14 21 28 35 42 49 56 63 6 13 20 27 34 41 48 55 62 5 12 19 26 33 40 47 54 61 4 11 18 25 32 39 46 53 60 3 10 17 24 31 38 45 52 59 2 9 16 23 30 37 44 51 58 1 8 15 22 29 36 43 50 57 0

不得不佩服下這個作者,通過使用了斐波那契散列法,來保證哈希表的離散度,讓結果很均勻。可見代碼要寫的好,數學還是少不了啊。其他的源碼就不分析了,大家感興趣可以自行去檢視下。

ThreadLocal的記憶體洩露

關于

ThreadLocal

是否會引起記憶體洩漏也是一個比較有争議性的問題。首先我們需要知道什麼是記憶體洩露?

在Java中,記憶體洩漏就是存在一些被配置設定的對象,這些對象有下面兩個特點,首先,這些對象是可達的,即在有向圖中,存在通路可以與其相連;其次,這些對象是無用的,即程式以後不會再使用這些對象。如果對象滿足這兩個條件,這些對象就可以判定為Java中的記憶體洩漏,這些對象不會被GC所回收,然而它卻占用記憶體。

ThreadLocal

的記憶體洩露情況:

  • 線程的生命周期很長,當

    ThreadLocal

    沒有被外部強引用的時候就會被

    GC

    回收(給

    ThreadLocal

    置空了):

    ThreadLocalMap

    會出現一個

    key

    null

    Entry

    ,但這個

    Entry

    value

    将永遠沒辦法被通路到(後續在也無法操作

    set、get

    等方法了)。如果當這個線程一直沒有結束,那這個

    key

    null

    Entry

    因為也存在強引用(

    Entry.value

    ),而

    Entry

    被目前線程的

    ThreadLocalMap

    強引用(

    Entry[] table

    ),導緻這個

    Entry.value

    永遠無法被

    GC

    ,造成記憶體洩漏。

    下面我們來示範下這個場景

public static void main(String[] args) throws InterruptedException {

ThreadLocal<Long []> threadLocal = new ThreadLocal<>();
    for (int i = 0; i < 50; i++) {
        run(threadLocal);
    }
    Thread.sleep(50000);
    // 去除強引用
    threadLocal = null;
    System.gc();
    System.runFinalization();
    System.gc();
}

private static void run(ThreadLocal<Long []> threadLocal) {
    new Thread(() -> {
        threadLocal.set(new Long[1024 * 1024 *10]);
        try {
            Thread.sleep(1000000000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }).start();
}
           

通過

jdk

自帶的工具

jconsole.exe

會發現即使執行了

gc

記憶體也不會減少,因為key還被線程強引用着。效果圖如下:

阿裡面試官問我ThreadLocal,我一口氣給他說了四種!
  • 針對于這種情況

    ThreadLocalMap

    在設計中,已經考慮到這種情況的發生,你隻要調用

    了set()、get()、remove()

    方法都會調用

    cleanSomeSlots()、expungeStaleEntry()

    方法去清除

    key

    null

    value

    。這是一種被動的清理方式,但是如果

    ThreadLocal

    set(),get(),remove()

    方法沒有被調用,就會導緻

    value

    的記憶體洩漏。它的文檔推薦我們使用

    static

    修飾的

    ThreadLocal

    ,導緻

    ThreadLocal

    的生命周期和持有它的類一樣長,由于

    ThreadLocal

    有強引用在,意味着這個

    ThreadLocal

    不會被

    GC

    。在這種情況下,我們如果不手動删除,

    Entry

    key

    永遠不為

    null

    ,弱引用也就失去了意義。是以我們在使用的時候盡可能養成一個好的習慣,使用完成後手動調用下

    remove

    方法。其實實際生産環境中我們手動

    remove

    大多數情況并不是為了避免這種

    key

    null

    的情況,更多的時候,是為了保證業務以及程式的正确性。比如我們下單請求後通過

    ThreadLocal

    建構了訂單的上下文請求資訊,然後通過線程池異步去更新使用者積分,這時候如果更新完成,沒有進行

    remove

    操作,即使下一次新的訂單會覆寫原來的值但是也是有可能會導緻業務問題。

如果不想手動清理是否還有其他方式解決下列?

FastThreadLocal

可以去了解下,它提供了自動回收機制。

  • 線上程池的場景,程式不停止,線程一直在複用的話,基本不會銷毀,其實本質就跟上面例子是一樣的。如果線程不複用,用完就銷毀了就不會存在洩露的情況。因為線程結束的時候會

    jvm

    主動調用

    exit

    方法清理。
    /**           
    • This method is called by the system to give a Thread
    • a chance to clean up before it actually exits.

      */

    private void exit() {
    if (group != null) {
        group.threadTerminated(this);
        group = null;
    }
    /* Aggressively null out all reference fields: see bug 4006245 */
    target = null;
    /* Speed the release of some of these resources */
    threadLocals = null;
    inheritableThreadLocals = null;
    inheritedAccessControlContext = null;
    blocker = null;
    uncaughtExceptionHandler = null;           

InheritableThreadLocal

文章開頭有提到過父子之間線程的變量傳遞丢失的情況。但是

InheritableThreadLocal

提供了一種父子線程之間的資料共享機制。可以解決這個問題。

static ThreadLocal threadLocal = new ThreadLocal<>();

static InheritableThreadLocal<String> inheritableThreadLocal = new InheritableThreadLocal<>();

public static void main(String[] args) throws InterruptedException {
    threadLocal.set("threadLocal主線程的值");
    Thread.sleep(100);
    new Thread(() -> System.out.println("子線程擷取threadLocal的主線程值:" + threadLocal.get())).start();
    Thread.sleep(100);
    inheritableThreadLocal.set("inheritableThreadLocal主線程的值");
    new Thread(() -> System.out.println("子線程擷取inheritableThreadLocal的主線程值:" + inheritableThreadLocal.get())).start();

}
           

輸出結果

線程擷取threadLocal的主線程值:null

子線程擷取inheritableThreadLocal的主線程值:inheritableThreadLocal主線程的值

但是

InheritableThreadLocal

和線程池使用的時候就會存在問題,因為子線程隻有線上程對象建立的時候才會把父線程

inheritableThreadLocals

中的資料複制到自己的

inheritableThreadLocals

中。這樣就實作了父線程和子線程的上下文傳遞。但是線程池的話,線程會複用,是以會存在問題。如果要解決這個問題可以有什麼辦法列?大家可以思考下,或者在下方留言哦。如果實在不想思考的話,可以參考下阿裡巴巴的

transmittable-thread-local

哦。

總結

  • 大概介紹了

    ThreadLocal

    的常見用法,以及大緻實作原理,以及關于

    ThreadLocal

    的記憶體洩露問題,以及關于使用它需要注意的事項,以及如何解決父子線程之間的傳遞。介紹了

    ThreadLocal、InheritableThreadLocal、FastThreadLocal、transmittable-thread-local

    各種使用場景,以及需要注意的事項。本文重點介紹了

    ThreadLocal

    ,如果把這個弄清楚了,其他幾種ThreadLocal就更好了解了。

結束

  • 由于自己才疏學淺,難免會有纰漏,假如你發現了錯誤的地方,還望留言給我指出來,我會對其加以修正。
  • 如果你覺得文章還不錯,你的轉發、分享、贊賞、點贊、留言就是對我最大的鼓勵。
  • 感謝您的閱讀,十分歡迎并感謝您的關注。

巨人的肩膀摘蘋果:

https://zhuanlan.zhihu.com/p/40515974 https://www.cnblogs.com/aspirant/p/8991010.html https://www.cnblogs.com/jiangxinlingdu/p/11123538.html https://blog.csdn.net/hewenbo111/article/details/80487252