该文章属于《Android Handler机制之》系列文章,若是想了解更多,请点击 《Android Handler机制之总目录》java
要想了解Android 的Handle机制,咱们首先要了解ThreadLocal,根据字面意思咱们都能猜出个大概。就是线程本地变量。那么咱们把变量存储在本地有什么好处呢?其中的原理又是什么呢?下面咱们就一块儿来讨论一下ThreadLocal的使用与原理。数组
该类提供线程局部变量。这些变量不一样于它们的正常变量,即每个线程访问自身的局部变量时,都有它本身的,独立初始化的副本。该变量一般是与线程关联的私有静态字段,列如用于ID或事物ID。你们看了介绍后,有可能仍是不了解其主要的主要做用,简单的画个图帮助你们理解。bash
从图上能够看出,经过ThreadLocal,每一个线程都能获取本身线程内部的私有变量,有可能你们以为无图无真相,“你一我的在那里神吹,我怎么知道你说的对仍是不对呢?”,下面咱们经过具体的例子详细的介绍,来看下面的代码。ide
class ThreadLocalTest {
//会出现内存泄漏的问题,下文会描述
private static ThreadLocal<String> mThreadLocal = new ThreadLocal<>();
public static void main(String[] args) {
mThreadLocal.set("线程main");
new Thread(new A()).start();
new Thread(new B()).start();
System.out.println(mThreadLocal.get());
}
static class A implements Runnable {
@Override
public void run() {
mThreadLocal.set("线程A");
System.out.println(mThreadLocal.get());
}
}
static class B implements Runnable {
@Override
public void run() {
mThreadLocal.set("线程B");
System.out.println(mThreadLocal.get());
}
}
}
复制代码
在上诉代码中,咱们在主线程中设置mThreadLocal的值为"线程main",在线程A中设置为”线程A“,在线程B中设置为”线程B",运行程序打印结果以下图所示:post
main
线程A
线程B
复制代码
从上面结果能够看出,虽然是在不一样的线程中访问的同一个变量mThreadLocal,可是他们经过ThreadLocl获取到的值倒是不同的。也就验证了上面咱们所画的图是正确的了,那么如今,咱们已经知道了ThreadLocal的用法,那么咱们如今来看看其中的内部原理。ui
为了帮助你们快速的知晓ThreadLocal原理,这里我将ThreadLocal的原理用下图表示出来了:this
在上图中咱们能够发现,整个ThreadLocal的使用都涉及到线程中ThreadLocalMap
,虽然咱们在外部调用的是ThreadLocal.set(value)方法,但本质是经过线程中的ThreadLocalMap中的set(key,value)方法
,那么经过该状况咱们大体也能猜出get方法也是经过ThreadLocalMap。那么接下来咱们一块儿来看看ThreadLocal中set与get方法的具体实现与ThreadLocalMap的具体结构。spa
在使用ThreadLocal时,咱们会调用ThreadLocal的set(T value)方法对线程中的私有变量设置,咱们来查看ThreadLocal的set方法线程
public void set(T value) {
Thread t = Thread.currentThread();//获取当前线程
ThreadLocalMap map = getMap(t);//拿到线程的LocalMap
if (map != null)
map.set(this, value);//设值 key->当前ThreadLocal对象。value->为当前赋的值
else
createMap(t, value);//建立新的ThreadLocalMap并设值
}
复制代码
当调用set(T value) 方法时,方法内部会获取当前线程中的ThreadLocalMap,获取后进行判断,若是不为空,就调用ThreadLocalMap的set方法(其中key为当前ThreadLocal对象,value为当前赋的值)。反之,让当前线程建立新的ThreadLocalMap并设值,其中getMap()与createMap()方法具体代码以下:翻译
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
复制代码
简简单单的经过ThreadLocalMap的set()方法,咱们已经大体了解了。ThreadLocal为何能操做线程内的私有数据了,ThreadLocal中全部的数据操做都与线程中的ThreadLocalMap有关,同时那咱们接下来看看ThreadLocalMap相关代码。
ThreadLocalMap是ThreadLocal中的一个静态内部类,官方的注释写的很全面,这里我大概的翻译了一下,ThreadLocalMap是为了维护线程私有值建立的自定义哈希映射。其中线程的私有数据都是很是大且使用寿命长的数据(其实想想,为何要存储这些数据呢,第一是为了把经常使用的数据放入线程中提升了访问的速度,第二是若是数据是很是大的,避免了该数据频繁的建立,不只解决了存储空间的问题,也减小了没必要要的IO消耗)。
ThreadLocalMap 具体代码以下:
static class ThreadLocalMap {
//存储的数据为Entry,且key为弱引用
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
//table初始容量
private static final int INITIAL_CAPACITY = 16;
//table 用于存储数据
private Entry[] table;
//负载因子,用于数组容量扩容
private int threshold; // Default to 0
//负载因子,默认状况下为当前数组长度的2/3
private void setThreshold(int len) {
threshold = len * 2 / 3;
}
//第一次放入Entry数据时,初始化数组长度,定义扩容阀值,
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
table = new Entry[INITIAL_CAPACITY];//初始化数组长度为16
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
table[i] = new Entry(firstKey, firstValue);
size = 1;
setThreshold(INITIAL_CAPACITY);//阀值为当前数组默认长度的2/3
}
复制代码
从代码中能够看出,虽然官方申明为ThreadLocalMap是一个哈希表,可是它与咱们传统认识的HashMap等哈希表内部结构是不同的。ThreadLocalMap内部仅仅维护了Entry[] table,数组。其中Entry实体中对应的key为弱引用(下文会将为何会用弱引用),在第一次放入数据时,会初始化数组长度(为16),定义数组扩容阀值(当前默认数组长度的2/3)。
private void set(ThreadLocal<?> key, Object value) {
//根据哈希值计算位置
Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1);
//判断当前位置是否有数据,若是key值相同,就替换,若是不一样则找空位放数据。
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {//获取下一个位置的数据
ThreadLocal<?> k = e.get();
//判断key值相同否,若是是直接覆盖 (第一种状况)
if (k == key) {
e.value = value;
return;
}
//若是当前Entry对象对应Key值为null,则清空全部Key为null的数据(第二种状况)
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
//以上状况都不知足,直接添加(第三种状况)
tab[i] = new Entry(key, value);
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)//若是当前数组到达阀值,那么就进行扩容。
rehash();
}
复制代码
直接经过代码理解比较困难,这里直接将set方法分为了三个步骤,下面咱们咱们就分别对这个三个步骤,分别经过图与代码的方式讲解。
若是当前数组中,若是当前位置对应的Entry的key值与新添加的Entry的key值相同,直接进行覆盖操做。具体状况以下图所示
若是当前数组中。存在key值相同的状况,ThreadLocal内部操做是直接覆盖的。这种状况就不过多的介绍了。
第二种状况相对来讲比较复杂,这里先给图,而后会根据具体代码来说解。
从图中咱们能够看出来。当咱们添加新Entry(key=19,value =200,index = 3)时,数组中已经存在旧Entry(key =null,value = 19),当出现这种状况是,方法内部会将新Entry的值所有赋值到旧Entry中,同时会将全部数组中key为null的Entry所有置为null(图中大黄色数据)。在源码中,当新Entry对应位置存在数据,且key为null的状况下,会走replaceStaleEntry
方法。具体代码以下:
private void replaceStaleEntry(ThreadLocal<?> key, Object value,
int staleSlot) {
Entry[] tab = table;
int len = tab.length;
Entry e;
//记录当前要清除的位置
int slotToExpunge = staleSlot;
//往前找,找到第一个过时的Entry(key为空)
for (int i = prevIndex(staleSlot, len);
(e = tab[i]) != null;
i = prevIndex(i, len))
if (e.get() == null)//判断引用是否为空,若是为空,擦除的位置为第一个过时的Entry的位置
slotToExpunge = i;
//日后找,找到最后一个过时的Entry(key为空),
for (int i = nextIndex(staleSlot, len);//这里要注意得到位置有可能为0,
(e = tab[i]) != null;
i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();
//在日后找的时候,若是获取key值相同的。那么就从新赋值。
if (k == key) {
//赋值到以前传入的staleSlot对应的位置
e.value = value;
tab[i] = tab[staleSlot];
tab[staleSlot] = e;
//若是往前找的时候,没有过时的Entry,那么就记录当前的位置(日后找相同key的位置)
if (slotToExpunge == staleSlot)
slotToExpunge = i;
//那么就清除slotToExpunge位置下全部key为null的数据
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
return;
}
//若是往前找的时候,没有过时的Entry,且key =null那么就记录当前的位置(日后找key==null位置)
if (k == null && slotToExpunge == staleSlot)
slotToExpunge = i;
}
// 把当前key为null的对应的数据置为null,并建立新的Entry在该位置上
tab[staleSlot].value = null;
tab[staleSlot] = new Entry(key, value);
//若是日后找,没有过时的实体,
//且staleSlot以前能找到第一个过时的Entry(key为空),
//那么就清除slotToExpunge位置下全部key为null的数据
if (slotToExpunge != staleSlot)
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
}
复制代码
上面代码看起来比较繁杂,可是你们仔细梳理就会发现其实该方法,主要对四种状况进行了判断,具体状况以下图表所示:
咱们已经了解了replaceStaleEntry方法内部会清除key==null的数据,而其中具体的方法与expungeStaleEntry()方法与cleanSomeSlots()方法有关,因此接下来咱们来分析这两个方法。看看其的具体实现。
expungeStaleEntry ()方法
private int expungeStaleEntry(int staleSlot) {
Entry[] tab = table;
int len = tab.length;
// 将staleSlot位置下的数据置为null
tab[staleSlot].value = null;
tab[staleSlot] = null;
size--;
Entry e;
int i;
//日后找。
for (i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();
if (k == null) {//清除key为null的数据
e.value = null;
tab[i] = null;
size--;
} else {
//若是key不为null,可是该key对应的threadLocalHashCode发生变化,
//计算位置,并将元素放入新位置中。
int h = k.threadLocalHashCode & (len - 1);
if (h != i) {
tab[i] = null;
while (tab[h] != null)
h = nextIndex(h, len);
tab[h] = e;
}
}
}
return i;//返回最后一个tab[i]) != null的位置
}
复制代码
expungeStaleEntry()方法主要干了三件事,第一件,将staleSlot的位置对应的数据置为null,第二件,删除并删除此位置后对应相关联位置key = null的数据。第三件,若是若是key不为null,可是该key对应的threadLocalHashCode发生变化,计算变化后的位置,并将元素放入新位置中。
cleanSomeSlots()方法
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;//若是有过时的数据被删除,就返回true,反之false
}
复制代码
在了解了expungeStaleEntry()方法后,再来理解cleanSomeSlots()方法就很简单了。其中第一个参数表示开始扫描的位置,第二个参数是扫描的长度。从代码咱们明显的看出。就是简单的遍历删除全部位置下key==null的数据。
图上为了方便你们,理解清空上下数据的状况,我并无从新计算位置(但愿你们注意!!!)
看到这里,为了方便你们避免没必要要的查阅代码,我直接将代码贴出来了。代码以下。
tab[i] = new Entry(key, value);
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
复制代码
从上述代码其实,你们很明显的看出来,就是清除key==null的数据,判断当前数据的长度是否是到达阀值(默认没扩容前为INITIAL_CAPACITY *2/3,其中INITIAL_CAPACITY = 16),若是达到了从新计算数据的位置。关于rehash()方法,具体代码以下:
private void rehash() {
expungeStaleEntries();
// Use lower threshold for doubling to avoid hysteresis
if (size >= threshold - threshold / 4)
resize();
}
//清空全部key==null的数据
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的数据。新的数组长度为以前的两倍
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++;
}
}
}
//从新计算阀值(负载因子)为扩容以后的数组长度的2/3
setThreshold(newLen);
size = count;
table = newTab;
}
复制代码
rehash内部全部涉及到的方法,我都列举出来了。能够看出在添加数据的时候,会进行判断是否扩容操做,若是须要扩容,会清除全部的key==null的数据,(也就是调用expungeStaleEntries()方法,其中expungeStaleEntry()方法已经介绍了,就不过多描述),同时会从新计算数据中的位置。
在了解了ThreadLocal的set()方法以后,咱们看看怎么获取ThreadLocal中的数据,具体代码以下:
public T get() {
Thread t = Thread.currentThread();//获取当前线程
ThreadLocalMap map = getMap(t);//拿到线程中的Map
if (map != null) {
//根据key值(ThreadLocal)对象,获取存储的数据
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
//若是ThreadLocalMap为空,建立新的ThreadLocalMap
return setInitialValue();
}
复制代码
其实ThreadLocal的get方法其实很简单,就是获取当前线程中的ThreadLocalMap对象,若是没有则建立,若是有,则根据当前的 key(当前ThreadLocal对象),获取相应的数据。其中内部调用了ThreadLocalMap的getEntry()方法区获取数据,咱们继续查看getEntry()方法。
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);
}
复制代码
getEntry()方法内部也很简单,也只是根据当前key哈希后计算的位置,去找数组中对应位置是否有数据,若是有,直接将数据放回,若是没有,则调用getEntryAfterMiss()方法,咱们继续往下看 。
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)//若是key相同,直接返回
return e;
if (k == null)//若是key==null,清除当前位置下全部key=null的数据。
expungeStaleEntry(i);
else
i = nextIndex(i, len);
e = tab[i];
}
return null;//没有数据直接返回null
}
复制代码
从上述代码咱们能够知道,若是从数组中,获取的key==null的状况下,get方法内部也会调用expungeStaleEntry()方法,去清除当前位置全部key==null的数据,也就是说如今不论是调用ThreadLocal的set()仍是get()方法,都会去清除key==null的数据。
经过整个ThreadLocal机制的探索,我相信你们确定会有一个疑惑,为何ThreadLocalMap中采用是的是弱引用做为Key?
关于该问题,涉及到Java的回收机制。
在Java中判断一个对象究竟是不是须要回收,都跟引用相关。在Java中引用分为了4类。
经过该知识点的了解后,咱们再来了解为何ThreadLocal不能使用强引用,若是key使用强引用,那么当引用ThreadLocal的对象被回收了,但ThreadLocalMap中还持有ThreadLocal的强引用,若是没有手动删除,ThreadLocal不会被回收,致使内存泄漏。
当咱们知道了为何采用弱引用来做为ThreadLocalMap中的key的知识点后,这个时候又会引伸出另外一个问题不论是调用ThreadLocal的set()仍是get()方法,都会去清除key==null的数据。为毛咱们要去清除那些key==null的Entry呢?
为何清除key==null的Entry主要有如下两个缘由,具体以下所示:
经过以上分析,咱们能够了解在ThreadLocalMap的设计中其实已经考虑到上述两种状况,也加上了一些防御措施。(在调用ThreadLocal的get(),set(),remove()方法的时候都会清除线程ThreadLocalMap里全部key为null的Entry)
虽然ThreadLocal帮咱们考虑了内存泄漏的问题,为咱们加上了一些防御措施。可是在实际使用中,咱们仍是须要注意避免如下两种状况,下述两种状况仍然有可能会致使内存泄漏。
使用static修饰的ThreadLocal,延长了ThreadLocal的生命周期,可能致使的内存泄漏。具体缘由是在Java虚拟机在加载类的过程当中为静态变量分配内存。static变量的生命周期取决于类的生命周期,也就是说类被卸载时,静态变量才会被销毁并释放内存空间。而类的生命周期结束与下面三个条件相关。
其实理解起来也很简单,就是第一次调用了ThreadLocal设置数据后,就不在调用get()、set()、remove()方法。也就是说如今ThreadLocalMap中就只有一条数据。那么若是调用ThreadLocal的线程一直不结束的话,即便ThreadLocal已经被置为null(被GC回收),也一直存在一条强引用链:Thread Ref(当前线程引用) -> Thread -> ThreadLocalMap -> Entry -> value,致使数据没法回收,形成内存泄漏。