Android Handler學習筆記(三)

回顧

 經過前面兩篇筆記的學習,已經知道了Handler如何使用,Looper的建立和做用,MessageQueue的建立和做用,Message的建立和做用。數組

  1. Handler在主線程中建立,在子線程中發送消息,經過調用sendMessage()post()系列重載方法來發送消息。發送消息的時候會將須要處理消息的時間一併攜帶上去,而後根據時間將Message添加到MessageQueue中相應的位置。安全

  2. Looper在主線程建立的時候就會經過調用prepareMainLooper()方法建立,將當前線程標記爲一個不可退出的循環線程。同時會建立MessageQueue對象,而後啓動loop()方法不斷地從MessageQueue中取出須要操做的Message對象,經過它的target屬性指定的Handler,調用Handler中對用的方法去處理消息。bash

  3. MessageQueue是一個根據時間建立的消息隊列,在Looper中的構造方法執行的時候同時會建立出MessageQueue對象。主要負責消息的入隊和出隊的操做。app

  4. Message是消息的實體,表示具體要執行的內容,Message內部經過維護一個對象池來實現Message對象的複用。在從obtain()方法中獲取到插入到MessageQueue的這段時間處於沒有使用的狀態,在插入到MessageQueuerecycleUnchecked()方法回收處於使用中的狀態。ide

 這篇筆記主要學習在Looper中拿到Message以後具體是如何處理的。函數

Handler.dispatchMessage(Message)方法源碼

 在第一篇筆記中,咱們已經瞭解了,在Looperloop()方法中執行循環獲取下一個Message以後,會經過msg.target.dispatchMessage(msg)來分發當前Message給對應的Handler,下面是這個方法的源碼:oop

/**
 * Handle system messages here.
 */
public void dispatchMessage(Message msg) {
    if (msg.callback != null) {
        handleCallback(msg);
    } else {
        if (mCallback != null) {
            if (mCallback.handleMessage(msg)) {
                return;
            }
        }
        handleMessage(msg);
    }
}
複製代碼
  1. 在這個方法中,首先會判斷當前Messagecallback是否爲空,經過上一篇筆記能夠了解到,Messagecallback的類型是Runnable,經過Handler.post(Runnable)系列構造函數發送消息的時候會給Messagecallback屬性賦值,源碼以下:
public final boolean post(Runnable r)
{
   return  sendMessageDelayed(getPostMessage(r), 0);
}

private static Message getPostMessage(Runnable r) {
    Message m = Message.obtain();
    m.callback = r;
    return m;
}
複製代碼
  1. 若是Messagecallback屬性不爲空,則會執行handCallback(Message)方法,源碼以下:
private static void handleCallback(Message message) {
    message.callback.run();
}
複製代碼

 源碼中能夠看到,這裏也只是執行的Messagecallback屬性所對應的Runnablerun()方法。post

  1. 若是Messagecallback屬性爲空,也就是說不是經過post()系列重載方法發送的消息,那麼接下里就判斷Handler中的mCallback屬性是否爲空。

mCallbackHandler.Callback類型,這是一個Handler內部的接口,也是用來處理Message信息的。經過查看mCallback的定義final Callback mCallback;也能知道,這個屬性只能經過構造函數賦值。因此咱們在定義Handler的時候還有另外一種方式:學習

private Handler mHandler = new Handler(new Handler.Callback() {
    @Override
    public boolean handleMessage(Message msg) {
        if(msg.what == 0){
            mBinding.tvResult.setText(msg.obj.toString());
        }
        return false;
    }
});
複製代碼

 經過這種方式咱們就不須要再使用內部類的方式來建立一個Handler,避免了可能出現的內存泄漏的問題。ui

  1. 若是mCallback屬性的值不爲空,那麼就經過這個接口來處理Message,同時查看也能知道,須要注意若是成功處理了Message,那麼就要返回true,這個方法默認實現返回false。由於若是處理成功了還返回false,則會繼續執行下面的handleMessage(msg)方法,這是沒有必要的。

  2. 若是mCallback屬性的值爲空,那麼就調用handleMessage(Message)方法來處理Message,這個方法通常須要咱們重寫它的實現,源碼中默認是空的實現:

/**
 * Subclasses must implement this to receive messages.
 */
