線程本地變量(ThreadLocal)

一、前言

之前看 Android Looper 源碼時,在看到 Looper.prepare 時廓八,有個靜態(tài)成員變量:sThreadLocal:

private static void prepare(boolean quitAllowed) {
    if (sThreadLocal.get() != null) {
        throw new RuntimeException("Only one Looper may be created per thread");
    }
    sThreadLocal.set(new Looper(quitAllowed));
}

我們看到京景,先 get 窿冯,沒有就會去 set。

然后就很好奇的去看了下 ThreadLocal 源碼确徙,看源碼的過程中醒串,JDK又給了我一個驚喜:

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

發(fā)現了一個很奇怪的常數执桌,但不知道這個常數是怎么得來的,故在網上查找了相關資料芜赌,總算明白這個常數的含義仰挣,以及 ThreadLocal 的作用。

二缠沈、ThreadLocal

先來說說這個 ThreadLocal 是什么吧膘壶?
ThreadLocal 是線程的內部存儲類,可以在指定的線程內洲愤,存儲數據颓芭,同時,也只有指定的線程可以訪問該數據柬赐;不同的數程是無法訪問其它線程的這個變量的亡问,因此,也就做到了線程的數據隔離肛宋。

This class provides thread-local variables.  These variables differ from their normal counterparts in 
that each thread that accesses one (via its {@code get} or {@code set} method) has its own, independently 
initialized copy of the variable.  {@code ThreadLocal} instances are typically private static fields in 
classes that wish to associate state with a thread (e.g., a user ID or Transaction ID).

官方注釋:ThreadLocal 提供了線程本地變量州藕,和其它的變量不同,只有每個線程自己可以 get / set悼吱,每個變量(副本)是獨立的慎框。

2.1、ThreadLocal.set

public class ThreadLocal<T> {
    public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value); // map的key = ThreadLocal, value 是要存儲的對象
        else
            createMap(t, value); // 沒有就為這個 Thread 創(chuàng)建 ThreadLocalMap 并賦值
    }
    
    ThreadLocalMap getMap(Thread t) {
        // Thread 中有個 threadLocals變量
        // 類型:ThreadLocal.ThreadLocalMap
        return t.threadLocals;
    }
    
    void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }
}

2.2后添、ThreadLocal.get

public class ThreadLocal<T> {
    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(); // 初始值為 null
    }
}

2.3笨枯、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;
        }
    }

    // 初始容量,之后擴容是 2的N次方擴容
    private static final int INITIAL_CAPACITY = 16;
    // 存儲 key & value
    private Entry[] table;
    // 已使用的個數
    private int size = 0;
    // 當前 table.length * 2 / 3遇西,即超過三分之二就開始擴容馅精,和 HashMap的負載因子 0.75 不一樣
    private int threshold;
}

\color{red}{看到網上有說到,內存泄露:}
因為 ThreadLocalMap 的生命周期與 Thread 一樣長(Thread 持有 threadLocals)粱檀,當你的 key( ThreadLocal )為 null 時洲敢,value 仍被 table 持有,因此茄蚯,需要主動調用 ThreadLocal 的 remove 方法來清除內存压彭。

三、魔數:0x61c88647

我們在一進入 ThreadLocal 類時渗常,就能看到一段代碼:

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(); // 1640531527

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

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

上面的代碼注釋中也解釋到:HASH_INCREMENT 是為了讓哈希碼能均勻的分布在2的N次方的數組里壮不。
每個 ThreadLocal 都使用該散列值!

散列是什么皱碘?

散列(Hash)也稱為哈希询一,就是把任意長度的輸入,通過散列算法,變換成固定長度的輸出健蕊,這個輸出值就是散列值菱阵。
在實際使用中,不同的輸入可能會散列成相同的輸出缩功,這時也就產生了沖突晴及。
通過上文提到的 HASH_INCREMENT 再借助一定的算法,就可以將哈希碼能均勻的分布在 2 的 N 次方的數組里掂之,保證了散列表的離散度抗俄,從而降低了沖突幾率。

散列算法

  • 取模法

散列長度 m, 對于一個小于 m 的數 p 取模世舰,所得結果為散列地址动雹。對 p 的選擇很重要,一般取素數或 m
公式:f(k) = k % p (p<=m)
因為求模數其實是通過一個除法運算得到的跟压,所以叫“取模法”

  • 平方散列法(平方取中法)

