ThreadLocal 工做原理、部分源碼分析

1.大概去哪裏看java

ThreadLocal 其根本實現方法,是在Thread裏面,有一個ThreadLocal.ThreadLocalMap屬性算法

ThreadLocal.ThreadLocalMap threadLocals = null;


ThreadLocalMap 靜態內部類維護了一個Entry 數組 數組

private Entry[] table;

查看Entry 源碼,它維護了兩個屬性,ThreadLocal 對象 與一個Objectthis

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

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

那麼,這幾項彷佛能夠這麼串下來: Thread. currentThread().threadLocals. table{當前線程,的ThreadLocalMap對象,的Entry數組}(忽略訪問權限的事兒)spa

------------------------------------------------我是分割線------------------------------------------------線程

2.代碼實現分析code

ThreadLocal 提供set(),get()方法,用於數據的寫入與讀取。數據的存儲與獲取的位置,即
Thread. currentThread().threadLocals. table {當前線程,的ThreadLocalMap對象,的Entry數組}對象

public void set(T value) {
        Thread t = Thread.currentThread();//獲取當前線程t
        ThreadLocalMap map = getMap(t);//獲取threadLocals 對象
        if (map != null) 
            map.set(this, value);//調用 ThreadLocalMap 的set方法向 threadLocals 中寫入一條數據
        else
            createMap(t, value);//若是threadLocals 爲null 則爲當前線程t 建立一個map,並插入數據
}

map.set(this, value);注意,這裏,傳入的第一個參數爲this 即 ThreadLocal 對象自身,blog

假如咱們聲明瞭一串代碼:索引

private static ThreadLocal<String> threadLocal = new ThreadLocal<String>();

而後咱們又執行了 threadLocal.set(「string 1234」);
那麼,在Thread. currentThread().threadLocals. table 中,應該有這麼一個Entry :ThreadLocal指向threadLocal,value 爲 「string 1234」

分析源碼(這裏,全部的源碼都來自於jdk1.7.0_71):

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

            // We don't use a fast path as with get() because it is at
            // least as common to use set() to create new entries as
            // it is to replace existing ones, in which case, a fast
            // path would fail more often than not.

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

                if (k == key) {
                    e.value = value;
                    return;
                }

                if (k == null) {
                    replaceStaleEntry(key, value, i);
                    return;
                }
            }

            tab[i] = new Entry(key, value);
            int sz = ++size;
            if (!cleanSomeSlots(i, sz) && sz >= threshold)
                rehash();
        }

分析兩處:
1. int i = key.threadLocalHashCode & (len-1);
根據當前的ThreadLocal 的threadLocalHashCode 跟 ThreadLocalMap.table的長度-1 ,按位與,得到目標索引值 i , 若是tab[i] 爲空的話,將會在 tab[i] 處插入一個Entry ;

2. for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)])

若是若是tab[i] 不爲空,則調用i = nextIndex(i, len) 將i值進行+1或者置爲0,而後判斷e是否爲null 若是e!=null 判斷e 中的 ThreadLocal對象,跟傳入的ThreadLocal 對象,是否爲同一個對象。若是是同一個對象,則對e的value 進行從新賦值。若是在遍歷的過程當中發現某個e的ThreadLocal 對象爲空,則將Entry(threadLocal,」 string 1234」) 設置在此時的tab[i]處。
(若是一開始進來的時候e 爲null 即 tab[i]==null 。是不會走for循環的,會直接把Entry(threadLocal,」 string 1234」) 賦值到table[i]);

private static int nextIndex(int i, int len) {
return ((i + 1 < len) ? i + 1 : 0);
}

分析,爲何會有i = nextIndex(i, len) 這樣的設定。(2017-01-04 ps:這是一種hash防衝撞的方案)
執行int i = key.threadLocalHashCode & (len-1);的時候,極可能不一樣的key.threadLocalHashCode獲得了相同的 i 值,那麼,就從 i 開始,遍歷table對象,找到一個能夠放置Entry(threadLocal,」 string 1234」) 的位置,

好比:

    System.out.println(626627285 & 16-1);//5
    System.out.println(626627317 & 16-1);//5
    System.out.println(626627573 & 16-1);//5

這三個,獲取到的i值,都爲5(固然實際用到的hashCode的算法不是這樣的,不會產生這麼接近的數)。

在同一個線程中,626627285先set(value1)了,獲得5,table[5]爲空,那就填進去 table[5]=new Entry(626627285,value1);
626627317接着set(value2),算出來i=5,
可是table[5]已經有人佔了,那就只能看table[6]有沒有空閒位置,一看table[6]==null,好,就放這兒了table[6]=new Entry(626627317,value2);
626627573接着set(value3),算出來i=5,
可是table[5]已經有人佔了,那就只能看table[6]有沒有空閒位置,一看table[6]也被佔了,再看table[7]==null,好,就放這兒了table[7]=new Entry(626627573,value3)
假若有個線程算出來i=15 可是table[15]!=null,須要向後找空閒位置,table[16]是越界的,nextIndex返回0,從table[0]開始找

下面這串代碼,維護了table 的長度,避免了遍歷了一圈table 卻找不到 table[i]==null 的狀況,即保證table的某些索引處確定爲null,由於還沒填滿的時候就已經擴容了。

