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