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回收。

posted on 2017-10-14 23:25  游不动の鱼  阅读(1383)  评论(0编辑  收藏  举报