JAVA concurrency 之ThreadLocal源碼詳解,80%人不會

来源:https://www.cnblogs.com/chengxuyuanaa/archive/2020/06/04/13043020.html
-Advertisement-
Play Games

概述 1、在併發編程中,為了控制數據的正確性,我們往往需要使用鎖來來保證代碼塊的執行隔離性。但是在很多時候鎖的開銷太大了,而在某些情況下,我們的局部變數是線程私有的,每個線程都會有自己的獨自的變/量,這個時候我們可以不對這部分數據進行加鎖操作。於是ThredLocal應運而生。 2、ThredLoc ...


 

JAVA concurrency -- ThreadLocal 源碼詳解

概述

1、在併發編程中,為了控制數據的正確性,我們往往需要使用鎖來來保證代碼塊的執行隔離性。但是在很多時候鎖的開銷太大了,而在某些情況下,我們的局部變數是線程私有的,每個線程都會有自己的獨自的變/量,這個時候我們可以不對這部分數據進行加鎖操作。於是ThredLocal應運而生。

2、ThredLocal顧名思義,是線程持有的本地變數,存放在ThredLocal中的變數不會同步到其他線程以及主線程,所有線程對於其他的線程變數都是不可見的。那麼我們來看下它是如何實現的吧。
3、註意:光理論是不夠的。在此免費贈送5大JAVA架構項目實戰教程及大廠面試題庫,有興趣的可以進裙 783802103獲取,沒基礎勿進哦!

實現原理

ThredLocal在內部實現了一個靜態類ThreadLocalMap來對於變數進行存儲,並且在Thread類的內部使用到了這兩個成員變數

    ThreadLocal.ThreadLocalMap threadLocals = null;
    ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;

來調用ThreadLocalMap存儲當前線程的內部變數。

ThreadLocalMap的實現

ThreadLocalMap是鍵值對結構的map,但是他沒有直接使用HashMap,而是自己實現了一個。

Entry

EntryThreadLocalMap中定義的map節點,他以ThreadLocal弱引用為key,以Object為value的K-V形式的節點。使用弱引用是為了可以及時釋放記憶體避免記憶體泄漏。

    static class Entry extends WeakReference<ThreadLocal<?>> {
        Object value;

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

這裡和HashMap不一樣的地方在於兩者解決hash衝突的方式的不同,HashMap採用的是鏈地址法,遇到衝突的時候將衝突的數據放入同一鏈表之中,等到鏈表到了一定程度再將鏈表轉化為紅黑樹。而ThreadLocalMap實現採用的是開放定址法,它內部沒有使用鏈表結構,因此Entry內部沒有next或者是prev指針。ThreadLocalMap的開放定址法是怎麼實現的,請看接下來的源碼。

成員變數

    // map預設的初始化大小
    private static final int INITIAL_CAPACITY = 16;

    // 存儲map節點數據的數組
    private Entry[] table;

    // map大小
    private int size = 0;

    // 臨界值,達到這個值的時候需要擴容
    private int threshold;

    // 當臨界值達到2/3的時候擴容
    private void setThreshold(int len) {
        threshold = len * 2 / 3;
    }

這裡的數組大小始終是2的冪次,原因和HashMap一樣,是為了在計算hash偏移的時候減少碰撞。

構造函數

    ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
        // 初始化table
        table = new Entry[INITIAL_CAPACITY];
        // 計算第一個值的hash值
        int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
        // 創建新的節點
        table[i] = new Entry(firstKey, firstValue);
        size = 1;
        setThreshold(INITIAL_CAPACITY);
    }

set方法

    private void set(ThreadLocal<?> key, Object value) {

        // 獲取ThreadLocal的hash值偏移量
        Entry[] tab = table;
        int len = tab.length;
        int i = key.threadLocalHashCode & (len-1);

        // 遍曆數組直到節點為空
        for (Entry e = tab[i];
                e != null;
                e = tab[i = nextIndex(i, len)]) {
            ThreadLocal<?> k = e.get();

            // 如果節點key相等,即找到了我們想要的節點,
            // 將值賦予節點
            if (k == key) {
                e.value = value;
                return;
            }

            // 如果節點的key為空,說明弱引用已經把key回收了,那麼需要做一波清理
            if (k == null) {
                replaceStaleEntry(key, value, i);
                return;
            }
        }

        // 如果沒有找到對應的節點說明該key不存在,創建新節點
        tab[i] = new Entry(key, value);
        int sz = ++size;
        // 進行清理,如果清理結果沒能清理掉任何的舊節點,
        // 並且數組大小超出了臨界值,就進行rehash操作擴容
        if (!cleanSomeSlots(i, sz) && sz >= threshold)
            rehash();
    }

