• Android源码学习(4) Handler之ThreadLocal


    线程的threadLocals

    Looper通过sThreadLocal来设置线程与Looper的对应关系,sThreadLocal是范型类ThreadLocal<Looper>的实例,其添加、移除元素的操作如下:

    public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }
    
    public void remove() {
        ThreadLocalMap m = getMap(Thread.currentThread());
        if (m != null)
            m.remove(this);
    }
    
    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }
    
    void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }

    从代码中可以看出,Looper对象实际上是被添加到当前线程的本地数据表中(t.threadLocals是ThreadLocalMap的实例)。ThreadLocalMap是通过hash表实现的,sThreadLocal被用作key。因此,查询任何线程的Looper时,都是以同一个变量(sThreadLocal)为key进行的。

    ThreadLocalMap

    ThreadLocalMap底层其实是数组实现的hash表,其类定义如下:

     1 static class ThreadLocalMap {
     2 
     3     /**
     4      * The entries in this hash map extend WeakReference, using
     5      * its main ref field as the key (which is always a
     6      * ThreadLocal object).  Note that null keys (i.e. entry.get()
     7      * == null) mean that the key is no longer referenced, so the
     8      * entry can be expunged from table.  Such entries are referred to
     9      * as "stale entries" in the code that follows.
    10      */
    11     static class Entry extends WeakReference<ThreadLocal> {
    12         /**
    13          * The value associated with this ThreadLocal.
    14          */
    15         Object value;
    16 
    17         Entry(ThreadLocal k, Object v) {
    18             super(k);
    19             value = v;
    20         }
    21     }
    22 
    23     /**
    24      * The initial capacity -- MUST be a power of two.
    25      */
    26     private static final int INITIAL_CAPACITY = 16;
    27 
    28     /**
    29      * The table, resized as necessary.
    30      * table.length MUST always be a power of two.
    31      */
    32     private Entry[] table;
    33 
    34     /**
    35      * The number of entries in the table.
    36      */
    37     private int size = 0;
    38 
    39     /**
    40      * The next size value at which to resize.
    41      */
    42     private int threshold; // Default to 0
    43     
    44 }

    Entry是静态内部类,定义了hash表的元素类型。该类继承WeakReference,实质上持了ThreadLocal的弱引用,value为实际的线程本地变量,在本例中为Looper对象。

    插入节点

     1 private void set(ThreadLocal key, Object value) {
     2 
     3     // We don't use a fast path as with get() because it is at
     4     // least as common to use set() to create new entries as
     5     // it is to replace existing ones, in which case, a fast
     6     // path would fail more often than not.
     7 
     8     Entry[] tab = table;
     9     int len = tab.length;
    10     int i = key.threadLocalHashCode & (len-1);
    11 
    12     for (Entry e = tab[i];
    13          e != null;
    14          e = tab[i = nextIndex(i, len)]) {
    15         ThreadLocal k = e.get();
    16 
    17         if (k == key) {
    18             e.value = value;
    19             return;
    20         }
    21 
    22         if (k == null) {
    23             replaceStaleEntry(key, value, i);
    24             return;
    25         }
    26     }
    27 
    28     tab[i] = new Entry(key, value);
    29     int sz = ++size;
    30     if (!cleanSomeSlots(i, sz) && sz >= threshold)
    31         rehash();
    32 }
    33 
    34 private static int nextIndex(int i, int len) {
    35     return ((i + 1 < len) ? i + 1 : 0);
    36 }

    第10行,可以看到hash函数是取threadLocalHashCode的低位,作为索引访问Entry数组。当存在冲突时(即当前位置已经被占了),则从当前位置开始查找下一个可用的位置。

    由于Entry是弱引用,所以其引用的ThreadLocal可能会被GC回收,因此在插入元素时,从hash函数计算的索引i开始查找:1) 若tab[i]为空,表明位置可用,则直接放入该位置;2) 若tab[i]的key与插入的key匹配,则更新该位置处的value;3) 若tab[i]的key为空,说明key已失效(被GC回收),则调用replaceStaleEntry将元素放入该位置。

    replaceStaleEntry具体流程如下:从staleSlot的下一位置开始查找待插入的key是否已经存在表中,若是,则更新其value值并将其与staleSlot位置处的元素交换;否则,直接将其放置在staleSlot处。replaceStaleEntry的代码如下:

      1 private void replaceStaleEntry(ThreadLocal key, Object value,
      2                                int staleSlot) {
      3     Entry[] tab = table;
      4     int len = tab.length;
      5     Entry e;
      6 
      7     // Back up to check for prior stale entry in current run.
      8     // We clean out whole runs at a time to avoid continual
      9     // incremental rehashing due to garbage collector freeing
     10     // up refs in bunches (i.e., whenever the collector runs).
     11     int slotToExpunge = staleSlot;
     12     for (int i = prevIndex(staleSlot, len);
     13          (e = tab[i]) != null;
     14          i = prevIndex(i, len))
     15         if (e.get() == null)
     16             slotToExpunge = i;
     17 
     18     // Find either the key or trailing null slot of run, whichever
     19     // occurs first
     20     for (int i = nextIndex(staleSlot, len);
     21          (e = tab[i]) != null;
     22          i = nextIndex(i, len)) {
     23         ThreadLocal k = e.get();
     24 
     25         // If we find key, then we need to swap it
     26         // with the stale entry to maintain hash table order.
     27         // The newly stale slot, or any other stale slot
     28         // encountered above it, can then be sent to expungeStaleEntry
     29         // to remove or rehash all of the other entries in run.
     30         if (k == key) {
     31             e.value = value;
     32 
     33             tab[i] = tab[staleSlot];
     34             tab[staleSlot] = e;
     35 
     36             // Start expunge at preceding stale entry if it exists
     37             if (slotToExpunge == staleSlot)
     38                 slotToExpunge = i;
     39             cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
     40             return;
     41         }
     42 
     43         // If we didn't find stale entry on backward scan, the
     44         // first stale entry seen while scanning for key is the
     45         // first still present in the run.
     46         if (k == null && slotToExpunge == staleSlot)
     47             slotToExpunge = i;
     48     }
     49 
     50     // If key not found, put new entry in stale slot
     51     tab[staleSlot].value = null;
     52     tab[staleSlot] = new Entry(key, value);
     53 
     54     // If there are any other stale entries in run, expunge them
     55     if (slotToExpunge != staleSlot)
     56         cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
     57 }

    由于某些ThreadLocal可能已经被GC回收,所以replaceStaleEntry会调用expungeStaleEntry和cleanSomeSlots清理已经被GC回收的元素,并将其value置空,防止内存泄漏。

    expungeStaleEntry从指定位置staleSlot处开始清理失效元素,expungeStaleEntry的具体流程如下:先清理staleSlot位置处的元素,然后从staleSlot的下一位置开始查找,若遇到失效元素,则将其清理;若遇到合法元素,则对其进行rehash,调整其位置;若遇到空值,则循环退出。expungeStaleEntry的代码如下:

     1 private int expungeStaleEntry(int staleSlot) {
     2     Entry[] tab = table;
     3     int len = tab.length;
     4 
     5     // expunge entry at staleSlot
     6     tab[staleSlot].value = null;
     7     tab[staleSlot] = null;
     8     size--;
     9 
    10     // Rehash until we encounter null
    11     Entry e;
    12     int i;
    13     for (i = nextIndex(staleSlot, len);
    14          (e = tab[i]) != null;
    15          i = nextIndex(i, len)) {
    16         ThreadLocal k = e.get();
    17         if (k == null) {
    18             e.value = null;
    19             tab[i] = null;
    20             size--;
    21         } else {
    22             int h = k.threadLocalHashCode & (len - 1);
    23             if (h != i) {
    24                 tab[i] = null;
    25 
    26                 // Unlike Knuth 6.4 Algorithm R, we must scan until
    27                 // null because multiple entries could have been stale.
    28                 while (tab[h] != null)
    29                     h = nextIndex(h, len);
    30                 tab[h] = e;
    31             }
    32         }
    33     }
    34     return i;
    35 }

    当清理掉一个元素,需要对其后面元素进行rehash的原因跟解决冲突的方式有关,设想hash表中存在冲突:

    ...,<key1(hash1), value1>, <key2(hash1), value2>,...(即key1和key2的hash值相同)

    此时,若插入<key3(hash2), value3>,其hash计算的目标位置被<key2(hash1), value2>占了,于是往后寻找可用位置,hash表可能变为:

    ..., <key1(hash1), value1>, <key2(hash1), value2>, <key3(hash2), value3>, ...

    此时,若<key2(hash1), value2>被清理,显然<key3(hash2), value3>应该往前移(即通过rehash调整位置),否则若以key3查找hash表,将会找不到key3

    cleanSomeSlots从指定位置i开始,以log2(n)为窗口宽度,检查并清理失效元素。cleanSomeSlots的实现代码如下:

     1 private boolean cleanSomeSlots(int i, int n) {
     2     boolean removed = false;
     3     Entry[] tab = table;
     4     int len = tab.length;
     5     do {
     6         i = nextIndex(i, len);
     7         Entry e = tab[i];
     8         if (e != null && e.get() == null) {
     9             n = len;
    10             removed = true;
    11             i = expungeStaleEntry(i);
    12         }
    13     } while ( (n >>>= 1) != 0);
    14     return removed;
    15 }

    第9行,如果检查到失效元素,则n会被重新赋值为len,所以该函数有可能把整个hash表的失效元素都清理掉。

    删除节点

    删除节点比较简单,代码如下:

     1 private void remove(ThreadLocal key) {
     2     Entry[] tab = table;
     3     int len = tab.length;
     4     int i = key.threadLocalHashCode & (len-1);
     5     for (Entry e = tab[i];
     6          e != null;
     7          e = tab[i = nextIndex(i, len)]) {
     8         if (e.get() == key) {
     9             e.clear();
    10             expungeStaleEntry(i);
    11             return;
    12         }
    13     }
    14 }

    rehash

    最后,我们来看下resh操作。具体流程如下:首先,清理掉所有的失效节点;若清理之后,表的大小还是超过了扩容的阈值,则进行resize操作将hash表的数组尺寸扩大一倍。rehash代码如下:

     1 private void rehash() {
     2     expungeStaleEntries();
     3 
     4     // Use lower threshold for doubling to avoid hysteresis
     5     if (size >= threshold - threshold / 4)
     6         resize();
     7 }
     8 
     9 private void resize() {
    10     Entry[] oldTab = table;
    11     int oldLen = oldTab.length;
    12     int newLen = oldLen * 2;
    13     Entry[] newTab = new Entry[newLen];
    14     int count = 0;
    15 
    16     for (int j = 0; j < oldLen; ++j) {
    17         Entry e = oldTab[j];
    18         if (e != null) {
    19             ThreadLocal k = e.get();
    20             if (k == null) {
    21                 e.value = null; // Help the GC
    22             } else {
    23                 int h = k.threadLocalHashCode & (newLen - 1);
    24                 while (newTab[h] != null)
    25                     h = nextIndex(h, newLen);
    26                 newTab[h] = e;
    27                 count++;
    28             }
    29         }
    30     }
    31 
    32     setThreshold(newLen);
    33     size = count;
    34     table = newTab;
    35 }
    36 
    37 private void expungeStaleEntries() {
    38     Entry[] tab = table;
    39     int len = tab.length;
    40     for (int j = 0; j < len; j++) {
    41         Entry e = tab[j];
    42         if (e != null && e.get() == null)
    43             expungeStaleEntry(j);
    44     }
    45 }

    总结

    ThreadLocalMap持了ThreadLocal的弱引用,而value值是强引用,显然这可能导致value临时泄漏。比如我们以线程池(ExecutorService)中线程构建Looper,当调用Looper的quit或者quitSafely退出Looper循环,此时因为线程来自于线程池,因此线程仍然会存活,加上Looper.sThreadLocal是静态变量,加入线程的threadLocals中的Looper显然是无法被清理,因而无法被GC回收。

  • 相关阅读:
    对position的认知观
    对于布局的见解
    Java中的多态
    继承中类型的转换
    继承中方法的覆盖
    继承条件下的构造方法调用
    Java函数的联级调用
    关于java中String的用法
    凯撒密码
    检查java 中有多少个构造函数
  • 原文地址:https://www.cnblogs.com/moderate-fish/p/7658926.html
Copyright © 2020-2023  润新知