[Java源碼][并發J.U.C]---解析ThreadLocal

前言

本文將以一個例子開頭簡單看看ThreadLocal類的特性,進而分析該類的源代碼.

本文源碼下載

例子

啟動三個線程,每個線程的操作都是使用靜態變量count把原先的值加1.

package com.com.example.threadlocal;

import java.util.concurrent.TimeUnit;
public class TestThreadLocal {

    static ThreadLocal<Integer> count = new ThreadLocal<Integer>(){
        protected Integer initialValue() {
            return 100;
        }
    };

    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < 3; i++) {
            new Thread(new Runner(), "thread-" + i).start();
            TimeUnit.SECONDS.sleep(1);
        }
    }

    static class Runner implements Runnable {
        public void run() {
            for (int i = 0; i < 3; i++) {
                count.set(count.get() + 1);
                System.out.println(Thread.currentThread().getName() + ":" +
                        count.get());
            }
        }
    }
}

結果如下: 可以看到每個線程都有單獨的一個count實例一樣,這個就是threadlocal的特性可以使得線程之間隔離,相當于每個線程自己保存了一份自己的數據副本,在本線程中操作只會改變當前線程的值并不會影響其他線程的值.

thread-0:101
thread-0:102
thread-0:103
thread-1:101
thread-1:102
thread-1:103
thread-2:101
thread-2:102
thread-2:103

類圖

下面的圖是整個ThreadLocal涉及到的所有類. 接下來通過該圖理解一下整體的操作.

threadlocal.png

實現思路: 每個線程實體類中都保存著一個ThreadLocal.ThreadLocalMap用于存放該線程中所有的映射關系, 這個映射關系是由threadlocal類和初始化的value對應并放在ThreadLocalMap.Entry類中存放.
對上面的例子而言: thread-1.threadlocals存放了[count, 100]的映射關系, thread-2thread-3 各自的threadlocals都存放著[count, 100]的映射關系. 所以當thread-1運行run方法時循環了三次操作后thread-1.threadlocals存放了[count,103],而hread-2thread-3則保存不變.

1. 通過類ThreadLocal中的構造方法threadlocal()生成對象.
2. 通過initialValue初始化value值.
3. ThreadLocal類的set,get,remove操作都是調用的ThreadLocalMap的方法進行操作, 因為ThreadLocalMap定義了這些邏輯的核心實現, 那ThreadLocal類的方法做了什么呢? 主要是為了獲取當前線程的ThreadLocalMap對象, 即當前線程的成員變量threadlocals, 通過該變量進行真正的邏輯操作.

所以接下來我們將簡單看看ThreadLocal的方法, 重點分析的是ThreadLocalMap類.

ThreadLocal中的set, get, remove

set 方法的邏輯

// 獲取線程t的ThreadLocalMap對象
ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
}
// 根據當前的ThreadLocal對象和firstValue為線程t的成員變量threadlocas生成一個ThreadLocalMap對象
void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
}
/**
  *  1. 獲取當前線程
  *  2. 如果當前線程的ThreadLocalMap對象threadlocals已經存在,則直接調用ThreadLocalMap類的set方法
  *  3. 如果不存在,則創建一個ThreadLocalMap對象
  */
public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
}

get 方法的邏輯

// 設置初始化值并返回初始值
private T setInitialValue() {
        T value = initialValue();
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
        return value;
}
// 取當前線程的ThreadLocalMap對象
// 如果不存在或者不存在當前ThreadLocal對象不在ThreadLocalMap中,調用setInitialValue()返回初始值
// 反之則返回當前線程的ThreadLocalMap對象中當前ThreadLocal對象對應的value值
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();
}

remove方法的邏輯

// 獲取當前線程的ThreadLocalMap對象,如果不為空,則當前線程的ThreadLocalMap對象
//中的當前ThreadLocal對象所對應的節點.
public void remove() {
         ThreadLocalMap m = getMap(Thread.currentThread());
         if (m != null)
             m.remove(this);
}

ThreaLocalMap中的方法

ThreadLocalMap是整個ThreadLocal的核心部分. 由于我把ThreadLocalMap的源碼單獨拿了出來(源碼下載),接下來先由一個小例子簡單測試一下.

在測試之前需要先看一下ThreadLocalMap中的Entry類繼承了WeakReference類,請注意Entrykey也就是ThreadLocal對象是采用弱引用的方法,而value還是一個強引用. 關于弱引用可以關注我的另一個博客通過例子理解java強引用,軟引用,弱引用,虛引用

