天天看點

ThreadLocal源碼閱讀一.概述二.内部類分析三.方法分析

文章目錄

  • 一.概述
  • 二.内部類分析
    • 1.ThreadLocalMap
      • A.内部類和參數分析
        • 1.内部類Entry
        • 2.table:Entry數組
        • 3.size
        • 4.threshold
        • 5.INITIAL_CAPACITY
      • B.方法分析
        • 1.連續段清除方法:expungeStaleEntry
        • 2.掃描控制清除操作:cleanSomeSlots
        • 3.換值操作:replaceStaleEntry
        • 4.getEntry
        • 5.set
        • 6.remove
  • 三.方法分析
    • 1.get
    • 2.set

一.概述

ThreadLocal能夠保證線程的隔離,不同的線程取到對應的參數,那麼這邊是怎麼做到的呢,我們來看一下源碼

二.内部類分析

1.ThreadLocalMap

      我們可以看到整個代碼結構,包括ThreadLocal的方法基本都是調用這個資料結構,是以我們首要的目标就是分析這個資料結構

ThreadLocal源碼閱讀一.概述二.内部類分析三.方法分析

A.内部類和參數分析

1.内部類Entry

Entry繼承WeakReference,使用弱引用,可以将ThreadLocal對象的生命周期和線程生命周期解綁,持有對ThreadLocal的弱引用,可以使得ThreadLocal在沒有其他強引用的時候被回收掉,這樣可以避免因為線程得不到銷毀導緻ThreadLocal對象無法被回收。

/**
         * Entry繼承WeakReference,并且用ThreadLocal作為key.如果key為null
         * (entry.get() == null)表示key不再被引用,表示ThreadLocal對象被回收
         * 是以這時候entry也可以從table從清除。
         **/
        static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            //Entry的value這個參數就是TreadLocal存儲的元素
            Object value;

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

2.table:Entry數組

/**
 * The table, resized as necessary.
 * table.length MUST always be a power of two.
 * 類似HashMap的結構,這個是用來存儲Entry數組.
 */
private Entry[] table;
           

3.size

/**
  * The number of entries in the table.
  * 在table中存在多少個數組
  */
 private int size = 0;
           

4.threshold

/**
 * The next size value at which to resize.
 * 進行擴容的門檻值,表使用量大于它的時候進行擴容。
 */
private int threshold;

//定義為表長度的2/3
private void setThreshold(int len) {
     threshold = len * 2 / 3;
 }
           

5.INITIAL_CAPACITY

/**
 * The initial capacity -- MUST be a power of two.
 * 預設初始化的table大小
 */
private static final int INITIAL_CAPACITY = 16;
           

B.方法分析

1.連續段清除方法:expungeStaleEntry

private int expungeStaleEntry(int staleSlot) {
            Entry[] tab = table;
            int len = tab.length;

            //1.先把傳進來的節點值置為null,使用的size-1
            tab[staleSlot].value = null;
            tab[staleSlot] = null;
            size--;

            Entry e;
            int i;
            //2.往後掃描一段不為空的Entry.遇到空Entry退出
            for (i = nextIndex(staleSlot, len); (e = tab[i]) != null; i = nextIndex(i, len)) {
                ThreadLocal<?> k = e.get();
                //3.如果目前key為空,則進行置空操作
                if (k == null) {
                    e.value = null;
                    tab[i] = null;
                    size--;
                } else {
                    //4.如果key不為空,計算索引.進行rehash的操作
                    int h = k.threadLocalHashCode & (len - 1);
                    if (h != i) {
                        /**
                         * 這邊的rehash操作還真是不知道為什麼這麼做
                         */
                        tab[i] = null;
                        while (tab[h] != null)
                            h = nextIndex(h, len);
                        tab[h] = e;
                    }
                }
            }
            //5.傳回下一個為空的索引
            return i;
        }
           

操作步驟如下:

ThreadLocal源碼閱讀一.概述二.内部類分析三.方法分析

2.掃描控制清除操作:cleanSomeSlots

/**
         * 這個方法是用來做掃描控制清除操作,也就是對TreadLocalMap裡面key為空的元素進行全部清除
         * @param i 起始位置
         * @param n 掃描次數
         * @return 是否進行了清除操作
         */
        private boolean cleanSomeSlots(int i, int n) {
            boolean removed = false;
            Entry[] tab = table;
            int len = tab.length;
            do {
                //1.目前下标向後掃描,線性探測法,當超過最大下标時傳回
                i = nextIndex(i, len);
                Entry e = tab[i];
                //2.如果遇到這種value不被使用了的Entry
                if (e != null && e.get() == null) {
                    //3.擴大掃描範圍
                    n = len;
                    removed = true;
                    //4.這邊會進行目前Entry的清空.然後繼續往後掃描清除,并傳回第一個為空的下标位置.
                    i = expungeStaleEntry(i);
                }
            } while ((n >>>= 1) != 0);
            //5.直到循環結束,傳回是否清除的結果
            return removed;
        }
           
ThreadLocal源碼閱讀一.概述二.内部類分析三.方法分析

3.換值操作:replaceStaleEntry

這個方法乍一看上去非常惡心,但是如果你把它分成三塊來看, 就沒那麼惡心了,因為他的目的就是把對應位置的key-value換掉,隻是在這個過程中還加入了了掃描清除的操作.

我們按照每一部分來看這個方法,就簡單了,下面這個圖:

ThreadLocal源碼閱讀一.概述二.内部類分析三.方法分析

其實這邊就分成了四種大情況,3的位置就是設值的操作,隻要2部分不進入2.2,就一定會達到設值的目的。

那麼這邊除了設定,還有一個重要的操作就是髒Entry的清除操作.1是向前找髒Entry,2是向後找髒Entry.