看到這段代碼,開放定址法的實現原理可以說是非常清楚了。首先計算節點的hash值,找到對應的位置,查看該位置是否為空,如果是空則插入,如果不為空,則順延至下個節點,直到找到空的位置插入。那麼我們的查詢邏輯也呼之欲出:計算節點的hash值,找到對應的位置,查看該節點是否是我們想要找的節點,如果不是,則繼續往下順序尋找。

get方法

    private Entry getEntry(ThreadLocal<?> key) {
        // 計算hash值
        int i = key.threadLocalHashCode & (table.length - 1);
        // 獲取該hash值對應的數組節點
        Entry e = table[i];
        if (e != null && e.get() == key)
            // 如果節點不為空並且key一致,說明是我們找的節點,直接返回
            return e;
        else
            // 否則繼續往後尋找
            return getEntryAfterMiss(key, i, e);
    }
    
    private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
        Entry[] tab = table;
        int len = tab.length;

        // 如果節點不為空就一直找下去
        while (e != null) {
            ThreadLocal<?> k = e.get();
            // key相同則說明找到,返回該節點
            if (k == key)
                return e;
            // key為空進行一次清理
            if (k == null)
                expungeStaleEntry(i);
            else
                i = nextIndex(i, len);
            e = tab[i];
        }
        return null;
    }

replaceStaleEntry

    // 這個方法的作用是在set操作的時候進行清理
    private void replaceStaleEntry(ThreadLocal<?> key, Object value,
                                       int staleSlot) {
        Entry[] tab = table;
        int len = tab.length;
        Entry e;
        
        // slotToExpunge是之後開始清理的節點位置
        int slotToExpunge = staleSlot;
        // 往前尋找找到第一個為空的節點記錄下位置
        for (int i = prevIndex(staleSlot, len);
                (e = tab[i]) != null;
                i = prevIndex(i, len))
            if (e.get() == null)
                slotToExpunge = i;

        // 從staleSlot開始向後遍歷直到節點為空
        for (int i = nextIndex(staleSlot, len);
                (e = tab[i]) != null;
                i = nextIndex(i, len)) {
            ThreadLocal<?> k = e.get();

            if (k == key) {
                // 如果節點的key一致,替換value的值
                e.value = value;

                // 將當前節點和staleSlot上的節點互換位置(將後方的值放到前方來,之前的值等待回收)
                tab[i] = tab[staleSlot];
                tab[staleSlot] = e;

                // 如果slotToExpunge和staleSlot相等,說明前面沒有需要清理的節點
                // 則從當前節點開始進行清理
                if (slotToExpunge == staleSlot)
                    slotToExpunge = i;
                // 進行節點清理
                cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
                return;
            }

            // 如果key為空並且slotToExpunge和staleSlot相等
            // 把slotToExpunge賦值為當前節點
            if (k == null && slotToExpunge == staleSlot)
                slotToExpunge = i;
        }

        // 如果沒法找到key相等的節點,
        // 則清空當前節點的value並生成新的節點
        tab[staleSlot].value = null;
        tab[staleSlot] = new Entry(key, value);

        // 如果slotToExpunge和staleSlot不相等則需要進行清理(因為前方發現空的節點)
        if (slotToExpunge != staleSlot)
            cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
    }

expungeStaleEntry

    // 對節點進行清理
    private int expungeStaleEntry(int staleSlot) {
        Entry[] tab = table;
        int len = tab.length;

        // 釋放當節點
        tab[staleSlot].value = null;
        tab[staleSlot] = null;
        size--;

        Entry e;
        int i;
        // 迴圈尋找到第一個空節點
        for (i = nextIndex(staleSlot, len);
                (e = tab[i]) != null;
                i = nextIndex(i, len)) {
            ThreadLocal<?> k = e.get();
            // key為空進行節點釋放
            if (k == null) {
                e.value = null;
                tab[i] = null;
                size--;
            } else {
                // 如果key不為空,找到對應的節點應該在的位置
                int h = k.threadLocalHashCode & (len - 1);
                if (h != i) {
                    // 如果和當前節點位置不同,
                    // 則清理節點並且迴圈找到後面的非空節點移到前面來
                    tab[i] = null;

                    while (tab[h] != null)
                        h = nextIndex(h, len);
                    tab[h] = e;
                }
            }
        }
        return i;
    }