static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;

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

下面的例子是生成了9(10的時候數組會擴展)個ThreadLocal對象并且全部都set到了ThreadLocalMap對象中,然后打印一下整個數組中的情況. 因為每個tls[i]對應的對象現在都有兩個引用,1個強引用tls[i]和一個弱引用在某個entry[h]里面.所以我做了個簡單測試把tls[4]這個強引用去除,然后主動調用gc后再次打印觀察數組情況.

public static void test_2() {
        ThreadLocal<Integer> [] tls = new ThreadLocal[9];
        for (int i = 0; i < tls.length; i++) {
            tls[i] = new ThreadLocal<Integer>();
        }
        ThreadLocalMap map = new ThreadLocalMap(tls[0], 0);
        for (int i = 1; i < 9; i++) {
            System.out.print("i = " + i + ", hash = ");
            map.set(tls[i], i);
        }
        map.printEntry();
        tls[4] = null;
        System.gc();
        //map.set(tls[4], 4);
        System.out.println("---------------------------------");
        map.printEntry();
    }

輸出: 被垃圾回收器回收了

i = 0, hash = 0 & (16 - 1) = 0
i = 1, hash = 1640531527 & (16 - 1) = 7
i = 2, hash = -1013904242 & (16 - 1) = 14
i = 3, hash = 626627285 & (16 - 1) = 5
i = 4, hash = -2027808484 & (16 - 1) = 12
i = 5, hash = -387276957 & (16 - 1) = 3
i = 6, hash = 1253254570 & (16 - 1) = 10
i = 7, hash = -1401181199 & (16 - 1) = 1
i = 8, hash = 239350328 & (16 - 1) = 8
table[0] = [com.sourcecode.threadlocal.ThreadLocal@6f94fa3e,0]
table[1] = [com.sourcecode.threadlocal.ThreadLocal@5e481248,7]
table[2] = null
table[3] = [com.sourcecode.threadlocal.ThreadLocal@66d3c617,5]
table[4] = null
table[5] = [com.sourcecode.threadlocal.ThreadLocal@63947c6b,3]
table[6] = null
table[7] = [com.sourcecode.threadlocal.ThreadLocal@2b193f2d,1]
table[8] = [com.sourcecode.threadlocal.ThreadLocal@355da254,8]
table[9] = null
table[10] = [com.sourcecode.threadlocal.ThreadLocal@4dc63996,6]
table[11] = null
table[12] = [com.sourcecode.threadlocal.ThreadLocal@d716361,4]
table[13] = null
table[14] = [com.sourcecode.threadlocal.ThreadLocal@6ff3c5b5,2]
table[15] = null
---------------------------------
table[0] = [com.sourcecode.threadlocal.ThreadLocal@6f94fa3e,0]
table[1] = [com.sourcecode.threadlocal.ThreadLocal@5e481248,7]
table[2] = null
table[3] = [com.sourcecode.threadlocal.ThreadLocal@66d3c617,5]
table[4] = null
table[5] = [com.sourcecode.threadlocal.ThreadLocal@63947c6b,3]
table[6] = null
table[7] = [com.sourcecode.threadlocal.ThreadLocal@2b193f2d,1]
table[8] = [com.sourcecode.threadlocal.ThreadLocal@355da254,8]
table[9] = null
table[10] = [com.sourcecode.threadlocal.ThreadLocal@4dc63996,6]
table[11] = null
table[12] = [null,4]
table[13] = null
table[14] = [com.sourcecode.threadlocal.ThreadLocal@6ff3c5b5,2]
table[15] = null

ThreadLocalMap 與 HashMap處理沖突不一樣, HashMap采用的是拉鏈法,而ThreadLocal采用的開放地址法, 每個ThreadLocal對象都有一個threadLocalHashCode 通過 nextHashCode() 每生成一個ThreadLocal對象都在前面對象的threadLocalHashCode基礎上加一個常量HASH_INCREMENT = 0x61c88647, (從上面的例子中也可以看出來.)至于為什么?應該是hash沖突的比較少,具體為什么我也不太清楚.

插入或者更新操作 set(ThreadLocal<?> key, Object value)

