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

來源:互聯網
上載者:User

標籤:ash   線程   地方   new t   索引   sha   amp   trie   1.7   

1.大概去哪裡看

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

ThreadLocal.ThreadLocalMap threadLocals = null;


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

private Entry[] table;

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

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數組}(忽略存取權限的事兒)

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

2.代碼實現分析

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 對象自身,

假如我們聲明了一串代碼:

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) 這樣的設定。
執行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;
以上 1、2 兩種情況的時候,會執行getEntryAfterMiss(key, i, e);

在同一個線程中,626627285、626627317、626627573算到的i值均為5,但是只有626627285==table[5].get(),
626627317、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);//5System.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 值,是適配新數組的正確的值。

繼續拿626627285、626627317、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得實現原理與工作原理。歡迎大家批評討論斧正。謝謝

 

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

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.