先通過求關鍵字的平方值擴大相近數的差別胰蝠,然后根據表長度取中間的幾位數作為散列函數值。
又因為一個乘積的中間幾位數和乘數的每一位都相關震蒋,所以由此產生的散列地址較為均勻茸塞。
公式:f(k) = ((k * k) >> X) << Y
對于常見的32位整數而言,也就是 f(k) = (k * k) >> 28

  • 斐波那契(Fibonacci)散列法

和平方散列法類似查剖,此種方法使用斐波那契數列的值作為乘數而不是自己钾虐。
對于 16 位整數而言,這個乘數是 40503笋庄。
對于 32 位整數而言效扫,這個乘數是 2654435769。
對于 64 位整數而言直砂,這個乘數是 11400714819323198485菌仁。

公式:f(k) = ((k * 2654435769) >> X) << Y

對于常見的32位整數而言,也就是 f(k) = (k * 2654435769) >> 28
這時我們可以隱隱感覺到 0x61c88647 與斐波那契數列有些關系静暂。

  • 隨機數法

選擇一隨機函數济丘,取關鍵字的隨機值作為散列地址,通常用于關鍵字長度不同的場合洽蛀。
公式:f(k) = random(k)

  • 鏈地址法(拉鏈法)

懂了散列算法摹迷,我們再來了解下拉鏈法。拉鏈法是為了 HashMap 中降低沖突郊供,除了拉鏈法峡碉,還可以使用開放尋址法、再散列法颂碘、鏈地址法异赫、公共溢出區(qū)等方法。這里就只簡單介紹了拉鏈法头岔。

把具有相同散列地址的關鍵字(同義詞)值放在同一個單鏈表中塔拳,稱為同義詞鏈表。有 m 個散列地址就有 m 個鏈表峡竣,同時用指針數組 T[0..m-1] 存放各個鏈表的頭指針靠抑,凡是散列地址為 i 的記錄都以結點方式插入到以 T[i] 為指針的單鏈表中。T 中各分量的初值應為空指針适掰。

\color{red}{對于HashMap}

hashmap.png

\color{red}{對于取模法( k = 16 }

mode.png

\color{red}{對于斐波那契散列法}

fac.png

可以看出用斐波那契散列法調整之后會比原來的除法散列離散度好很多颂碧。

再來說說,魔數怎么來的类浪?

public class HashTest {
    public static void main(String[] args) {
        long lg = (long) ((1L << 32) * (Math.sqrt(5) - 1)/2);
        System.out.println("as 32 bit unsigned: " + lg);
        int i = (int) lg;
        System.out.println("as 32 bit signed:   " + i);
        System.out.println("MAGIC = " + 0x61c88647);
    }
}

// as 32 bit unsigned: 2654435769
// as 32 bit signed:   -1640531527
// MAGIC = 1640531527

可以發(fā)現 0x61c88647 與一個神奇的數字產生了關系载城,它就是 (Math.sqrt(5) - 1)/2。
也就是傳說中的黃金比例 0.618(0.618 只是一個粗略值)费就,即 0x61c88647 = 2^32 * 黃金分割比诉瓦。同時也對應上了上文所提到的斐波那契散列法。

四力细、ThreadLocal 應用場景

ThreadLocal的使用場景:

  • 某些對象在多線程并發(fā)訪問時可能出現問題睬澡,比如使用SimpleDataFormat的parse()方法,內部有一個Calendar對象眠蚂,調用SimpleDataFormat的parse()方法會先調用Calendar.clear()煞聪,然后調用Calendar.add(),如果一個線程先調用了add()然后另一個線程又調用了clear()逝慧,這時候parse()方法解析的時間就不對了昔脯,我們就可以用ThreadLocal<SimpleDataFormat>來解決并發(fā)修改的問題;
  • 另一種場景是Spring事務馋艺,事務是和線程綁定起來的栅干,Spring框架在事務開始時會給當前線程綁定一個Jdbc Connection,在整個事務過程都是使用該線程綁定的connection來執(zhí)行數據庫操作捐祠,實現了事務的隔離性碱鳞。Spring框架里面就是用的ThreadLocal來實現這種隔離:
public abstract class TransactionSynchronizationManager {
        //線程綁定的資源,比如DataSourceTransactionManager綁定是的某個數據源的一個Connection,在整個事務執(zhí)行過程中
        //都使用同一個Jdbc Connection
        private static final ThreadLocal<Map<Object, Object>> resources =
                new NamedThreadLocal<>("Transactional resources");

        //事務注冊的事務同步器
        private static final ThreadLocal<Set<TransactionSynchronization>> synchronizations =
                new NamedThreadLocal<>("Transaction synchronizations");

    //事務名稱
    private static final ThreadLocal<String> currentTransactionName =
            new NamedThreadLocal<>("Current transaction name");

    //事務只讀屬性
    private static final ThreadLocal<Boolean> currentTransactionReadOnly =
            new NamedThreadLocal<>("Current transaction read-only status");

    //事務隔離級別
    private static final ThreadLocal<Integer> currentTransactionIsolationLevel =
            new NamedThreadLocal<>("Current transaction isolation level");
            
    //事務同步開啟
    private static final ThreadLocal<Boolean> actualTransactionActive =
            new NamedThreadLocal<>("Actual transaction active");
    }
}