public void handleMessage(Message msg) {
}
複製代碼

 至此,Handler的整個處理流程就結束了,以下圖所示:

Handler總體執行流程
Handler總體執行流程

關於Message中障棧的添加

 在以前的筆記中有提到過,有一種target爲空的Message稱爲障棧,可是咱們本身經過Handler發送的Message都是給target賦值了的,那麼MessageQueue中的障棧是如何添加的呢,源碼以下:

public int postSyncBarrier() {
    return postSyncBarrier(SystemClock.uptimeMillis());
}

private int postSyncBarrier(long when) {
    // Enqueue a new sync barrier token.
    // We don't need to wake the queue because the purpose of a barrier is to stall it. synchronized (this) { final int token = mNextBarrierToken++; final Message msg = Message.obtain(); msg.markInUse(); msg.when = when; msg.arg1 = token; Message prev = null; Message p = mMessages; if (when != 0) { while (p != null && p.when <= when) { prev = p; p = p.next; } } if (prev != null) { // invariant: p == prev.next msg.next = p; prev.next = msg; } else { msg.next = p; mMessages = msg; } return token; } } 複製代碼

 在上面的源碼中,經過postSyncBarrier()的這個函數和它的重載函數就能夠添加一個障棧,添加的方式和普通的添加Message的方式差異不大,惟一的區別就是不用考慮障棧的影響,由於如今要添加的自己就是一個障棧,因此只須要根據它的執行時間的因素將它插入到隊列的合適的位置便可。

ThreadLocal

 在以前的筆記中,咱們瞭解過,一個線程只能有一個Looper,這是由於在建立Looper對象的時候,咱們建立完一個Looper對象,便會將這個對象保存到ThreadLocal中,下一若是還須要建立Looper對象,那麼首先會先去檢測ThreadLocal中有沒有值,若是有,說明當前線程已經建立過一個Looper了,就會拋出異常,源碼以下:

static final ThreadLocal<Looper> sThreadLocal = new ThreadLocal<Looper>();
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));
}
複製代碼

 從上面的源碼能夠看出,建立線程的時候首先會去ThareadLocal中查找是否有已經建立過的Looper對象,若是有,則會拋出異常,沒有則會調用私有的構造方法建立出一個Looper對象,而後把這個對象設置到ThreadLocal中。

 那麼源碼看到這裏,就會有如下問題須要解決。

ThreadLocal是什麼?有什麼做用?

 源碼中對ThreadLocal的註釋是:ThreadLocal類提供了線程局部變量,這些變量與普通變量不一樣,每一個線程均可以經過其getset方法來訪問本身獨立的初始化的變量副本。ThreadLocal實例變量一般是類中的privte static字段,它們但願將狀態與某一個線程(例如用戶ID或事物ID)相關聯。

 一個簡單的例子是這樣的:

private ThreadLocal<Integer> threadLocal1 = new ThreadLocal<>();
private Integer local2 = Integer.valueOf(0);

@Override
protected void initUi() {
    threadLocal1.set(100);
    local2 = 200;
    Log.e("TAG","主線程中的數據:"+threadLocal1.get()+","+local2);
}

@Override
public void doClick(View view) {
    super.doClick(view);
    if(view.getId() == R.id.btn_get_value){
        Log.e("TAG","主線程中的數據:"+threadLocal1.get()+","+local2);
        return;
    }
    new Thread(new Runnable() {
        @Override
        public void run() {
            threadLocal1.set(400);
            local2 = 500;
            Log.e("TAG","子線程1中的數據:"+threadLocal1.get()+","+local2);
        }
    }).start();
    new Thread(new Runnable() {
        @Override
        public void run() {
            Log.e("TAG","子線程2中的數據:"+threadLocal1.get()+","+local2);
        }
    }).start();
}
複製代碼

 在上面的代碼中,首先在主線程中定義了一個個ThreadLocal<Integer>變量和一個普通的Integer變量,而後給這兩個變量在主線程中分別設置值爲100和200,在點擊事件中啓動了兩個線程,其中線程1對這兩個變量分別設置值爲400和500,線程2沒有設置值,只是打印數據。當點擊另外一個按鈕的時候會再次在主線程中打印數據,最後打印的數據以下:

2020-03-03 09:59:14.097 4059-4059/com.example.myapplication E/TAG: 主線程中的數據:100,200
2020-03-03 09:59:16.150 4059-4151/com.example.myapplication E/TAG: 子線程1中的數據:400,500
2020-03-03 09:59:16.155 4059-4152/com.example.myapplication E/TAG: 子線程2中的數據:null,500
2020-03-03 09:59:18.955 4059-4059/com.example.myapplication E/TAG: 主線程中的數據:100,500
複製代碼

 經過上面打印的數據能夠看到,使用ThreadLocal變量的值和線程相關,在哪一個線程中設置了什麼值,只有在這個線程才能獲取到。另一個線程修改了變量的值也不會對另外一個線程中設置的值有影響。可是普通的變量則是全部線程均可以任意修改使用這個變量。

 還有一種方式是當咱們傳遞一個ThreadLocal變量會發生什麼?

class ThreadTest3 extends Thread{

    private ThreadLocal threadLocal;
    ThreadTest3(ThreadLocal threadLocal){
        this.threadLocal = threadLocal;
    }
    @Override
    public void run() {
        super.run();
        Log.e("TAG","線程3中的數據:"+threadLocal.get());
    }
}
複製代碼

 上面的代碼運行結果以下:

2020-03-03 10:05:44.042 4261-4261/com.example.myapplication E/TAG: 主線程中的數據:100,200
2020-03-03 10:05:46.747 4261-4338/com.example.myapplication E/TAG: 線程3中的數據:null
複製代碼

 能夠看到,即使咱們將ThreadLocal傳遞到另外一個線程,在新的線程中也不會出現以前線程中設置的數據。

 經過上面的例子就能夠知道,ThreadLocal的做用就是提供了當前線程獨立的值,這個值對其它線程是不可見的,其它線程也就不能使用和修改當前線程的值。

 那麼說回到Looper,經過前面的筆記咱們也已經可以瞭解,Looper並非只能存在於主線程中,在其它線程中咱們也可使用Looper來建立本身的消息循環機制。也就是說,Looper是和線程綁定的,主線程擁有主線程的Looper,子線程擁有子線程的Looper,主線程和子線程的Looper不能互相影響,因此咱們看到在建立Looper的時候是經過ThreadLocal來保存Looper對象的,從而達到不一樣線程的Looper不會互相影響的做用。

相關變量

 在ThreadLocal中定義瞭如下變量,根據註釋能夠明白變量的做用:

變量名 做用
threadLocalHashCode 這個變量經過final int修飾,經過靜態方法nextHashCode()賦值。在類初始化的時候便會初始化這個參數的值,因爲ThreadLocal的值實際上是保存在一個Map結構的數據中,其中Map中的key即是ThreadLocal對象,value即是咱們要保存的值。這個值能夠認爲是表明了初始化的那個對象,後面即是經過這個值進行Map的相關操做
nextHashCode 這是一個靜態變量,生成下一個要使用的哈希碼,原子更新,從0開始。nextHashCode()方法內部就是經過這個值加上一個固定的16進制的數字來生成下一個須要使用的threadLocalHashCode
HASH_INCREMENT 這是一個常量,值爲0x61c88647,表示每次生成哈希碼的增量,nextHashCode()方法中使用nextHashCode值加上這個數字來生成下一個須要使用的threadLocalHashCode

 使用到的變量就是這些,下面是一些相關方法的學習。

set(T)

 經過上面的例子咱們知道,初始化一個ThreadLocal以後,咱們會經過set()方法來進行賦值,下面是set(T)方法的源碼:

public void set(T value) {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
    }
複製代碼

 這個方法中的註釋以下:將此線程局部變量的當前線程副本設置爲指定值。大多數子類將不須要重寫此方法,而僅依靠initialValue()方法來設置線程局部變量的值。

 通常狀況下,若是咱們知道ThreadLocal中保存的值,那麼咱們能夠經過重寫initialValue()方法來指定值。可是有時候咱們並不知道初始化的值,也能夠經過這個方法來指定。

 在這個方法中首先獲取到當前的線程,而後經過getMap(t)方法獲取到當前線程的ThreadLocalMap,下面是getMap(t)方法的源碼:

ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}
複製代碼

 能夠看到是直接獲取Thread中的threadLocals對象:

//Thread類中
ThreadLocal.ThreadLocalMap threadLocals = null;
複製代碼

 判斷若是獲取到的ThreadLocalMap爲空,則會執行createMap(t,value)來建立一個ThareadLocalMap

void createMap(Thread t, T firstValue) {
    t.threadLocals = new ThreadLocalMap(this, firstValue);
}
複製代碼

 在這個方法裏面建立了ThreadLocalMap對象,並把須要保存的值經過構造函數傳遞進去,下面是ThreadLocalMap的構造方法:

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);
}
複製代碼

 在這裏開始進入到了ThreadLoacalMap這個類,下面首先總體認識一下這個類。這個類的文檔註釋以下:

 ThreadLocalMap是自定義的哈希映射,僅適用於維護線程局部值。沒有操做導出到ThreadLocal類以外。該類是包私有的,以容許聲明Thread類中的字段。爲了幫助處理很是長的使用壽命,哈希表條目使用WeakReference做爲鍵。可是,因爲不使用參考隊列,所以僅在表空間不足時,才保證刪除過期的條目。

 在這個類中,定義了一個Entry類來保存數據,關於這個類的註釋以下:

 此哈希映射中的條目使用其主引用字段做爲鍵(始終是ThreadLocal對象),擴展了WeakReference.須要注意的是,空鍵(即entry.get() = null)意味着再也不引用該鍵(從擴容方法中能夠看出),所以能夠從表中刪除該條目,在下面的代碼中,此類條目稱爲「陳舊條目」

 相關屬性以下:

屬性名 做用
INITIAL_CAPACITY 這是一個常量,值爲16,註釋中指出這個值必須爲2的冪
table 這是一個Entry數組,根據須要調整大小,length必須爲2的冪
size 表中的條目數
threshold 下一個要調整大小的大小值,默認爲0

 瞭解了ThreadLoalMap的一些基本的信息,再來看建立ThreadLocalMap時的構造方法的源碼:

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);
}
複製代碼

 能夠看到,在構造方法中作了以下操做:

  1. 建立Entry數組,長度爲16
  2. 獲取傳遞過來的ThreadLocal對象中的threadLocalHashCode的值,同時和15作與操做,得出當前的數據應該插入到什麼位置。
  3. 建立一個新的Entry類,保存ThreadLocal(key)和要保存的值firstValue(value),並插入到上一步計算的位置當中。
  4. 設置size屬性的值爲1,表示當前數組中已經插入了一個值
  5. 調用setThreshold(16)來肯定下一次要增長的大小,這個方法源碼以下:
/**
 * Set the resize threshold to maintain at worst a 2/3 load factor.
 */
private void setThreshold(int len) {
    threshold = len * 2 / 3;
}
複製代碼

 能夠看到,是對傳入的參數作了一個*2/3的操做,而後賦值給threshold屬性。

 至此,咱們第一次向ThreadLocalMap中添加數據的時候這個過程就結束了。整個流程相對仍是比較簡單的。

set(T) --ThreadLocalMap不爲空

 設置數據的時候,咱們已經知道,當獲取到當前線程的ThreadLocalMap爲空的時候,會經過建立ThreadLocalMap對象來保存須要保存的數據。

 查看源碼,若是獲取到的ThreadLocalMap不爲空,此時會直接調用map.set(this,value)來設置數據,下面是這個方法的源碼:

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(); } 複製代碼

 這個方法的源碼註釋以下:

 咱們不像get()那樣使用快速路徑,由於使用set()建立條目和替換現有條目至少是存在其中一種狀況的,在這種狀況下,快速路徑失敗的可能性會更高。

因此這裏就有一個問題,get()是如何獲取路徑的,和set()有什麼區別?這個問題先記錄下來,查看get()源碼的時候再去思考。

 在上面的源碼中,操做過程以下:

  1. 計算當要建立或者修改的條目所在的位置,計算方法仍然是以前的ThreadLocal中的threadLocalHashCode值和數組的長度 - 1作與運算,這裏須要注意的是,數組的初始容量是2的n次冪,同時規定了數組的容量也必須是2的n次冪(這個在擴容的時候每次擴容是當前數組長度的2倍就能夠保證了).
  2. 接下來進入到一個for循環,獲取上一步中計算的位置的Entry對象,而後判斷是否爲空,若是爲空就直接跳出循環,將當前須要設置的數據直接建立Entry對象設置到數組中指定的位置上。若是不爲空,則判斷當前位置上這個Entry對象的key是否和要設置/修改的key同樣,同樣的話就直接修改Entryvalue的值,不同則經過nextIndex(i,len)來獲取下一個位置上的Entry,一直循環知道結束或者找到知足條件的Entry。下面是nextIndex(i,len)方法的源碼:
