面試官問我:ThreadLocal的原理是什麼,Looper物件為什麼要存在ThreadLocal中?

語言: CN / TW / HK

記得看文章三部曲,點贊,評論,轉發。 微信搜尋【程式設計師小安】關注還在移動開發領域苟活的大齡程式設計師,“面試系列”文章將在公眾號同步釋出。

1.前言

最近看到網路上都說現在內卷化嚴重,面試很難,作為顏值擔當的天才少年_也開始了面試之路,既然說面試官各個都是精銳,很不巧,老子打的就是精銳。

2.正文

天才少年_信心滿滿的來到某東的會議室,等待面試,決定跟他們好好切磋一翻。

在這裡插入圖片描述

小夥子,我是今天的面試官,看我的髮型你應該知道我的技術有多強了,閒話不多說了,Looper物件使用ThreadLocal來保證每個執行緒有唯一的Looper物件,並且執行緒之間互不影響,這個知道吧,那麼我們來聊聊ThreadLocal吧。

果然是精銳,這麼直接,毫無前戲,看來得拿出真本領了。 ThreadLocal為每個使用該變數的執行緒提供獨立的變數副本,所以每一個執行緒都可以獨立地改變自己的副本,而不會影響其它執行緒所對應的副本,從而實現執行緒隔離。

那給我講講Looper中是如何使用ThreadLocal的?

說這麼多原來還是聊Looper的原始碼,哈哈,這可是我的強項。 在這裡插入圖片描述

如下是Looper類關於ThreadLocal的主要程式碼行 1)初始化ThreadLocal: java // sThreadLocal.get() will return null unless you've called prepare(). static final ThreadLocal<Looper> sThreadLocal = new ThreadLocal<Looper>(); 2)呼叫set方法可以儲存當前執行緒的Looper物件,呼叫get方法獲取當前執行緒的Looper物件: java private static void prepare(boolean quitAllowed) { if (sThreadLocal.get() != null) { throw new RuntimeException("Only one Looper may be created per thread"); } sThreadLocal.set(new Looper(quitAllowed)); }

嗯,小夥子看來對Looper很熟悉,既然內卷,那我肯定不問Looper,我們來聊聊ThreadLocal的原理。

就知道會這麼問,還好,那晚我跟小韓一起在辦公室看原始碼,她偷偷告訴我她有了我的孩子。 不對,那晚好像是我一個人看原始碼的,不管了,我努力的回憶著ThreadLocal的原始碼。 1)我們先看看ThreadLocal的set方法: java public void set(T value) { Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) map.set(this, value); else createMap(t, value); } 可以看到先獲取到當前執行緒t,隨後通過getMap方法獲取ThreadLocalMap物件,把value塞到ThreadLocalMap物件中,繼續跟到getMap方法: java ThreadLocalMap getMap(Thread t) { return t.threadLocals; } 這邊就是從Thread物件中獲取到threadLocals變數,讓我們來看看threadLocals是什麼,直接定位到Thread類中: java class Thread implements Runnable { /* ThreadLocal values pertaining to this thread. This map is maintained * by the ThreadLocal class. */ ThreadLocal.ThreadLocalMap threadLocals = null; ...... } 到這裡是不是豁然開朗,原來每個Thread內部都有一個ThreadLocalMap物件,用來儲存Looper。這樣,每個執行緒在儲存Looper物件到ThreadLocal中的時候,其實是儲存在每個執行緒內部的ThreadLocalMap物件中,從而其他執行緒無法獲取到Looper物件,實現執行緒隔離。

既然已經說到這裡了,那給我講講ThreadLocalMap吧。

問吧,反正那晚很漫長,我們一起除了看原始碼,也沒有做其他的事情,至於孩子怎麼來的,我只能說我是個老實人,我什麼都不知道。

在這裡插入圖片描述

1)先看下ThreadLocalMap的建構函式和關鍵成員變數: ```java /* * The table, resized as necessary. * table.length MUST always be a power of two. / private Entry[] table;

    ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
        table = new Entry[INITIAL_CAPACITY];
        int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
        table[i] = new Entry(firstKey, firstValue);
        size = 1;
        setThreshold(INITIAL_CAPACITY);
    }

2)通過Entry[] table可以知道,雖然他叫做ThreadLocalMap,但是底層竟然不是基於hashmap儲存的,而是以陣列形式。呸,渣男,表裡不一。 那我們就不看他的外表了,去看看他的內在,Entry的定義如下:java static class Entry extends WeakReference<ThreadLocal<?>> { /* The value associated with this ThreadLocal. / Object value;

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

``` 可以看到,雖然他不是以hashmap的形式儲存,但是Entry物件裡面也是設計成key/value的形式解決hash衝突的。所以你可以想象成ThreadLocalMap是個陣列,而儲存在數組裡面的各個物件是以key/value形式的Entry物件。