但是不是說一遇到并發(fā)場景就用ThreadLocal來解決,我們還可以用synchronized或者鎖來實現線程安全。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末踱蛀,一起剝皮案震驚了整個濱河市窿给,隨后出現的幾起案子,更是在濱河造成了極大的恐慌率拒,老刑警劉巖崩泡,帶你破解...
    沈念sama閱讀 222,378評論 6 516
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現場離奇詭異猬膨,居然都是意外死亡角撞,警方通過查閱死者的電腦和手機,發(fā)現死者居然都...
    沈念sama閱讀 94,970評論 3 399
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來谒所,“玉大人热康,你說我怎么就攤上這事×恿欤” “怎么了姐军?”我有些...
    開封第一講書人閱讀 168,983評論 0 362
  • 文/不壞的土叔 我叫張陵,是天一觀的道長尖淘。 經常有香客問我奕锌,道長,這世上最難降的妖魔是什么村生? 我笑而不...
    開封第一講書人閱讀 59,938評論 1 299
  • 正文 為了忘掉前任惊暴,我火速辦了婚禮,結果婚禮上趁桃,老公的妹妹穿的比我還像新娘缴守。我一直安慰自己,他們只是感情好镇辉,可當我...
    茶點故事閱讀 68,955評論 6 398
  • 文/花漫 我一把揭開白布屡穗。 她就那樣靜靜地躺著,像睡著了一般忽肛。 火紅的嫁衣襯著肌膚如雪村砂。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,549評論 1 312
  • 那天屹逛,我揣著相機與錄音础废,去河邊找鬼。 笑死罕模,一個胖子當著我的面吹牛评腺,可吹牛的內容都是我干的。 我是一名探鬼主播淑掌,決...
    沈念sama閱讀 41,063評論 3 422
  • 文/蒼蘭香墨 我猛地睜開眼蒿讥,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了抛腕?” 一聲冷哼從身側響起芋绸,我...
    開封第一講書人閱讀 39,991評論 0 277
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎担敌,沒想到半個月后摔敛,有當地人在樹林里發(fā)現了一具尸體,經...
    沈念sama閱讀 46,522評論 1 319
  • 正文 獨居荒郊野嶺守林人離奇死亡全封,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 38,604評論 3 342
  • 正文 我和宋清朗相戀三年马昙,在試婚紗的時候發(fā)現自己被綠了桃犬。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,742評論 1 353
  • 序言:一個原本活蹦亂跳的男人離奇死亡行楞,死狀恐怖疫萤,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情敢伸,我是刑警寧澤,帶...
    沈念sama閱讀 36,413評論 5 351
  • 正文 年R本政府宣布恒削,位于F島的核電站池颈,受9級特大地震影響,放射性物質發(fā)生泄漏钓丰。R本人自食惡果不足惜躯砰,卻給世界環(huán)境...
    茶點故事閱讀 42,094評論 3 335
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望携丁。 院中可真熱鬧琢歇,春花似錦、人聲如沸梦鉴。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,572評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽肥橙。三九已至魄宏,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間存筏,已是汗流浹背宠互。 一陣腳步聲響...
    開封第一講書人閱讀 33,671評論 1 274
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留椭坚,地道東北人予跌。 一個月前我還...
    沈念sama閱讀 49,159評論 3 378
  • 正文 我出身青樓,卻偏偏與公主長得像善茎,于是被迫代替她去往敵國和親券册。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 45,747評論 2 361