private static int nextIndex(int i, int len) {
    return ((i + 1 < len) ? i + 1 : 0);
}
複製代碼

 經過上面的源碼能夠看到,這裏只是簡單的判斷了下一個數組下標是否越界,若是越界了就從下標0開始,沒有越界則使用當前的下標。

 這裏之因此要這樣作,主要就是由於哈希碼可能存在衝突,爲了解決衝突,這裏使用的是線性探測法,也就是說我須要插入一個數據,可是發現要插入的位置已經有數據了(hash衝突),而且這個位置的數據和我要插入的數據的key並不同,那我就找下一個位置去插入或者修改。結合以前的源碼,其實咱們可以知道,數組的初始長度爲16,第一次賦值佔用一個位置,那麼後面每次設置值的時候總能找到空的位置,每次執行完向空位置插入數據的操做,都會去判斷數組是否須要擴容,若是須要擴容就去擴大數組的大小,從而不會出現元素沒有位置能夠插入的問題。

  1. 若是獲取到的Entry不爲空,那麼判斷獲取到的Entrykey和當前指定的key是否同樣,同樣的話直接修改Entryvalue字段的值並返回。
  2. 若是發現key不同,那麼判斷key是否爲空,爲空,則調用replaceStaleEntry(key,value,i)來替換當前Entrykey對應的value值並返回。下面是replaceStaleEntry(key,value,i)的源碼:
private void replaceStaleEntry(ThreadLocal<?> key, Object value,
                               int staleSlot) {
    Entry[] tab = table;
    int len = tab.length;
    Entry e;
    // Back up to check for prior stale entry in current run.
    // We clean out whole runs at a time to avoid continual
    // incremental rehashing due to garbage collector freeing
    // up refs in bunches (i.e., whenever the collector runs).
    int slotToExpunge = staleSlot;
    for (int i = prevIndex(staleSlot, len);
         (e = tab[i]) != null;
         i = prevIndex(i, len))
        if (e.get() == null)
            slotToExpunge = i;
    // Find either the key or trailing null slot of run, whichever
    // occurs first
    for (int i = nextIndex(staleSlot, len);
         (e = tab[i]) != null;
         i = nextIndex(i, len)) {
        ThreadLocal<?> k = e.get();
        // If we find key, then we need to swap it
        // with the stale entry to maintain hash table order.
        // The newly stale slot, or any other stale slot
        // encountered above it, can then be sent to expungeStaleEntry
        // to remove or rehash all of the other entries in run.
        if (k == key) {
            e.value = value;
                tab[i] = tab[staleSlot];
            tab[staleSlot] = e;
            // Start expunge at preceding stale entry if it exists
            if (slotToExpunge == staleSlot)
                slotToExpunge = i;
            cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
            return;
        }
        // If we did not find stale entry on backward scan, the
        // first stale entry seen while scanning for key is the
        // first still present in the run.
        if (k == null && slotToExpunge == staleSlot)
            slotToExpunge = i;
    }
    // If key not found, put new entry in stale slot
    tab[staleSlot].value = null;
    tab[staleSlot] = new Entry(key, value);
    // If there are any other stale entries in run, expunge them
    if (slotToExpunge != staleSlot)
        cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
}
複製代碼

 在這個方法中,首先設置slotToExpunge變量的值爲如今要替換的位置(提早說明一下,slotToExpunge保存的是須要清理的位置),而後從這個位置的前一個位置開始,向前查找是否還有Entrykeynull的對象,若是有就記錄下來,因爲這是一個環形的數組,所以循環會在遇到第一個Entry爲空的時候中止,或者循環跑了一圈又回到了開始的位置中止。這樣,slotToExpunge變量中保存的實際上是另外一個(或者同一個)Entrykeynull的下標。

 接着開始下一個循環,首先考慮一種狀況:剛開始我但願將一個數據保存在數組下表爲10的這個Entry中,可是不巧因爲哈希衝突數組下表爲10的Entry不爲空,那沒有辦法,就只能日後面找,找了一會,找到數組下表爲13的位置是空的,沒有數據,此時我便把要保存的數據保存在了數組下標爲13的位置上。保存完以後,過了一段時間,數組下表爲10的位置的Entry中的key被清理了,變爲了null。這樣等我下次想要對以前的保存的數據進行修改的時候,哈希計算出來的位置仍是10,可是10上的Entrykey已經爲空了,因此我就從10這個位置向後面找,看看能不能找到我想要修改的那個數據,最後在確定會在13的位置上找到,找到以後,我就把數據修改了。而後呢我就把位置13上的數據和位置10上的數據交換位置,這樣下次我在須要修改原先保存的數據的時候,我就能夠經過哈希計算獲得下標10,而後直接修改就行了,再也不須要向後面遍歷了。因此,這個時候咱們要清理的就是下標爲13的位置的數據了。第二個循環首先就是作了這個事情,對應以下源碼:

for (int i = nextIndex(staleSlot, len);
                 (e = tab[i]) != null;
                 i = nextIndex(i, len)) {
                ThreadLocal<?> k = e.get();

                // If we find key, then we need to swap it
                // with the stale entry to maintain hash table order.
                // The newly stale slot, or any other stale slot
                // encountered above it, can then be sent to expungeStaleEntry
                // to remove or rehash all of the other entries in run.
                if (k == key) {
                    e.value = value;

                    tab[i] = tab[staleSlot];
                    tab[staleSlot] = e;
複製代碼

 接着上面的思路,slotToExpunge保存的是須要清理的位置下標,staleSlot是一開始傳遞進來的要清理的位置的下標,通過第一次的循環以後,slotToExpunge若是仍是和staleSlot相等,那就說明須要清理的就是這個位置,可是因爲通過第二次循環,staleSlot可能和i交換了位置,上面也說了,這種狀況下須要清理的是位置i的數據,所以這裏給slotToExpunge賦值爲i,並執行了cleanSomeSlots(expungeStaleEntry(slotToExpunge), len)方法,首先是expungeStaleEntry(slotToExpunge)方法的源碼以下:

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;
}
複製代碼

 從這個方法的註釋來看,這個方法的做用是將當前要清理的位置上的數據進行清理,同時會檢查當前位置到下一個不爲空的位置上的hash值是否對應,若是不對應那麼也會判斷這個不對應的位置上的數據是否須要交換。
 假設這樣一個例子,有一個ThreadLocal須要保存,經過hash值計算出來的位置爲2,那麼就把這個ThreadLocal存儲在數組下標爲2的位置上,接着又有一個ThreadLocal被建立,計算出來的位置爲3(其實這個狀況發生機率比較小,這裏僅作說明)。那麼又把這個ThreadLocal保存在3的位置上,接下來又有一個ThreadLocal被建立,計算出來的位置也是2,出現了hash衝突,那麼根據以前的規律,咱們已經知道這個ThreadLocal將會保存在4這個位置上,而後又出現一個ThreadLocal,計算出來的位置是3,因爲hash衝突,咱們知道這個ThreadLocal將會被保存在5這個位置上。過了一段時間,3位置上面的ThreadLocal被清理了,致使位置3上的Entrykey變爲了null,此時咱們想要修改原來位置5上的數據,這個數據經過hash計算位置爲3,可是因爲此時這個位置的ThreadLocal被清理,致使key爲空,根據以前的代碼,會從這裏開始遍歷查找下一個key相同或者位置爲空的位置,而後就找到了5,找到5以後,修改了數據,因爲3位置key爲空,則會把3位置上的Entry清理掉,而後把3和5的位置上的數據進行交換,這時3位置上有了數據,5位置上沒了數據。作完這個,就開始從3位置遍歷,一直遍歷到下一個位置爲空的地方,在這個例子中會遍歷到位置爲5的地方中止。之因此要作這個遍歷,就是2和4位置上一樣出現了hash衝突,此時2位置上的key也可能被清理了,那麼就須要把4位置上的數據設置到2位置上。(須要注意的是,若是位置4上的key被清理了,那麼就直接設置位置4上的數據爲空,若是位置2上的key沒被清理,那麼則會從位置2開始往下尋找下一個位置,那麼此時可能尋找的位置仍是4,那這樣數據就沒有變化)。

我的理解這裏爲何要作的這麼複雜,多是由於上面提到的兩個例子自己發生的機率就比較小,計算hash的那個數在不少時候均可以完成完美的散列排布,因此每當發生hash衝突的時候,就把這些須要考慮的因素都考慮進去,由於下一次發生hash衝突還不知道在何時呢,避免了這些髒數據沒法回收。另一點我我的以爲也多是由於get()方法咱們在前面提到過,它在獲取數據的時候就比set()方便,這裏這樣操做以後,會讓get()方法少處理一些邏輯。可是這是我我的的猜想,不必定準確。

 這個方法最終會返回咱們修改的那個位置的下一個位置,若是沒有數據被修改,默認返回咱們在上一步交換的那個位置的下一個位置。

 下面進入到cleanSomeSlots(int i, int n)方法的源碼中:

private boolean cleanSomeSlots(int i, int n) {
    boolean removed = false;
    Entry[] tab = table;
    int len = tab.length;
    do {
        i = nextIndex(i, len);
        Entry e = tab[i];
        if (e != null && e.get() == null) {
            n = len;
            removed = true;
            i = expungeStaleEntry(i);
        }
    } while ( (n >>>= 1) != 0);
    return removed;
}
複製代碼

 從這個方法的註釋來看,這個方法使用啓發式掃描某些單元來檢查陳舊條目當添加了新元素或者刪除另外一箇舊元素會調用這個方法。具體執行仍然是經過遍從來查看當前位置的Entrykey(ThreadLocal)是否爲空,若是爲空則仍然是調用咱們剛纔分析的expungeStaleEntry(i)來執行刪除元素的操做。
 須要注意的是,這裏的循環條件使用到了無符號右移的操做,這裏若是數組的長度爲16,那麼無符號右移的操做可以使得循環執行4次(這裏是do...while...循環,一開始就會執行一次),15的二進制數爲1111,無符號右移一位分別是0111,0011,0001,0000也就是15,7,3,1的是否會分別執行一次。

 而所謂的啓發式掃描,則是由於一旦發現有key爲空的狀況,則會重置n的值,致使循環的次數增長。而最壞的條件則是須要把整個數組都掃描一遍,因此註釋中也說這個方法可能會致使某些時候set(T)數據時間複雜度爲O(n)注意這是我我的的理解,我沒有找到啓發式掃面的具體含義,根據源碼的執行流程產生的這樣的理解,也不知道是否正確。

 上面所說的都是咱們指望可以在key == null的這個位置的後面找到咱們要插入/修改數據的那個key,可是其實不少時候咱們並不能知道,緣由也是由於不多會出現hash衝突,從上面的執行流程來看,屢次用到了遍歷,這也會致使時間複雜度較高,因此上面的方法應該儘量少的被使用到。

 那麼接下來就是若是沒有找到咱們要設置/修改的那個key,咱們就把當前位置的Entry移除,而後把如今新的數據設置上去,對應源碼中的以下操做:

// If key not found, put new entry in stale slot
    tab[staleSlot].value = null;
    tab[staleSlot] = new Entry(key, value);
複製代碼

replaceStaleEntry方法一開始就會從當前這個key爲空的位置開始向前查找這個位置以前還有沒有key爲空的位置,若是找到了這個位置,那麼就從這個位置開始清理key爲空的數據,對應源碼中的以下操做:

// If there are any other stale entries in run, expunge them
if (slotToExpunge != staleSlot)
    cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
複製代碼

咱們須要注意的是,數組會根據狀況進行擴容的操做,因此不會存在數據中的數據被存滿的狀況,因此說以前的遍歷確定會遇到某一個位置數據爲空而後停下來,不會存在死循環的狀況。

 至此,當咱們要設置/修改的位置上的Entrykey爲空的狀況就判斷完了。

  1. 程序執行到此處,說明當前數組中沒有找到要設置/修改的Entry,那麼就建立一個新的Entry保存keyvalue,
  2. 最後,若是咱們沒有清理數據,而且當前已經存在的數據大於以前咱們設置的閾值,那麼就進行rehash()的操做。
if (!cleanSomeSlots(i, sz) && sz >= threshold)
    rehash();
複製代碼
private void rehash() {
    expungeStaleEntries();

    // Use lower threshold for doubling to avoid hysteresis
    if (size >= threshold - threshold / 4)
        resize();
}
複製代碼

 能夠看到,這裏首先進行了expungeStaleEntries()方法,源碼以下:

private void expungeStaleEntries() {
    Entry[] tab = table;
    int len = tab.length;
    for (int j = 0; j < len; j++) {
        Entry e = tab[j];
        if (e != null && e.get() == null)
            expungeStaleEntry(j);
    }
}
複製代碼

 在這個方法中經過遍歷整個數組來清理那些key == null的元素。

 清理完數據以後,判斷當前已經存在的數據是否大於等於threshold - threshold / 4這個閾值,若是大於等於這個值,則調用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;
}
複製代碼

 能夠看到,擴容的方法其實很簡單,主要作了以下操做:

  1. 設置新數組的長度爲原來數組長度的2倍,因爲一開始的數組長度爲16,因此每次擴容都是能夠保證數組的長度是2的n次冪的。
  2. 遍歷原來的數組,判斷每一個位置的Entrykey(ThreadLocal)是否爲空,若是爲空,則設置其中的value也爲空來幫助GC.
  3. 若是不爲空,則經過threadLocalHashCode計算位置,若是位置有衝突,則循環計算下一個能夠插入的位置,以後將數據保存進去。
  4. 設置擴容的閾值,當前已經填充的數據量,數組變量指向新的數組。擴容完成。

 至此,set(T)方法就分析完了,這個方法分析完成後,能夠發現其實裏面的大部分方法都分析完了。