這麼說的話就是找到沒找到,四種情況,我們一一來分析一下.

  • 向前找向後找都沒找到髒Entry
    ThreadLocal源碼閱讀一.概述二.内部類分析三.方法分析
  • 向前找到了髒Entry,向後沒找到
    ThreadLocal源碼閱讀一.概述二.内部類分析三.方法分析
  • 向前沒找到,向後找到了髒Entry
    ThreadLocal源碼閱讀一.概述二.内部類分析三.方法分析
  • 向前向後都找到了髒Entry
    ThreadLocal源碼閱讀一.概述二.内部類分析三.方法分析
/**
         *
         * @param key 要儲存的key
         * @param value 要儲存的value
         * @param staleSlot 目前位置(這邊傳入的是目前key=null的位置下标)
         */
        private void replaceStaleEntry(ThreadLocal<?> key, Object value,
                                       int staleSlot) {
            Entry[] tab = table;
            int len = tab.length;
            Entry e;


            int slotToExpunge = staleSlot;
            //1.從目前位置向前搜尋到key=null的entry,并且在空Entry處結束,得到key=null的Entry下标
            for (int i = prevIndex(staleSlot, len); (e = tab[i]) != null; i = prevIndex(i, len))
                if (e.get() == null)
                    slotToExpunge = i;

            //2.向後搜尋,直到空Entry結束
            for (int i = nextIndex(staleSlot, len); (e = tab[i]) != null; i = nextIndex(i, len)) {
                ThreadLocal<?> k = e.get();

                //3.如果找到key一樣的Entry.
                if (k == key) {
                    //4.進行值的覆寫
                    e.value = value;

                    tab[i] = tab[staleSlot];
                    tab[staleSlot] = e;

                    if (slotToExpunge == staleSlot)
                        slotToExpunge = i;
                    //5.從目前位置開始進行指定次數的清除操作
                    cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
                    return;
                }

                if (k == null && slotToExpunge == staleSlot)
                    slotToExpunge = i;
            }

            //6.如果在查找過程中沒有找到可以覆寫的entry,則将新的entry插入在髒entry
            tab[staleSlot].value = null;
            tab[staleSlot] = new Entry(key, value);

            if (slotToExpunge != staleSlot)
                cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
        }
           

4.getEntry

getEntry這個方法通過算法得到下标之後,判斷下标對應本地變量參數是否存在,存在則傳回,否則繼續,操作步驟:

ThreadLocal源碼閱讀一.概述二.内部類分析三.方法分析
private Entry getEntry(ThreadLocal<?> key) {
            //1.一種算法稱為完美散列
            int i = key.threadLocalHashCode & (table.length - 1);
            //2.拿到Entry,不為空且key和拿到的元素相同,傳回Entry
            Entry e = table[i];
            if (e != null && e.get() == key)
                return e;
            //3.如果不滿足上述條件則進入這邊
            else
                return getEntryAfterMiss(key, i, e);
        }


        private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
            Entry[] tab = table;
            int len = tab.length;

            //1.當Entry不為空時,循環周遊
            while (e != null) {
                ThreadLocal<?> k = e.get();
                if (k == key)
                    return e;
                if (k == null)
                    //2.當key弱應用已經為空時,這邊要進行清除操作,并且這個方法會繼續往後掃描清除,知道null元素傳回
                    expungeStaleEntry(i);
                else
                    i = nextIndex(i, len);
                e = tab[i];
            }
            //3.沒有結果則傳回null
            return null;
        }
           

5.set

ThreadLocal源碼閱讀一.概述二.内部類分析三.方法分析

set的方法這邊有這樣幾步:

  • 向後探測,遇到key相等的,修改key對應的值,return
  • 向後探測,遇到key為空的髒Entry,調用替換該位置的值的方法,return
  • 向後探測沒有遇到上述兩種情況,在目前位置插入這個Entry
  • 控制清除方法遇到髒Entry進行清除并且長度超過,則rehash這個TreadLocalMap

6.remove

remove方法相對簡單,但是也伴随着節點清除操作

private void remove(ThreadLocal<?> key) {
            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)]) {
                //1.循環找到key相同的Entry,進行clear操作
                if (e.get() == key) {
                    e.clear();
                    //2.從該位置往後進行清除操作.
                    expungeStaleEntry(i);
                    return;
                }
            }
        }
           

三.方法分析

1.get

get方法這邊也是用到了懶性加載的思想,真正使用到的時候才去建立

public T get() {
        //1.拿到目前線程
        Thread t = Thread.currentThread();
        //2.擷取TreadLocalMap
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            //3.Map不為空,則拿到Entry
            ThreadLocalMap.Entry e = map.getEntry(this);
            //4.Entry不為空則傳回值
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T) e.value;
                return result;
            }
        }
        //5.ThreadLocalMap或者Entry為空,則初始化Map或Entry
        return setInitialValue();
    }


      private T setInitialValue() {
        T value = initialValue();
        //1.拿到目前線程
        Thread t = Thread.currentThread();
        //2.拿到目前Map
        ThreadLocalMap map = getMap(t);
        //3.map不為空則設值Entry
        if (map != null)
            map.set(this, value);
        else
            //4.map為空則建立新map
            createMap(t, value);
        //5.傳回值
        return value;
    }
           

2.set

set就比較簡單啊,這邊就是用到了懶性加載的思想,當用到的時候才去加載
           
public void set(T value) {
        //1.拿到目前線程
        Thread t = Thread.currentThread();
        //2.擷取ThreadLocalMap
        ThreadLocalMap map = getMap(t);
        //3.如果Map不為空,調用ThreadLocalMap的set方法設值
        if (map != null)
            map.set(this, value);
        else
            //4.如果Map為空,建構value入參的Map
            createMap(t, value);
    }
           

繼續閱讀