天天看點

Android ThreadLocal 源碼分析

1.為啥 說是Android ThreadLocal ,而不是java ThreadLocal,因為Android 對它進行了 優化.優化地方:記憶體複用,使用弱引用解決記憶體洩漏.而且他們處理方式也不同Java 使用類來包裹 key和value的.使用魔數0x61c88647, 計算得到的索引值偶數和奇數之間不斷切換.而Android 隻是在偶數索引index 存放key ,index+1來存放值.魔數為0x61c88647*2 得到的索引值都是偶數,非常适合它的處理方式.

2.從使用來分析源碼.

ThreadLocal<Object> local=new ThreadLocal<>();
           Object data= local.get();
           if (data==null){
               data=new Object();
            local.set(data);
           }
               //拿到對象來處理一下問題
            
        }
           

3.ThreadLocal對象的建立:  

public ThreadLocal() {}
 就一個空構造 ,那麼建立對象都做了什麼呢,那就看它成員屬性了.

  /** Weak reference to this thread local instance. */
    private final Reference<ThreadLocal<T>> reference
            = new WeakReference<ThreadLocal<T>>(this);
//弱引用持有它,有利于回收,防止記憶體洩漏.如果他為null時它所在的存放數組索引的地方将被設定為TOMBSTONE 對象,value所在的地方設定為null,不在持有它對象也有利于回收.下一個ThreadLocal 對象set時.如果找到數組存放的索引,而且在這個索引數組裡面的對象為TOMBSTONE将會被替換成這個.進而達到記憶體複用.
 /** Hash counter. */
    private static AtomicInteger hashCounter = new AtomicInteger(0);

    /**
     * Internal hash. We deliberately don't bother with #hashCode().
     * Hashes must be even. This ensures that the result of
     * (hash & (table.length - 1)) points to a key and not a value.
     *
     * We increment by Doug Lea's Magic Number(TM) (*2 since keys are in
     * every other bucket) to help prevent clustering.
     */
    private final int hash = hashCounter.getAndAdd(0x61c88647 * 2);
為啥使用0x61c88647  防止集中,為啥*2 因為key  所在的索引為偶數.第一次計算時hash 為零.也就是第一次建立ThreadLocal key 必然在0索引.然後系統已經使用了n次(hashCounter 為靜态...).
           

主線程存放的對象

Android ThreadLocal 源碼分析

其中Looper最為熟悉.

3.get()方法

public T get() {
        // Optimized for the fast path.
        Thread currentThread = Thread.currentThread();
//value 是從線程對象 localValues 成員屬性取出來的中,是以不同線程有不同value, 就是有不同的副本.
        Values values = values(currentThread);
        if (values != null) {
            Object[] table = values.table;
            int index = hash & values.mask;
// 嘗試從第一次計算hash 得到索引取值,如果key 等于 将執行getAfterMiss 方法.一般都存在第一次計算得到索引的地方
            if (this.reference == table[index]) {
                return (T) table[index + 1];
            }
        } else {
//初始化數組
            values = initializeValues(currentThread);
        }

        return (T) values.getAfterMiss(this);
    }

 Object getAfterMiss(ThreadLocal<?> key) {
            Object[] table = this.table;
            int index = key.hash & mask;

            // If the first slot is empty, the search is over.
            //如果第一次存放的索引都為null ,那麼必然沒有set過資料
            if (table[index] == null) {
                Object value = key.initialValue();//預設是null

                // If the table is still the same and the slot is still empty...
// 看到這裡挺懵圈的,為啥這樣判斷呢.在同一個線程是串行執行的,不應該table 發生變化才對(并發問題)
// 直到看 這句話The table changed during initialValue() 就突然明白,如果繼承重寫initialValue方法在裡面set 是不是有可能發生擴容,擴容時索引可能發生偏移.
                if (this.table == table && table[index] == null) {
                    table[index] = key.reference;
                    table[index + 1] = value;
                    size++;
                    //擴容或者檢查key 是否被回收
                    cleanUp();
                    return value;
                }

                // The table changed during initialValue().
// 當發生擴容時 就要重新周遊索引了
                put(key, value);
                return value;
            }

            // Keep track of first tombstone. That's where we want to go back
            // and add an entry if necessary.
            int firstTombstone = -1;

            // Continue search.
// 如果不為空 那就繼續周遊 而周遊的範圍永遠都在0-table.length-1 之間,而且必然是偶數
            for (index = next(index);; index = next(index)) {
                Object reference = table[index];
                if (reference == key.reference) {
                    return table[index + 1]; //找到就傳回
                }

                // If no entry was found...
                // 這裡的邏輯跟上面差不多的
                if (reference == null) {
                    Object value = key.initialValue();

                    // If the table is still the same...
                    if (this.table == table) {
                        // If we passed a tombstone and that slot still
                        // contains a tombstone...
                        if (firstTombstone > -1
                                && table[firstTombstone] == TOMBSTONE) {
//這裡記憶體複用
                            table[firstTombstone] = key.reference;
                            table[firstTombstone + 1] = value;
                            tombstones--;
                            size++;

                            // No need to clean up here. We aren't filling
                            // in a null slot.
                            return value;
                        }

                        // If this slot is still empty...
                        if (table[index] == null) {
                            table[index] = key.reference;
                            table[index + 1] = value;
                            size++;

                            cleanUp();
                            return value;
                        }
                    }

                    // The table changed during initialValue().
                    put(key, value);
                    return value;
                }

                if (firstTombstone == -1 && reference == TOMBSTONE) {
                    // Keep track of this tombstone so we can overwrite it.
// 為啥不把  table[firstTombstone] = key.reference...這些語句 放到這裡來執行呢.萬一後面還有 //reference ==key.reference就不就重複了嗎 
                    firstTombstone = index;
                }
            }
        }
           