T get()

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();
}
複製代碼

 能夠看到,仍然是首先獲取當前線程,而後獲取到當前線程的ThreadLocalMap,若是獲取到的ThreadLocalMap不爲空,則調用map.getEntry(this)獲取數據最後返回,其它狀況下調用setInitialValue()方法。

map.getEntry(this)

 首先看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);
}
複製代碼

 邏輯比較簡單,首先仍然是經過threadLocalHashCode值計算位置,計算出來以後,若是這個位置上的ThreadLocalMap.Entry不爲空而且key也相同,那就直接返回這個Entry。不然調用getEntryAfterMiss(ThreadLocal<?> key, int i, Entry 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;
}
複製代碼

 邏輯也很簡單,就是經過循環判斷數組中的Entrykey有沒有和須要的同樣的,若是有就拿出來返回,沒有就返回null。另外在遍歷的時候若是發現有key == null的狀況,仍然會調用expungeStaleEntry(i)方法進行清理。

setInitialValue()

 若是獲取到的ThreadLocalMap爲空或者ThreadLocalMap中沒有保存當前ThreadLocal對應的值,那麼會調用這個方法,源碼以下:

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;
}
複製代碼

 能夠看到,這個方法首先調用了initialValue()獲取初始值,而後就和set(T)方法執行的邏輯同樣了。再也不贅述。

remove()方法

remove()方法的執行邏輯也比較簡單,仍然是獲取ThreadLocalMap,而後調用它裏面的remove(ThreadLocal<?> t)方法刪除數據,源碼以下:

public void remove() {
    ThreadLocalMap m = getMap(Thread.currentThread());
    if (m != null)
    m.remove(this);
}
複製代碼
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)]) {
        if (e.get() == key) {
            e.clear();
            expungeStaleEntry(i);
            return;
        }
    }
}
複製代碼

 能夠看到,仍然是經過threadLocalHashCode計算位置,而後遍歷key是否相等,若是相等則調用clear()清理對象,以後調用expungeStaleEntry(i)來清理位置。

 須要注意的是,ThreadLocalMap.Entry是繼承自WeakReference,上面的clear()方法內部會調用本地方法clearReferent()方法來清理引用,這個方法的註釋以下: 禁止直接訪問參照對象,而且在安全的狀況下清理對象塊而且將參照對象設置爲null

 至此,關於ThreadLocal的源碼就學習完了。

相關文章
相關標籤/搜索