/**
         * 作用: 將key和value 插入(如果key不存在)或者更新(如果key存在)
         * @param key    鍵
         * @param value  值
         */
        private void set(ThreadLocal<?> key, Object value) {

            Entry[] tab = table;
            int len = tab.length;
            int i = key.threadLocalHashCode & (len-1);
            //System.out.format("%d & (%d - 1) = %d\n", key.threadLocalHashCode, len, i);
            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;
                }
                /**
                 * 如果當前k過期,則調用replaceStaleEntry方法
                 * 無論key是否存在,都會保存在位置i,具體細節可以看replaceStaleEntry的注釋
                 */
                
                if (k == null) {
                    replaceStaleEntry(key, value, i);
                    return;
                }
            }

            tab[i] = new Entry(key, value);
            int sz = ++size;
            /**
             *  有限次去查找過期節點并刪除過期節點,如果有刪除則返回
             *  如果沒有刪除則判斷是否超過閥值
             *  如果超過閥值則調用rehash函數.
             */
            if (!cleanSomeSlots(i, sz) && sz >= threshold)
                rehash();
        }

作用: 將key和value 插入(如果key不存在)或者更新(如果key存在)

對應流程圖如下
set.png

expungeStaleEntry(int staleSlot)

/**
         *
         * 作用: 從該索引staleSlot往下直到遇到null結束返回當前下標,遇到的過期元素tab[i]設置為null,遇到的正常節點做rehash.
         * @param staleSlot 需要清理的位置, 一個已經確定過期的位置
         * @return 返回從staleSlot位置開始第一個為entry值為null的位置
         */
        private int expungeStaleEntry(int staleSlot) {
            Entry[] tab = table;
            int len = tab.length;

            /**
             *  清除該staleSlot的值
             */
            tab[staleSlot].value = null;
            tab[staleSlot] = null;
            size--;

            /**
             *  從stateSlot 開始往下繼續搜索
             *  1. 如果為null, 直接退出
             *  2. 如果虛引用對應的key已經為null,也就是被垃圾回收器回收了,則清除該位置
             *  3. 如果不是1或者2,表明該位置存著一個正常值,觀察是否需要rehash,因為取值的時候會方便
             *     因為該類處理hash沖突使用的是:開放定址法
             */
            Entry e;
            int i;
            for (i = nextIndex(staleSlot, len);
                 (e = tab[i]) != null;
                 i = nextIndex(i, len)) {
                ThreadLocal<?> k = e.get();
                if (k == null) {
                    e.value = null;
                    tab[i] = null;
                    size--;
                } else {
                    /**
                     *  因為處理沖突使用的開放地址法, 現在已經刪除了一個位置,
                     *  并且該節點前面的節點有可能為null,因為k==null的時候會把tab[i]=null,
                     *  所以比如下次set操作對該key進行操作的時候就找不到該key,因為前面有null值,
                     *  會認為該key不存在,重新創建一個新的節點,因此會造成有兩個節點擁有同一個key.
                     *
                     *  所以需要進行rehash
                     *
                     *  因此之前有些位置因為沖突沒有存放到對應的hash值該有的位置,
                     *  所以下面的方法就是檢查并且把此對象存到對應的hash值的位置或者它的后面.
                     */
                    int h = k.threadLocalHashCode & (len - 1);
                    if (h != i) {
                        tab[i] = null;

                        // 往下繼續尋找,值到找到為null的空位置,然后把只放進去
                        while (tab[h] != null)
                            h = nextIndex(h, len);
                        tab[h] = e;
                    }
                }
            }
            return i;
        }

作用: 從該索引往下直到遇到null結束返回當前下標,遇到的過期元素tab[i]設置為null,遇到的正常節點做rehash.

為什么需要做rehash?

因為處理沖突使用的開放地址法, 現在已經刪除了一個位置, 并且該節點前面的節點有可能為null,因為k==null的時候(表明該節點已經過期)會把tab[i]=null, 所以比如下次set操作對該key進行操作的時候就找不到該key,因為前面有null值, 會認為該key不存在,然后重新創建一個新的節點,因此會造成有兩個節點擁有同一個key. 所以需要進行rehash.

對應流程圖如下
expungeStaleEntry.png

cleanSomeSlots

/**
         *
         * @param i 從該位置i的下一個位置開始
         * @param n n >>>= 1決定嘗試的次數
         * @return 返回是否有清除過陳舊的值
         */
        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];
                // 如果當前節點不為null,并且對應的key已經被垃圾回收器收集
                if (e != null && e.get() == null) {
                    // 重新設置n 和 設置removed標志位為true
                    n = len;
                    removed = true;
                    // 清除陳舊的位置節點i, 并設置i為當前i下一個位置開始第一個為entry值為null的位置
                    i = expungeStaleEntry(i);
                }
            } while ( (n >>>= 1) != 0);
            return removed;
        }