不好意思,打斷一下,這邊我有幾個問題想問下,第一個是為什麼要設計成陣列?

這種問題還問,我們中臺返回資料給客戶端的時候,不全是憑心情嗎,明明就只返回一個物件,他非要返回一個數組,這tm我怎麼知道為什麼要這麼設計,可能寫ThreadLocalMap的工程師是我們中臺的同學吧,哈哈。 抱怨歸抱怨,我大腦開始瘋狂運轉,這得從ThreadLocal的set方法說起,那我們繼續深入看set方法吧: 1)ThreadLocal的set方法: java public void set(T value) { Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) map.set(this, value); else createMap(t, value); } 2)上面已經講過,set方法是先獲取到當前執行緒t,隨後通過getMap方法獲取ThreadLocalMap物件,然後把this作為key,Looper作為value塞到ThreadLocalMap物件中,this是什麼,就是當前類物件唄,也就是ThreadLocal,到這裡,我應該能夠解答糟老頭子,不對,是面試官的問題了,ThreadLocalMap設計成陣列,肯定是有些執行緒裡面不止一個ThreadLocal物件,可能會初始化多個,這樣儲存的時候就需要陣列了。 為了弄清楚,ThreadLocalMap是如何儲存的,我們繼續看下ThreadLocalMap的set方法,誰讓咱是個好奇心很重的人呢。

在這裡插入圖片描述 ```java 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();
    }

``` 程式碼量不大,int i = key.threadLocalHashCode & (len-1);這段程式碼我相信經常面試頭條的同學應該不陌生(面試必問題目,hashmap的原始碼)這段程式碼跟hashmap中key的hash值的計算規則一致,目的就是為了解決hash衝突,尋找陣列插入下標的。

再往下是個for迴圈,裡面是尋找可插入的位置,如果需要插入的key在陣列中已存在,則直接把需要插入的value覆蓋到陣列中的vaule上: java if (k == key) { e.value = value; return; } 如果key為空,則創建出Entry物件,放在該位置上: java if (k == null) { replaceStaleEntry(key, value, i); return; } 如果上面兩種情況都不滿足,那就尋找下一個位置i,繼續迴圈上面的兩個判斷,直到找到可以插入或者重新整理的位置。 java e = tab[i = nextIndex(i, len)]

那順便把get方法也講下吧。

服務肯定會全套,不用你問,我也會講get方法的邏輯,這是咱技工(技術工種)的職業操守。 1)ThreadLocal的get方法如下: java 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(); } 跟set方法類似,先獲取到當前執行緒t,隨後通過getMap方法獲取ThreadLocalMap物件,再通過getEntry獲取到Enety物件: 2)getEntry方法如下所示: java 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);又是非常熟悉的程式碼,通過該方法獲取到陣列下標i,如果該位置的Entry物件中的key跟當前的TreadLocal一致,則返回該Entry物件,否則繼續執行getEntryAfterMiss方法: ```java 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;
    }

程式碼很容易理解,開啟迴圈查詢,如果當前ThreadLocal跟陣列下標i對應的Entry物件的key相等,則返回當前Entry物件; 如果陣列下標I對應的Entry物件的key為空,則執行expungeStaleEntry(i)方法,從方法命名就知道,刪除廢棄的Entry對應,其實就是做了次記憶體回收,expungeStaleEntry原始碼如下所示:java private int expungeStaleEntry(int staleSlot) { Entry[] tab = table; int len = tab.length;

        // expunge entry at staleSlot
        tab[staleSlot].value = null;
        tab[staleSlot] = null;
        size--;

        // Rehash until we encounter null
        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 {
                int h = k.threadLocalHashCode & (len - 1);
                if (h != i) {
                    tab[i] = null;

                    // Unlike Knuth 6.4 Algorithm R, we must scan until
                    // null because multiple entries could have been stale.
                    while (tab[h] != null)
                        h = nextIndex(h, len);
                    tab[h] = e;
                }
            }
        }
        return i;
    }

我們主要看如下幾行程式碼:java if (k == null) { e.value = null; tab[i] = null; ``` 這個方法其實實現的功能就是,如果陣列中,某個Entry物件的key為空,該方法會釋放掉value物件和Entry物件。 再回到上面,如果ThreadLocal跟陣列下標i對應的Entry物件的key既不相等,也不為空,則呼叫nextIndex方法,向下查詢,跟set方法的nextIndex方法一致。

