欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

java編程ThreadLocal上下傳遞源碼解析

 更新時(shí)間:2022年03月10日 17:29:21   作者:Q.E.D  
這篇文章主要為大家介紹了java編程中ThreadLocal提供的上下傳遞方式的源碼解析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步

引導(dǎo)語

ThreadLocal 提供了一種方式,讓在多線程環(huán)境下,每個(gè)線程都可以擁有自己獨(dú)特的數(shù)據(jù),并且可以在整個(gè)線程執(zhí)行過程中,從上而下的傳遞。

1、用法演示

可能很多同學(xué)沒有使用過 ThreadLocal,我們先來演示下 ThreadLocal 的用法,demo 如下:

/**
 * ThreadLocal 中保存的數(shù)據(jù)是 Map
 */
static final ThreadLocal<Map<String, String>> context = new ThreadLocal<>();
@Test
public void testThread() {
  // 從上下文中拿出 Map
  Map<String, String> contextMap = context.get();
  if (CollectionUtils.isEmpty(contextMap)) {
    contextMap = Maps.newHashMap();
  }
  contextMap.put("key1", "value1");
  context.set(contextMap);
  log.info("key1,value1被放到上下文中");
	// 從上下文中拿出剛才放進(jìn)去的數(shù)據(jù)
  getFromComtext();
}
private String getFromComtext() {
  String value1 = context.get().get("key1");
  log.info("從 ThreadLocal 中取出上下文,key1 對(duì)應(yīng)的值為:{}", value1);
  return value1;
}
//運(yùn)行結(jié)果:
demo.ninth.ThreadLocalDemo - key1,value1被放到上下文中
demo.ninth.ThreadLocalDemo - 從 ThreadLocal 中取出上下文,key1 對(duì)應(yīng)的值為:value1

從運(yùn)行結(jié)果中可以看到,key1 對(duì)應(yīng)的值已經(jīng)從上下文中拿到了。

getFromComtext 方法是沒有接受任何入?yún)⒌?,通過 context.get().get(“key1”) 這行代碼就從上下文中拿到了 key1 的值,接下來我們一起來看下 ThreadLocal 底層是如何實(shí)現(xiàn)上下文的傳遞的。

2、類結(jié)構(gòu)

2.1、類泛型

ThreadLocal 定義類時(shí)帶有泛型,說明 ThreadLocal 可以儲(chǔ)存任意格式的數(shù)據(jù),源碼如下:

public class ThreadLocal<T> {}

2.2、關(guān)鍵屬性

ThreadLocal 有幾個(gè)關(guān)鍵屬性,我們一一看下:

// threadLocalHashCode 表示當(dāng)前 ThreadLocal 的 hashCode,用于計(jì)算當(dāng)前 ThreadLocal 在 ThreadLocalMap 中的索引位置
private final int threadLocalHashCode = nextHashCode();
// 計(jì)算 ThreadLocal 的 hashCode 值(就是遞增)
private static int nextHashCode() {
    return nextHashCode.getAndAdd(HASH_INCREMENT);
}
// static + AtomicInteger 保證了在一臺(tái)機(jī)器中每個(gè) ThreadLocal 的 threadLocalHashCode 是唯一的
// 被 static 修飾非常關(guān)鍵,因?yàn)橐粋€(gè)線程在處理業(yè)務(wù)的過程中,ThreadLocalMap 是會(huì)被 set 多個(gè) ThreadLocal 的,多個(gè) ThreadLocal 就依靠 threadLocalHashCode 進(jìn)行區(qū)分
private static AtomicInteger nextHashCode = new AtomicInteger();

還有一個(gè)重要屬性:ThreadLocalMap,當(dāng)一個(gè)線程有多個(gè) ThreadLocal 時(shí),需要一個(gè)容器來管理多個(gè) ThreadLocal,ThreadLocalMap 的作用就是這個(gè),管理線程中多個(gè) ThreadLocal。

2.2.1、ThreadLocalMap

ThreadLocalMap 本身就是一個(gè)簡單的 Map 結(jié)構(gòu),key 是 ThreadLocal,value 是 ThreadLocal 保存的值,底層是數(shù)組的數(shù)據(jù)結(jié)構(gòu),源碼如下:

// threadLocalHashCode 表示當(dāng)前 ThreadLocal 的 hashCode,用于計(jì)算當(dāng)前 ThreadLocal 在 ThreadLocalMap 中的索引位置
private final int threadLocalHashCode = nextHashCode();
// 計(jì)算 ThreadLocal 的 hashCode 值(就是遞增)
private static int nextHashCode() {
    return nextHashCode.getAndAdd(HASH_INCREMENT);
}
// static + AtomicInteger 保證了在一臺(tái)機(jī)器中每個(gè) ThreadLocal 的 threadLocalHashCode 是唯一的
// 被 static 修飾非常關(guān)鍵,因?yàn)橐粋€(gè)線程在處理業(yè)務(wù)的過程中,ThreadLocalMap 是會(huì)被 set 多個(gè) ThreadLocal 的,多個(gè) ThreadLocal 就依靠 threadLocalHashCode 進(jìn)行區(qū)分
private static AtomicInteger nextHashCode = new AtomicInteger();