作用:
1. 盡可能多的刪除過期的節點.
2. 檢查次數由n決定. 為logn或者n.
3. 返回是否有刪除過期元素

對應流程圖如下
cleanSomeSlots.png

replaceStaleEntry

/**
         *
         * 將set操作期間遇到的過期節點替換為指定鍵的節點。
         * 無論指定鍵的節點是否已存在,value參數中傳遞的值都存儲在節點中。
         * 作為副作用,此方法將清除包含過期節點的“run”中的所有過期節點。 (run是兩個空槽之間的一系列節點。)
         *
         *
         * @param key         節點的鍵
         * @param value       節點的值
         * @param staleSlot   在尋找key過程中遇到的第一個過期的節點
         */
        private void replaceStaleEntry(ThreadLocal<?> key, Object value,
                                       int staleSlot) {
            Entry[] tab = table;
            int len = tab.length;
            Entry e;

            /**
             * 備份以檢查當前"run"中的先前失效節點。
             * 我們一次清理整個"run",以避免由于垃圾收集器釋放串聯的refs(即,每當收集器運行時)不斷的增量重復。
             * slotToExpung 始終代表著整個run里面的第一個過期節點.
             */
            int slotToExpunge = staleSlot;
            for (int i = prevIndex(staleSlot, len);
                 (e = tab[i]) != null;
                 i = prevIndex(i, len))
                if (e.get() == null)
                    slotToExpunge = i;

            /**
             *   尋找"run"中的key 或者第一個空節點(null)
             *   1. 找到key的位置i,就交換tab[i]和tab[staleSlot],提高查找時候的命中率
             *   2. 如果找到一個空節點,就表示該key之前沒有插入到該tab中過,跳出循環后創建一個新的節點(key,value)
              */

            for (int i = nextIndex(staleSlot, len);
                 (e = tab[i]) != null;
                 i = nextIndex(i, len)) {
                ThreadLocal<?> k = e.get();

                /**
                 * 如果我們找到鍵,那么我們需要將它與陳舊條目交換以維護哈希表順序。
                 * 新陳舊的插槽或任何其他過期的插槽,在它上面遇到,然后可以發送到expungeStaleEntry
                 * 刪除或重新運行run中的所有其他條目。
                 *
                 */
                if (k == key) {
                    e.value = value; // 替換value

                    tab[i] = tab[staleSlot];  // 交換
                    tab[staleSlot] = e;

                    /**
                     * 如果slotToExpunge == staleSlot
                     * 表明當前的i是整個run里面的第一個過期的元素節點,更新一下slotToExpunge即可.
                     */
                    if (slotToExpunge == staleSlot)
                        slotToExpunge = i;
                    cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
                    return;
                }

                /**
                 *  如果在反向掃描中找不到過期的節點, 那么在掃描key是看到的
                 *  第一個過期節點就是整個run里面的過期節點
                 */
                if (k == null && slotToExpunge == staleSlot)
                    slotToExpunge = i;
            }

            /**
             *  如果key沒有找到,表明該key是第一次存入到該table中,
             *  則生成一個新的節點并放到staleSlot的位置.
             */
            tab[staleSlot].value = null;
            tab[staleSlot] = new Entry(key, value);

            /**
             * 如果staleSlot不是該run里面的唯一一個過期節點,
             * 則都需要進行清除工作
             */
            if (slotToExpunge != staleSlot)
                cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
        }

作用: 替代原有key的節點或者新生成節點后做清除過期節點操作.
1. 定義了slotToExpunge為整個run(run是兩個空槽之間的一系列節點.)里面的第一個過期節點.
2. 查找該key是否之前有存入過. 如果存在則存入到位置staleSlot并清除從slotToExpunge開始該run里面的過期元素.
3. 如果不存在該key則創建一個新節點并放到位置staleSlot,如果staleSlot是整個run里面的唯一一個過期節點,則不需要清除,否則需要清除從slotToExpunge開始該run里面的過期元素.

詳細操作可以看代碼注釋和下面的流程圖.

對應流程圖如下
replaceStaleEntry.png

rehash, resize, expungeStaleEntries