4.cleanUp():擴容或者回收标記(設定TOMBSTONE)

private void cleanUp() {
// 檢查是否擴容
            if (rehash()) {
                // If we rehashed, we needn't clean up (clean up happens as
                // a side effect).
                return;
            }
          //數量為0 那就沒有必要進行回收标記了
            if (size == 0) {
                // No live entries == nothing to clean.
                return;
            }

            // Clean log(table.length) entries picking up where we left off
            // last time.
//這裡要從上一次的位置開始檢查,為什麼呢 因為周遊次數為log2Table.length ,不能完全周遊完,索引需要記錄上一次位置才能 完全周遊完.
            int index = clean;
            Object[] table = this.table;
            for (int counter = table.length; counter > 0; counter >>= 1,
                    index = next(index)) {
                Object k = table[index];

                if (k == TOMBSTONE || k == null) { //已經标記跳過
                    continue; // on to next entry
                }

                // The table can only contain null, tombstones and references.
                @SuppressWarnings("unchecked")
                Reference<ThreadLocal<?>> reference
                        = (Reference<ThreadLocal<?>>) k;
                if (reference.get() == null) {
                    // This thread local was reclaimed by the garbage collector.
                    table[index] = TOMBSTONE; //有利于回收
                    table[index + 1] = null; 
                    tombstones++;
                    size--;
                }
            }

            // Point cursor to next index.
            clean = index;//記錄
        }

      
        private boolean rehash() {
 
            if (tombstones + size < maximumLoad) {
                return false;
            }

            int capacity = table.length >> 1;

         
            int newCapacity = capacity;
           //當數量大于四分之一時 在擴容兩倍 .而當數量大于2分之1 getAndAdd() 才會獲得 0索引,後面擷取的索引會跟之前一樣(測試過).是以1/3 或者1/4 擴容都可以.也就是說在1/3 或者1/4之前擷取 索引值是不沖突 ,在set 時用周遊有點想不通.
            if (size > (capacity >> 1)) {
                // More than 1/2 filled w/ live entries.
                // Double size.
                newCapacity = capacity * 2;
            }

            Object[] oldTable = this.table;

            // Allocate new table.
            initializeTable(newCapacity);

            // We won't have any tombstones after this.
            this.tombstones = 0;

            // If we have no live entries, we can quit here.
            if (size == 0) {
                return true;
            }

            // Move over entries.
       //既然索引不沖突 向後或者向前周遊都沒有關系
            for (int i = oldTable.length - 2; i >= 0; i -= 2) {
                Object k = oldTable[i];
                if (k == null || k == TOMBSTONE) {
                    // Skip this entry.
                    continue;  //這裡回收标記已經不用管了,因為建立了新的數組,是以上面this.tombstones = 0
                }

                // The table can only contain null, tombstones and references.
                @SuppressWarnings("unchecked")
                Reference<ThreadLocal<?>> reference
                        = (Reference<ThreadLocal<?>>) k;
                ThreadLocal<?> key = reference.get();
                if (key != null) {
                    // Entry is still live. Move it over.
                    add(key, oldTable[i + 1]); // 找到位置添加進去就行了
                } else {
                    // The key was reclaimed.
                    size--;
                }
            }

            return true;
        }
           

 5.set(...) 方法:

直接貼vaules.put..
 void put(ThreadLocal<?> key, Object value) {
            cleanUp();//說過

          
            int firstTombstone = -1;
      //getAfter 部分代碼差不多 ,唯一比較不了解的是既然索引不沖突 ,是否直接用 key.hash & mask就得了?為啥還周遊?
            for (int index = key.hash & mask;; index = next(index)) {
                Object k = table[index];

                if (k == key.reference) {
                    // Replace existing entry.
                    table[index + 1] = value;
                    return;
                }

                if (k == null) {
                    if (firstTombstone == -1) {
                        // Fill in null slot.
                        table[index] = key.reference;
                        table[index + 1] = value;
                        size++;
                        return;
                    }

                    // Go back and replace first tombstone.
                    table[firstTombstone] = key.reference;
                    table[firstTombstone + 1] = value;
                    tombstones--;
                    size++;
                    return;
                }

                // Remember first tombstone.
                if (firstTombstone == -1 && k == TOMBSTONE) {
                    firstTombstone = index;
                }
            }
        }
           

ps.終于知道為啥要周遊了,因為 static AtomicInteger hashCounter 那麼 在另一個線程建立多個Threadlocal 對象,在傳回主線程建立就可能出現索引沖突.怪不得自己計算的索引值跟上面的圖不一樣.