從源碼中看到 ThreadLocalMap 其實(shí)就是一個(gè)簡單的 Map 結(jié)構(gòu),底層是數(shù)組,有初始化大小,也有擴(kuò)容閾值大小,數(shù)組的元素是 Entry,Entry 的 key 就是 ThreadLocal 的引用,value 是 ThreadLocal 的值。

3、ThreadLocal 是如何做到線程之間數(shù)據(jù)隔離的

ThreadLocal 是線程安全的,我們可以放心使用,主要因?yàn)槭?ThreadLocalMap 是線程的屬性,我們看下線程 Thread 的源碼,如下:

圖片描述

從上圖中,我們可以看到 ThreadLocals.ThreadLocalMap 和 InheritableThreadLocals.ThreadLocalMap 分別是線程的屬性,所以每個(gè)線程的 ThreadLocals 都是隔離獨(dú)享的。

父線程在創(chuàng)建子線程的情況下,會(huì)拷貝 inheritableThreadLocals 的值,但不會(huì)拷貝 threadLocals 的值,源碼如下:

圖片描述

從上圖中我們可以看到,在線程創(chuàng)建時(shí),會(huì)把父線程的 inheritableThreadLocals 屬性值進(jìn)行拷貝。

4、set 方法 

set 方法的主要作用是往當(dāng)前 ThreadLocal 里面 set 值,假如當(dāng)前 ThreadLocal 的泛型是 Map,那么就是往當(dāng)前 ThreadLocal 里面 set map,源碼如下:

// set 操作每個(gè)線程都是串行的,不會(huì)有線程安全的問題
public void set(T value) {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    // 當(dāng)前 thradLocal 之前有設(shè)置值,直接設(shè)置,否則初始化
    if (map != null)
        map.set(this, value);
    // 初始化ThreadLocalMap
    else
        createMap(t, value);
}

代碼邏輯比較清晰,我們在一起來看下 ThreadLocalMap.set 的源碼,如下:

private void set(ThreadLocal<?> key, Object value) {
    Entry[] tab = table;
    int len = tab.length;
    // 計(jì)算 key 在數(shù)組中的下標(biāo),其實(shí)就是 ThreadLocal 的 hashCode 和數(shù)組大小-1取余
    int i = key.threadLocalHashCode & (len-1);
 
    // 整體策略:查看 i 索引位置有沒有值,有值的話,索引位置 + 1,直到找到?jīng)]有值的位置
    // 這種解決 hash 沖突的策略,也導(dǎo)致了其在 get 時(shí)查找策略有所不同,體現(xiàn)在 getEntryAfterMiss 中
    for (Entry e = tab[i];
         e != null;
         // nextIndex 就是讓在不超過數(shù)組長度的基礎(chǔ)上,把數(shù)組的索引位置 + 1
         e = tab[i = nextIndex(i, len)]) {
        ThreadLocal<?> k = e.get();
        // 找到內(nèi)存地址一樣的 ThreadLocal,直接替換
        if (k == key) {
            e.value = value;
            return;
        }
        // 當(dāng)前 key 是 null,說明 ThreadLocal 被清理了,直接替換掉
        if (k == null) {
            replaceStaleEntry(key, value, i);
            return;
        }
    }
    // 當(dāng)前 i 位置是無值的,可以被當(dāng)前 thradLocal 使用
    tab[i] = new Entry(key, value);
    int sz = ++size;
    // 當(dāng)數(shù)組大小大于等于擴(kuò)容閾值(數(shù)組大小的三分之二)時(shí),進(jìn)行擴(kuò)容
    if (!cleanSomeSlots(i, sz) && sz >= threshold)
        rehash();
}

上面源碼我們注意幾點(diǎn):

  • 是通過遞增的 AtomicInteger 作為 ThreadLocal 的 hashCode 的;
  • 計(jì)算數(shù)組索引位置的公式是:hashCode 取模數(shù)組大小,由于 hashCode 不斷自增,所以不同的 hashCode 大概率上會(huì)計(jì)算到同一個(gè)數(shù)組的索引位置(但這個(gè)不用擔(dān)心,在實(shí)際項(xiàng)目中,ThreadLocal 都很少,基本上不會(huì)沖突);
  • 通過 hashCode 計(jì)算的索引位置 i 處如果已經(jīng)有值了,會(huì)從 i 開始,通過 +1 不斷的往后尋找,直到找到索引位置為空的地方,把當(dāng)前 ThreadLocal 作為 key 放進(jìn)去。

