面試官:說說你對ThreadLocal的瞭解

yes的練級攻略發表於2019-05-09

一般有多個孩子的家庭,買玩具都得買多個。如果就買一個,嘿嘿就比較刺激了。這就是避免共享,給孩子每人一個玩具對應到我們Java中也就是每個執行緒都有自己的本地變數,我們們自己玩自己的,避免爭搶,和諧相處使得執行緒安全。

Java就是通過ThreadLocal來實現執行緒本地儲存的。

這思路也很清晰,就是每個執行緒要有自己的本地變數唄,那就Thread裡面搞一個私有屬性唄ThreadLocal.ThreadLocalMap threadLocals = null; 就是如下圖所示的這個關係

面試官:說說你對ThreadLocal的瞭解

ThreadLocal

簡單的應用如下

    public class Demo {
		private static final ThreadLocal<Foo> fooLocal = new ThreadLocal<Foo>();
 
		public static Foo getFoo() {
			return fooLocal.get();
		}
 
		public static void setFoo(Foo foo) {
			fooLocal.set(foo);
		}
	}
複製程式碼

再深入瞭解一下內部情況,ThreadLocalMapThreadLocal的內部靜態類,它雖然叫Map但是和java.util.Map沒有啥親戚關係,只是它實現的功能像Map

    static class ThreadLocalMap {
        static class Entry extends WeakReference<ThreadLocal<?>> {
            Object value;
            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }
        private static final int INITIAL_CAPACITY = 16;
        private Entry[] table;
複製程式碼

可以看到ThreadLocalMap裡面有個Entry陣列,只有陣列沒有像HashMap那樣有連結串列,因此當hash衝突的之後,ThreadLocalMap採用線性探測的方式解決hash衝突。

線性探測,就是先根據初始keyhashcode值確定元素在table陣列中的位置,如果這個位置上已經有其他key值的元素被佔用,則利用固定的演算法尋找一定步長的下個位置,依次直至找到能夠存放的位置。在ThreadLocalMap步長是1。

用這種方式解決hash衝突的效率很低,因此要注意ThreadLocal的數量

        /**
         * Increment i modulo len. 
         */
        private static int nextIndex(int i, int len) {
            return ((i + 1 < len) ? i + 1 : 0);
        }

        /**
         * Decrement i modulo len.
         */
        private static int prevIndex(int i, int len) {
            return ((i - 1 >= 0) ? i - 1 : len - 1);
        }
複製程式碼

而且可以看到這個EntryThreadLocal的弱引用作為key。那為什麼要搞成弱引用(只要發生了GC弱引用物件就會被回收)呢?

首先ThreadLocal內部沒有儲存任何的值,它的作用只是當我們的ThreadLocalMap的key,讓執行緒可以拿到對應的value。當我們不需要用這個key的時候我們,我們把fooLocal=null這樣強引用就沒了。假設Entry裡面也是強引用的話,那等於這個ThreadLocal例項還有個強引用在,那麼我們想讓GC回收fooLocal就回收不了了。那可能有人想,你弄成弱引用不是很危險啊,萬一GC一下不是沒了?別怕只要fooLocal這個強引用在這個ThreadLocal例項就不會回收的。(關於強軟弱虛引用可以看我之前的文章四種引用方式的區別)

因此弄成弱引用,主要是讓沒用的ThreadLocal得以GC清除。

這裡可能還有人問那key清除掉了,value咋辦,這個Entry還在的呀。是的,當在使用執行緒池的情況下,由於執行緒的生命週期很長,某些大物件的key被移除了之後,value一直存在的就可能會導致記憶體洩漏。

不過java考慮到這點了。當呼叫get()、set()方法時會去找到那個key被幹掉的entry然後幹掉它。並且提供了remove()方法。雖然get()、set()會清理keynull的Entry,但是不是每次呼叫就會清理的,只有當get時候直接hash沒中,或者set時候也是直接hash沒中,開始線性探測時候,碰到key為null的才會清理。

        //get 方法
        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); //直接沒命中
        }
        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)  //探測到key是null的就清理
                    expungeStaleEntry(i);
                else
                    i = nextIndex(i, len); //否則繼續
                e = tab[i];
            }
            return null;
        }
        //set 方法
        private void set(ThreadLocal<?> key, Object value) {
            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;    //如果已經有就替換原有的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();
        }
複製程式碼

因此,當不需要threadlocal的時候還是顯示呼叫remove()方法較好。

結語

執行緒本地儲存本質就是避免共享,在使用中注意記憶體洩露問題和hash碰撞問題即可。使用還是很廣泛的像spring中事務就用到threadlocal


如有錯誤歡迎指正!

個人公眾號:yes的練級攻略

有相關面試進階(分散式、效能調優、經典書籍pdf)資料等待領取

相關文章