if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();

 

------------------------------------------------我是分割線------------------------------------------------

下面分析get()

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();
}

1. 獲取當前線程的threadLocals 並傳入 ThreadLocal 對象,獲取對應的值。
2. 若是當前線程的threadLocals 爲null ,則爲當前線程t 建立一個map,並插入數據setInitialValue ()=null,並返回null

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;
}

分析map.getEntry(this)

private Entry getEntry(ThreadLocal<?> key) {
    int i = key.threadLocalHashCode & (table.length - 1);
    Entry e = table[i];
    if (e != null && e.get() == key)
        return e;
    else
        return getEntryAfterMiss(key, i, e);
}

這裏看到,也是先使用 int i = key.threadLocalHashCode & (table.length - 1); 獲得一個索引值,而後去table獲取 Entry對象,獲得幾種結果:
1. e!=null && e.get()!=key 由於是經過「int i = key.threadLocalHashCode & (table.length - 1);」獲取的索引值,不一樣的ThreadLocal 對象,可能獲取到相同的索引值,因此,這種狀況是存在的。
2. e==null 當前的ThreadLocal 對象 壓根兒沒有set值。
3. e!=null&&e.get()==key 若是Entry對象的key值與當前傳入的ThreadLocal 對象,是同一個對象,則返回e 而後在 get()方法中,返回e.value;
以上 一、2 兩種狀況的時候,會執行getEntryAfterMiss(key, i, e);

在同一個線程中,62662728五、62662731七、626627573算到的i值均爲5,可是隻有626627285==table[5].get(),
62662731七、626627573這兩個,都須要走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();
        if (k == key)
            return e;
        if (k == null)
            expungeStaleEntry(i);
        else
            i = nextIndex(i, len);
        e = tab[i];
    }
    return null;
}

1. 若是初始出入的e==null 則不進入while 循環直接返回null

2. 在while循環裏面,若是e.get()==key 即,e 的Entry對象的key值與當前傳入的ThreadLocal 對象,是同一個對象,則返回e 。
3. 若是e.get()==null 的狀況下,先將table[i]置爲空,而後向後遍歷直到
table[nextIndex(i, len)]==null,將table[nextIndex(i, len)]!=null的對象,從新寫入table中。
4. k!=key&&key!=null的時候,則調用i = nextIndex(i, len) 將i值進行+1或者置爲0,而後判斷e是否爲null 若是e!=null 判斷e 中的 ThreadLocal對象,跟傳入的ThreadLocal 對象,是否爲同一個對象。若是是同一個對象,返回當前的 Entry對象,若是遇到了e==null的時候,尚未找到目標的Entry ,就返回null 。
爲何找到e==null的地方就能夠跳出了呢?
若是若是tab[i] 不爲空,則調用i = nextIndex(i, len) 將i值進行+1或者置爲0,而後判斷e是否爲null 若是e!=null 判斷e 中的 ThreadLocal對象,跟傳入的ThreadLocal 對象,是否爲同一個對象。若是是同一個對象,則對e的value 進行從新賦值。若是在遍歷的過程當中發現某個e的ThreadLocal 對象爲空,則將Entry(threadLocal,」 string 1234」) 設置在此時的tab[i]處。
這裏,插入的Entry(threadLocal,」 string 1234」) ,要麼,在初始的i 處,要麼,日後+1順延,不會跳過某個索引值,而後進行賦值。因此,當table[i]==null的時候,已經能夠不繼續找了。


------------------------------------------------我是分割線------------------------------------------------

3 還有一點我以爲很重要的東西

分析到這裏的時候,咱們應該有發現一個很重要的問題,即:
int i = key.threadLocalHashCode & (table.length - 1);
得到的i值是固定的嗎?
很明顯不是的,由於table 會擴容,table.length 會變,獲得的i值也是不同的:

System.out.println(626627285 & 16-1);//5
System.out.println(626627285 & 32-1);//21

這樣的話,table[i]豈不是會出錯?

而後,仔細閱讀源碼,在進行擴容的時候,會調用resize()方法:

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;
}

在resize() 方法中,咱們看到,這裏新建了一個長度爲本來的長度2倍的Entry數組,而後,將原Entry數組的全部元素,

挨個兒的從新計算索引 int h = k.threadLocalHashCode & (newLen - 1);

而後賦值原有的Entry 到新的Entry 數組中,這樣,就保證了數組擴容以後,獲取到的 i 值,是適配新數組的正確的值。

繼續拿62662728五、62662731七、626627573舉例,這三個,在數組擴容爲長度=32的時候,算出來的i值,均爲21,那麼他們將會是這麼算的:
oldTab[5]!=null oldTab[5].get=626627285 算得i=21,table[21]==null table[21]=oldTab[5];
oldTab[6]!=null oldTab[6].get=626627317 算得i=21,table[21]!=null table[22]==null table[22]=oldTab[6];
oldTab[7]!=null oldTab[7].get=626627573 算得i=21,table[21]!=null tanle[22]!=null table[23]==null table[23]=oldTab[7];

 

------------------------------------------------我是分割線------------------------------------------------

 以上,是我我的查看jdk源碼,分析出來的ThreadLocal得實現原理與工做原理。歡迎你們批評討論斧正。謝謝

相關文章
相關標籤/搜索