好在日常工作中使用 ThreadLocal 時(shí),常常只使用 1~2 個(gè) ThreadLocal,通過 hash 計(jì)算出重復(fù)的數(shù)組的概率并不是很大。

set 時(shí)的解決數(shù)組元素位置沖突的策略,也對(duì) get 方法產(chǎn)生了影響,接著我們一起來看一下 get 方法。

5、get 方法

get 方法主要是從 ThreadLocalMap 中拿到當(dāng)前 ThreadLocal 儲(chǔ)存的值,源碼如下:

public T get() {
    // 因?yàn)?threadLocal 屬于線程的屬性,所以需要先把當(dāng)前線程拿出來
    Thread t = Thread.currentThread();
    // 從線程中拿到 ThreadLocalMap
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        // 從 map 中拿到 entry,由于 ThreadLocalMap 在 set 時(shí)的 hash 沖突的策略不同,導(dǎo)致拿的時(shí)候邏輯也不太一樣
        ThreadLocalMap.Entry e = map.getEntry(this);
        // 如果不為空,讀取當(dāng)前 ThreadLocal 中保存的值
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result;
        }
    }
    // 否則給當(dāng)前線程的 ThreadLocal 初始化,并返回初始值 null
    return setInitialValue();
}

接著我們來看下 ThreadLocalMap 的 getEntry 方法,源碼如下:

// 得到當(dāng)前 thradLocal 對(duì)應(yīng)的值,值的類型是由 thradLocal 的泛型決定的
// 由于 thradLocalMap set 時(shí)解決數(shù)組索引位置沖突的邏輯,導(dǎo)致 thradLocalMap get 時(shí)的邏輯也是對(duì)應(yīng)的
// 首先嘗試根據(jù) hashcode 取模數(shù)組大小-1 = 索引位置 i 尋找,找不到的話,自旋把 i+1,直到找到索引位置不為空為止
private Entry getEntry(ThreadLocal<?> key) {
    // 計(jì)算索引位置:ThreadLocal 的 hashCode 取模數(shù)組大小-1
    int i = key.threadLocalHashCode & (table.length - 1);
    Entry e = table[i];
    // e 不為空,并且 e 的 ThreadLocal 的內(nèi)存地址和 key 相同,直接返回,否則就是沒有找到,繼續(xù)通過 getEntryAfterMiss 方法找
    if (e != null && e.get() == key)
        return e;
    else
    // 這個(gè)取數(shù)據(jù)的邏輯,是因?yàn)?set 時(shí)數(shù)組索引位置沖突造成的  
        return getEntryAfterMiss(key, i, e);
}
// 自旋 i+1,直到找到為止
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
    Entry[] tab = table;
    int len = tab.length;
    // 在大量使用不同 key 的 ThreadLocal 時(shí),其實(shí)還蠻耗性能的
    while (e != null) {
        ThreadLocal<?> k = e.get();
        // 內(nèi)存地址一樣,表示找到了
        if (k == key)
            return e;
        // 刪除沒用的 key
        if (k == null)
            expungeStaleEntry(i);
        // 繼續(xù)使索引位置 + 1
        else
            i = nextIndex(i, len);
        e = tab[i];
    }
    return null;
}

get 邏輯源碼中注釋已經(jīng)寫的很清楚了,我們就不重復(fù)說了。

6、擴(kuò)容

ThreadLocalMap 中的 ThreadLocal 的個(gè)數(shù)超過閾值時(shí),ThreadLocalMap 就要開始擴(kuò)容了,我們一起來看下擴(kuò)容的邏輯:

//擴(kuò)容
private void resize() {
    // 拿出舊的數(shù)組
    Entry[] oldTab = table;
    int oldLen = oldTab.length;
    // 新數(shù)組的大小為老數(shù)組的兩倍
    int newLen = oldLen * 2;
    // 初始化新數(shù)組
    Entry[] newTab = new Entry[newLen];
    int count = 0;
    // 老數(shù)組的值拷貝到新數(shù)組上
    for (int j = 0; j < oldLen; ++j) {
        Entry e = oldTab[j];
        if (e != null) {
            ThreadLocal<?> k = e.get();
            if (k == null) {
                e.value = null; // Help the GC
            } else {
                // 計(jì)算 ThreadLocal 在新數(shù)組中的位置
                int h = k.threadLocalHashCode & (newLen - 1);
                // 如果索引 h 的位置值不為空,往后+1,直到找到值為空的索引位置
                while (newTab[h] != null)
                    h = nextIndex(h, newLen);
                // 給新數(shù)組賦值
                newTab[h] = e;
                count++;
            }
        }
    }
    // 給新數(shù)組初始化下次擴(kuò)容閾值,為數(shù)組長度的三分之二
    setThreshold(newLen);
    size = count;
    table = newTab;
}