cleanSomeSlots

    // 該方法用於清理空節點
    private boolean cleanSomeSlots(int i, int n) {
        // 標記是否有節點被清除
        boolean removed = false;
        Entry[] tab = table;
        int len = tab.length;
        do {
            i = nextIndex(i, len);
            Entry e = tab[i];
            // 如果有節點為空並且key為空
            // 該節點需要被清除
            if (e != null && e.get() == null) {
                // 重置n的值並且標記removed為true
                n = len;
                removed = true;
                // 清理該節點
                i = expungeStaleEntry(i);
            }
        } while ( (n >>>= 1) != 0);
        return removed;
    }

rehash

    // 當數組的元素到達臨界值,進行擴容
    private void rehash() {
        // 先對所有的節點進行清理
        expungeStaleEntries();

        // 然後判斷臨界值是否進行擴容
        // 此處由於先做過一次清理,這裡的數字可能會和之前的臨界值判斷有縮小
        // 所以此處臨界值判斷為threshold - threshold / 4
        // 即1/2的size時進行擴容
        if (size >= threshold - threshold / 4)
            resize();
    }

    private void resize() {
        // 獲取舊數組,開闢新數組
        // 新數組大小為舊數組的2倍
        Entry[] oldTab = table;
        int oldLen = oldTab.length;
        int newLen = oldLen * 2;
        Entry[] newTab = new Entry[newLen];
        int count = 0;

        // 遍歷舊數組
        for (int j = 0; j < oldLen; ++j) {
            Entry e = oldTab[j];
            if (e != null) {
                // 如果節點不為空,判斷key是否為空
                // 如果key為空,將節點置空幫助gc
                // 如果key不為空將舊數組的節點放入新數組
                // 放入方式和set實現一致,只是由於是剛創建的新數組
                // 不會有需要清理的數據,所以不需要額外清理
                ThreadLocal<?> k = e.get();
                if (k == null) {
                    e.value = null;
                } else {
                    int h = k.threadLocalHashCode & (newLen - 1);
                    while (newTab[h] != null)
                        h = nextIndex(h, newLen);
                    newTab[h] = e;
                    count++;
                }
            }
        }

        setThreshold(newLen);
        size = count;
        table = newTab;
    }

expungeStaleEntries

    // 清理所有節點
    private void expungeStaleEntries() {
        Entry[] tab = table;
        int len = tab.length;
        // 迴圈清理
        for (int j = 0; j < len; j++) {
            Entry e = tab[j];
            if (e != null && e.get() == null)
                expungeStaleEntry(j);
        }
    }

關於Map的清理

ThreadLocalMap實現採用的是開放定址法,它的實現本身應該是比較簡潔的,但是為了便於GC,內部節點採用了弱引用作為key,一旦數組中節點的強引用被設置為了null,節點的key就會被gc自動回收。這樣導致了ThreadLocalMap的實現變得異常的複雜。為了防止記憶體泄漏,在get和set方法的時候不得不進行額外的清理。

Q 為什麼需要清理?

A 不清理的話key被回收,但是value依舊會存在,並且難以被回收導致記憶體泄漏。

Q 為什麼清理的時候會涉及到節點的移動?

A 因為在開放定址法中,可能會有相同hash值的節點連續排在一起,當其中的一個或多個節點被回收後會造成同hash值的節點中間存在null節點,而我們get節點的時候會在碰到空節點的時候停止尋找,所以如果不進行一定的清理移動會導致部分節點永遠不會被查詢到。

ThreadLocal的實現

hashcode的實現

講完了ThreadLocalMap的實現原理,我們可以深深的體會到ThreadLocalhashcode是多麼的重要,如果hash值不能夠以合理的方式生成,導致數據的分佈不均勻,ThreadLocalMap的效率將會非常的低下。

hashcode的實現:

    private final int threadLocalHashCode = nextHashCode();

    private static AtomicInteger nextHashCode =
        new AtomicInteger();

    private static final int HASH_INCREMENT = 0x61c88647;

    private static int nextHashCode() {
        return nextHashCode.getAndAdd(HASH_INCREMENT);
    }

ThreadLocalhashcode實現代碼很簡短:每一個新的ThreadLocal的hash值都是在nextHashCode的基礎上增加0x61c88647。實現很簡單,但是很讓人迷惑。這個莫名其妙的魔數0x61c88647是什麼?