嗯,小夥可以啊,ThreadLocal理解算比較透徹了,但是既然你過來打精英,那咱們就再深入一點,聊聊為什麼Entry物件要key設定成弱引用呢?還有ThreadLocal是否存在記憶體洩露呢?

傳統面試其實講究點到為止,點到為止我就通過了,如果我使勁吹牛逼,一下就能把他忽悠懵逼。這個年輕人不講面德,來!騙!來!內卷我一個老客戶端,這好嗎?這不好,我勸,這位面試官,耗子尾汁,好好反思,以後不要再出這種面試題,IT人應該以和為貴,謝謝!

在這裡插入圖片描述

既然來面試,我肯定是跟小韓單獨相處了好幾個夜晚,不對,是看了好幾個夜晚的原始碼。 讓我們再回顧下Entry的建構函式: ```java static class Entry extends WeakReference<ThreadLocal<?>> { /* The value associated with this ThreadLocal. / Object value;

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

``` 從建構函式可以看到,Entry物件中的key,即ThreadLocal物件為弱引用,為了再秀一把技術,我先普及下弱引用的定義吧:

弱引用:在垃圾回收器執行緒掃描它所管轄的記憶體區域的過程中,一旦發現了只具有弱引用的物件,不管當前記憶體空間足夠與否,都會回收它的記憶體。不過,由於垃圾回收器是一個優先順序很低的執行緒,因此不一定會很快發現那些只具有弱引用的物件。

接下來的這段話要仔細讀幾遍哦,畫重點啦。 在這裡插入圖片描述

key如果不是WeakReference弱引用,則如果某個執行緒死迴圈,則ThreadLocalMap一直存在,引用住了ThreadLocal,導致ThreadLocal無法釋放,同時導致value無法釋放;當是WeakReference弱引用時,即使執行緒死迴圈,當建立ThreadLocal的地方釋放了,ThreadLocalMap的key會同樣被被釋放,在呼叫getEntry時,會判斷如果key為null,則會釋放value,記憶體洩露則不存在。當然ThreadLocalMap類也提供remove方法,該方法會幫我們把當前ThreadLocal對應的Entry物件清除,從而不會記憶體洩露,所以如果我個人覺得如果每次在不需要使用ThreadLocal的時候,手動呼叫remove方法,也不存在記憶體洩露。

嗯,不錯不錯,深度挖得差不多了,我們再回到表明來,說說為什麼Looper物件要存在ThreadLocal中,為什麼不能公用一個呢,或者每個執行緒持有一個呢?

果然是資深面試官,問題由淺入深,再回到問題本質中來,這技術能力,對得起他那脫落的毛髮。 在這裡插入圖片描述

首先,個人覺得,技術上,Looper物件可以公用一個全域性的,即每個執行緒公用同一個Looper物件,但是為了執行緒安全,我們就要進行執行緒同步處理,比如加同步鎖,這樣執行效率會降低,另外一方面Andriod系統如果5秒內沒有處理Looper訊息,則會造成ANR,加同步鎖會增加ANR的機率。

至於為什麼不每個執行緒都持有一個Looper物件呢,這個也很好理解:為了節約記憶體。 如果你就只有2個執行緒,其實用不用ThreadLocal感覺不到優勢,如果要初始化1000個執行緒,每個執行緒都初始化Looper物件的話,那麼就會存在1000個Looper物件,造成很大的記憶體開銷,而且我們知道,多執行緒時,往往會把執行緒加入執行緒池,比如: java ExecutorService threadPool = Executors.newFixedThreadPool(8); 用執行緒池的好處就是執行緒複用,如上的程式碼,只會例項出8個執行緒,而ThreadLocal為每個使用該變數的執行緒提供獨立的變數副本,所以每個執行緒持有一個Looper物件,也就會初始化出8個Looper物件,很明顯,用ThreadLocal節省了記憶體。

可以了,你對ThreadLocal的瞭解比較全面了,把我打動了,回去等offer吧。


微信搜尋【程式設計師小安】“面試系列(java&andriod)”文章將在公眾號同步釋出。