/**
         * 作用:
         * 1. 先對整個數組的過期節點進行清除
         * 2. 判斷是否需要對數組進行擴展
         */
        private void rehash() {
            /**
             * 先對整個數組的過期節點進行清除
             */
            expungeStaleEntries();

            /**
             *  size >= 0.75 * threshold 則擴大容量
             */

            if (size >= threshold - threshold / 4)
                resize();
        }

        /**
         *  作用: 擴展數組
         *  size擴大兩倍, 每一個正常的元素做rehash映射到新的數組中
         *  每一個過期的元素的value都設置為null方便gc
         */
        private void resize() {
            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) {
                    ThreadLocal<?> k = e.get();
                    if (k == null) {
                        e.value = null; // Help the GC
                    } 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;
        }

        /**
         *  作用: 從頭到尾掃描整個數組對所有過期節點做清理工作
         */
        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);
            }
        }

取值操作 getEntry, getEntryAfterMiss

/**
         * 作用:  先用hash定位尋找key,如果找到key 返回該節點
         *       如果沒有找到key 返回getEntryAfterMiss(key, i, e)的結果
         */
        private Entry getEntry(ThreadLocal<?> key) {
            int i = key.threadLocalHashCode & (table.length - 1); //計算hash值
            Entry e = table[i];
            if (e != null && e.get() == key) // 如果命中
                return e;
            else
                return getEntryAfterMiss(key, i, e);
        }

        /**
         * 作用:  利用開發地址法尋找key,如果找到key 返回該節點
         *       如果沒有找到key 返回null
         */
        private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
            Entry[] tab = table;
            int len = tab.length;

            while (e != null) {
                ThreadLocal<?> k = e.get();
                if (k == key) //找到key存在的位置,直接返回節點
                    return e;
                /**
                 *  如果當前節點的key過期,則調用expungeStaleEntry(i)進行清理當前位置
                 *  并且不接受返回值,i 沒有發生變化
                 *
                 *  如果不過期則取下一個節點
                 */
                if (k == null)
                    expungeStaleEntry(i);
                else
                    i = nextIndex(i, len);
                e = tab[i];
            }
            return null;
        }

作用: 根據key獲得對應的節點,如果不存在則返回null.

對應流程圖如下
getEntry.png

刪除操作 remove

        /**
         *
         * 作用: 刪除key,調用了expungeStaleEntry(i)做清除和rehash工作,
         *
         * @param key 要刪除的鍵值
         */
        private void remove(ThreadLocal<?> key) {
            Entry[] tab = table;
            int len = tab.length;
            int i = key.threadLocalHashCode & (len-1); //獲取hash值, 如果不在該位置則繼續往下找直到遇到null
            for (Entry e = tab[i];
                 e != null;
                 e = tab[i = nextIndex(i, len)]) {
                if (e.get() == key) {
                    e.clear();
                    expungeStaleEntry(i); //做清除工作
                    return;
                }
            }
        }

作用: 刪除該key所對應的節點.

為什么需要調用expungeStaleEntry(i)?

這是因為在清除了位置i后, 整個run(run是兩個空槽之間的一系列節點.)該位置會變為null,因此位置i后面的節點需要做rehash, 這是因為該數組處理hash沖突采用的是開放地址法,因此后面的節點的hash值有可能不在它本身所處的位置, 如果后面的某一個節點K本身的hash值在i前面(比如i-1,還是在整個run里面), 那么后續操作在對節點K更新或者獲取操作時就會找不到節點K, 因為取hash值是i-1,檢查到i時發現已經為null,所以會認為該節點K不存在. 所以可以看到整個ThreadLocalMap對過期元素做刪除操作都是調用expungeStaleEntry(i)方法.

關于為什么使用弱引用和內存泄露的問題?

可以參考該文章: 深入分析 ThreadLocal 內存泄漏問題
深入分析 ThreadLocal 內存泄漏問題

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • 前言 ThreadLocal很多同學都搞不懂是什么東西,可以用來干嘛。但面試時卻又經常問到,所以這次我和大家一起學...
    liangzzz閱讀 12,488評論 14 228
  • 一、使用姿勢 二、數據結構 三、源碼分析 四、回收機制 總結 一、使用姿勢 最佳實踐 在類中定義ThreadLoc...
    原水寒閱讀 1,637評論 2 8
  • 本文是我自己在秋招復習時的讀書筆記,整理的知識點,也是為了防止忘記,尊重勞動成果,轉載注明出處哦!如果你也喜歡,那...
    波波波先森閱讀 11,307評論 4 56
  • 下面我就以面試問答的形式學習我們的——ThreadLocal類(源碼分析基于JDK8) 問答內容 1、問:Thre...
    Sophia_dd35閱讀 2,089評論 1 36
  • 記錄一下,現在下載 jdk7 地址有點難找http://www.oracle.com/technetwork/ja...
    candyleer閱讀 3,191評論 0 49