0x61c88647是有斐波那契構造而成的黃金比例數字,經過實驗測試,這個數字生成的hashcode可以很大程度的保證hash值能夠在數組中均勻的分佈。

get

    public T get() {
        // 獲取當前線程
        Thread t = Thread.currentThread();
        // 獲取當前線程的變數map
        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();
    }

set

    public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        // 如果map不為空,加入數據
        if (map != null)
            map.set(this, value);
        else
            // 否則新建map並放入第一個和數據
            createMap(t, value);
    }

總結

1、Thredlocal這個類可能對於很多人來說是一個常常會用到的類,但是未必所有人都會去關註他的內部實現,但是他的源碼是比較值得去閱讀的,一來它的實現代碼相對其他的常用類很短,只有幾百行;二來它的實現很經典,經典的開放定址法,經典的弱引用方便GC,可以說是很好的學習材料。這裡我雖然對於整個Thredlocal的源碼進行了完整的註釋解釋,但是它最值得細細品味的還是它的設計理念以及設計思路,這會對我們寫出優秀的代碼有著重要的作用。
2、註意:光理論是不夠的。在此免費贈送5大JAVA架構項目實戰教程及大廠面試題庫,有興趣的可以進裙 783802103獲取,沒基礎勿進哦!

本文的文字及圖片來源於網路加上自己的想法,僅供學習、交流使用,不具有任何商業用途,版權歸原作者所有,如有問題請及時聯繫我們以作處

最後

 


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 一、使用反射機制來 (1)獲取一個類; (2)獲取類的構造函數 (3)通過構造函數來獲取一個對象 package com.bjpowernode.java_learning; import java.lang.reflect.*; ​ public class D120_1_ConstructerO ...
  • 本教程源碼請訪問:tutorial_demo 一、AOP概述 1.1、概念 AOP:全稱是Aspect Oriented Programming,即:面向切麵編程。 通過預編譯方式和運行期間動態代理實現程式功能的統一維護的一種技術。AOP是OOP的延續,是軟體開發中的一個熱點,也是Spring框架中 ...
  • 本篇主要討論的是不同存儲結構(主要是LSM-tree和B-tree),它們應對的不同場景,所採用的底層存儲結構,以及對應用以提升效率的索引。 所謂資料庫,最基礎的功能,就是保存數據,並且在需要的時候可以方便地檢索到需要的數據。在這個基礎上,演化出了不同的資料庫系統,以及多種索引機制幫助檢索數據。這篇 ...
  • 雙向鏈表的實現 創建3個文件:doubleLinked.h、doubleLinked.c、doubleLinkedTest.c doubleLinked.h #ifndef DOUBLE_LINKED_H_ #define DOUBLE_LINKED_H_ #ifdef lxx_gnuc #defi ...
  • 本文源碼:GitHub·點這裡 || GitEE·點這裡 一、非同步處理 1、非同步概念 非同步處理不用阻塞當前線程來等待處理完成,而是允許後續操作,直至其它線程將處理完成,並回調通知此線程。 必須強調一個基礎邏輯,非同步是一種設計理念,非同步操作不等於多線程,MQ中間件,或者消息廣播,這些是可以實現非同步處理 ...
  • 前言 隨著現在直播的興起,主播這個職業逐漸走入人們的視野。越來越多有顏值、有才藝的人紛紛加入到主播這個行業。但是主播最難熬的就是前期粉絲的積累,粉絲多人氣就高。 接下來帶大家爬取主播直播間人氣數據,發現人氣王!! 加企鵝群695185429即可免費獲取,資料全在群文件里。資料可以領取包括不限於Pyt ...
  • 根據B站狂神學java的筆記 模板引擎Thymeleaf 前端想要顯示數據,我們以前是把頁面轉換成jsp。這樣我們就能夠實現數據的顯示,及交互等。 jsp支持非常強大的功能,包括能寫Java代碼 。但是我們使用SpringBoot項目是jar方式而不是war。我們還是使用嵌入式的Tomcat,但現在 ...
  • 《Java虛擬機規範》將虛擬機的記憶體分為以下幾個區域: 堆區:堆區是JVM中最大的一塊記憶體區域,按照垃圾分代收集的角度劃分,又可以分成年輕代和老年代,而年輕代記憶體又被分成三部分,Eden空間、From Survivor空間、To Survivor空間,預設情況下年輕代按照8:1:1的比例來分配; 方 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...