源碼注解也比較清晰,我們注意兩點(diǎn):

  • 擴(kuò)容后數(shù)組大小是原來數(shù)組的兩倍;
  • 擴(kuò)容時(shí)是絕對(duì)沒有線程安全問題的,因?yàn)?ThreadLocalMap 是線程的一個(gè)屬性,一個(gè)線程同一時(shí)刻只能對(duì) ThreadLocalMap 進(jìn)行操作,因?yàn)橥粋€(gè)線程執(zhí)行業(yè)務(wù)邏輯必然是串行的,那么操作 ThreadLocalMap 必然也是串行的。

7、總結(jié)

ThreadLocal 是非常重要的 API,我們在寫一個(gè)中間件的時(shí)候經(jīng)常會(huì)用到,比如說流程引擎中上下文的傳遞,調(diào)用鏈ID的傳遞等等,非常好用,但坑也很多。

以上就是java編程ThreadLocal上下傳遞源碼解析的詳細(xì)內(nèi)容,更多關(guān)于java編程ThreadLocal上下傳遞的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!

相關(guān)文章

  • Mybatis-plus操作json字段實(shí)戰(zhàn)教程

    Mybatis-plus操作json字段實(shí)戰(zhàn)教程

    這篇文章主要介紹了Mybatis-plus操作json字段實(shí)戰(zhàn)教程,本文結(jié)合實(shí)例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下
    2023-02-02
  • Spring?Data?Jpa?中原生查詢?REGEXP?的使用詳解

    Spring?Data?Jpa?中原生查詢?REGEXP?的使用詳解

    這篇文章主要介紹了Spring?Data?Jpa?中原生查詢?REGEXP?的使用詳解,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教
    2021-12-12
  • Spring Boot webflux使用方法解析

    Spring Boot webflux使用方法解析

    這篇文章主要介紹了Spring Boot webflux使用方法解析,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下
    2020-03-03
  • 解決maven加載依賴時(shí)遇到的問題

    解決maven加載依賴時(shí)遇到的問題

    這篇文章主要介紹了解決maven加載依賴時(shí)遇到的問題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教
    2023-12-12
  • Java中ArrayList去除重復(fù)元素(包括字符串和自定義對(duì)象)

    Java中ArrayList去除重復(fù)元素(包括字符串和自定義對(duì)象)

    本文主要介紹了Java中ArrayList去除重復(fù)元素(包括字符串和自定義對(duì)象)的方法。具有很好的參考價(jià)值。下面跟著小編一起來看下吧
    2017-03-03
  • SpringBoot在接收參數(shù)的七種方式詳解

    SpringBoot在接收參數(shù)的七種方式詳解

    這篇文章主要介紹了SpringBoot在接收參數(shù)的七種方式詳解,隨著前后端的分離,接口方式開發(fā)成為普遍的開發(fā)形式,前端相對(duì)于后端來說,常用的接口傳參方式就一定要了解和熟悉,下面?我們梳理了常用的七種?Controller層接受參數(shù)的方式,需要的朋友可以參考下
    2023-10-10
  • SpringDataJPA實(shí)體類關(guān)系映射配置方式

    SpringDataJPA實(shí)體類關(guān)系映射配置方式

    這篇文章主要介紹了SpringDataJPA實(shí)體類關(guān)系映射配置方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教
    2021-12-12
  • Java代碼重用之功能與上下文重用

    Java代碼重用之功能與上下文重用

    代碼重用通常使得程序開發(fā)更加快速,并使得 BUG 減少。一旦一段代碼被封裝和重用,那么只需要檢查很少的一段代碼即可確保程序的正確性。接下來通過本文給大家介紹Java代碼重用之功能與上下文重用的相關(guān)知識(shí),感興趣的朋友跟隨腳本之家小編一起學(xué)習(xí)吧
    2018-05-05
  • 從log4j2到Disruptor詳解

    從log4j2到Disruptor詳解

    這篇文章主要介紹了從log4j2到Disruptor詳解,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教
    2021-12-12
  • Java如何計(jì)算兩個(gè)時(shí)間段內(nèi)的工作日天數(shù)

    Java如何計(jì)算兩個(gè)時(shí)間段內(nèi)的工作日天數(shù)

    這篇文章主要介紹了Java如何計(jì)算兩個(gè)時(shí)間段內(nèi)的工作日天數(shù),具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教
    2022-07-07

最新評(píng)論