今晚重新看了一下Android 消息传递的机制,其中发现了每一个Looper的对象放在了一个ThreadLocal对象里面,当时就非常的好奇,ThreadLocal这个类是怎么实现的。于是结合了Java 8的源代码和网上的资料对这个类有了一定的理解,在此记录一下,如果有错误之处,请各位指正!
本文参考资料:
1.深入分析 ThreadLocal 内存泄漏问题
2.【Java 并发】详解 ThreadLocal
3.周志明老师的《深入理解Java虚拟机》
其实,大家可以看看上面两篇文章,上面的两篇文章比我写的好。我这里写的都是我自己对ThreadLocal的理解!哈哈!
1.初识ThreadLocal
这里就不对ThreadLocal类的基本使用进行展开了,我们直接从源码入手,来理解ThreadLocal实现的原理。
(1).set方法
当我们在使用ThreadLocal类时,通常会使用set方法来给当前的线程设置一个变量。我们来看看set方法到底为我们做了什么。
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
set方法的代码非常短,至少比我之前看的其他代码短得多。
首先我们是获取的是当前的线程,然后调用getMap方法来获得TreadLocalMap对象,再来看看getMap方法获取的是什么:
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
哎呀,这个也太简单了吧。这里简单的介绍一下threadLocals表示的含义,threadLocals其实就是Thread类的一个成员变量,也就是说每个Thread对象里面都有一个这个变量,这样就能保证这个变量在线程之间是保持独立,线程之间不会相互的影响。
回到我们的set方法里面来,然后调用map的set方法来进行赋值。请注意这里,第一个参数传入的是this,也就是说是当前的ThreadLocal,而这个参数是干嘛的呢?就是key。也就是说,这里是ThreadLocal对象来作为key的。
map的set方法又在干嘛呢?这里我不解释,待会在讲解ThreadLocalMap类时在详细解释set方法的作用。
(2).get方法
在使用ThreadLocal类时,get方法也是不可避免的,通常我们调用get方法来获取在ThreadLocal里面保存的变量。
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
get方法也是非常的简单,最终还是到了ThreadLocalMap里面去了。看来不得不去ThreadLocalMap是一个什么东西了。
2.ThreadLocalMap
(1).成员变量
在理解ThreadLocalMap之前,我们还是来看看这类的里面有那些成员变量:
//默认的容量
private static final int INITIAL_CAPACITY = 16;
//Entry数组,用来保存每个键值对的
private Entry[] table;
//map的size
private int size = 0;
//阈值,用来扩容的
private int threshold; // Default to 0
这个成员变量也是非常的少,之前在看HashMap源代码的时候,那家伙!!!
(2).Entry
在理解代码之前,我们必须还说Entry这个东西:
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
Entry的封装也是非常简单,继承了WeakReference类,表示是一个弱引用。这里弱引用是什么东西呢?其实这个就能扯到JVM的GC那一块了,由于不是本文的重点,所以就不讲解(其实是自己太菜了!!!!)。这里就简单的介绍一下Java中的四种引用,摘抄自周志明老师的《深入理解Java虚拟机》。
1.强引用:在程序代码中普遍存在的,类似 Object obj = new Object()。这类的引用便是强应用。只要强引用存在,GC是永远不会回收该引用的对象。
2.软引用:用来描述一些有用但是并非必需的对象。对于软引用关联着的对象,在系统将要发生内存溢出异常之前,将会把这些对象列进回收对象范围之中,进行第二次的回收。如果这次回收还没有足够的内存的话,才会抛出内存溢出异常。在JDK 1.2之后,提供了SoftReference类来实现软引用。
3.弱引用:也是用来描述非必须的对象,但是他的强度比软引用更弱一些,被弱引用关联的对象只能生存到下一次GC发生之前。当GC工作时,无论当前内存是否足够,都会回收掉纸杯弱引用关联的对象。在JDK 1.2之后,提供了WeakReference类来实现弱引用。
4.虚引用:也称为幽灵引用或者幻影引用,它是最弱的一种引用。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的就是能在这个对象被GC回收时收到一个系统通知。在JDK 1.2之后,提供了PhantomReference类来实现虚引用。
回到Entry里面来,我们需要注意的是key被弱引用关联了。这就是因为这一点,出现了ThreadLocal的内存泄露问题。详细请看:深入分析 ThreadLocal 内存泄漏问题。
(3).set方法
准备的差不多了,我们开始研究ThreadLocalMap的set方法了。这里先贴出全部的代码,然后逐一的分析。
private void set(ThreadLocal<?> key, Object value) {
// We don't use a fast path as with get() because it is at
// least as common to use set() to create new entries as
// it is to replace existing ones, in which case, a fast
// path would fail more often than not.
Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1);
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();
if (k == key) {
e.value = value;
return;
}
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数组里面,而保存的index不是指定,而是通过key的hashCode来计算的。将一个key的hashCode通过一个hash函数得到一个index值,而这个值就是这个键值对在数组里面的位置。
但是理想是美好的,现实是残酷的,当两个不同的key而产生相同的index时,就出现了hash碰撞。这种情况下,通常来说有两种解决的办法:1.开放地址法;2.链地址法。由于这两种方法在数据结构中是比较重要的内容,大家应该都学过,所以这里就不解释这两种方法的区别。
在ThreadLocalMap里面使用的是开放地址法,也就是说当出现了哈希碰撞时,会从当前的位置往后找一个为null的位置来保存键值对。
接下来详细的分析一下set方法的代码,首先是:
int i = key.threadLocalHashCode & (len-1);
可能有些老哥在看到这段代码时,就有点懵逼了,这特么是什么鬼东西?如果我说这个其实就是取模运算,老哥们会不会what fuck?
我们详细的解释一下这段代码。当len为2的n次方时,key.threadLocalHashCode & (len-1)就相当于是一个取模运算;例如,当len为16时,上面的代码相当于就是对16取模。这个是为什么呢?因为16的二进制是:10000,当减1变成15时,二进制变为:1111。这样len - 1与任何一个数字进行与运算时,最终剩下来的都是那个数字的低4位,而低4位就是对16取模的结果。如果还不懂的话,看图:
如图,相当于是20%16的结果。但是,需要注意的是,len必须是2的n次方,这样才能保证-1之后,低位全为1。这个也是Map为什么是2倍扩容的原因之一。
通过上一步的hash操作,算是找到一个index来存储我们键值对,但是必须考虑到hash碰撞的情况。其中当hash碰撞了之后,是通过这个方法来获取下一个index位置:
i = nextIndex(i, len)
/**
* Increment i modulo len.
*/
private static int nextIndex(int i, int len) {
return ((i + 1 < len) ? i + 1 : 0);
}
当获取的inedx对应的数组中为null时,表示这个位置没有被占据。如果当前key已经在Entry数组中里面存在,直接替换值即可:
if (k == key) {
e.value = value;
return;
}
当时k为null这种情况怎么里面?这个就得引出之前需要注意的Entry中key为WeakReference关联,也就是说,在Entry数组里面,每个Entry对象的key可能随时都会被GC回收,从而导致k为null,由于Entry是一个对象,虽然Entry里面的key被回收了,但是key对应value并没有回收,从而导致这个value不可能再被get到。由于value是被一个强引用关联,除非value所在的Entry对象被回收,value才会被回收,由于这个Entry在数组中有一个强引用,所以除非收到将数组的相应位置置为nulll,否则这个强引用会一直存在。
由于以上原因,导致Entry对象不能回收,从而导致value内存泄露。
卧了个槽,怎么去分析内存了,跑题了跑题了。回到主题,根据上面的解释,我们知道了k为null是因为相应k被释放导致的,此时为了防止内存泄露,会去清理垃圾对象。如下:
if (k == null) {
//将旧的对象替换程新的Entry对象,并且清理垃圾对象
replaceStaleEntry(key, value, i);
return;
}
当然如果找到了空位置的话,就占了。
tab[i] = new Entry(key, value);
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
添加数据之后,必不可少的就是判断是否达到了扩容的条件。
这个就是整个set的过程。这里,简单的总结一下。
1.在set方法里面,将key的hashCode对len进行取模运算来获取index。这里需要注意的是len必须是2的n次方。
2.如果发生了哈希碰撞了,set方法采用的是开发地址方法来解决的。
3.在进行开放地址方法时,有可能会出现key被回收的情况,这里会可能会导致内存泄露的问题。官方的手段是,对key为null的Entry进行清理。
(3).getEntry方法
看完了set方法,我们再来看看get方法。在ThreadLocal的get方法里面,是通过调用getEntry来获取一个Entry的
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);
}
从这段代码里面,我们可以看出来,如果index能获取得到的Entry的key与想要找到的key是一样的话,那么直接就返回,否则的话,就通过开放地址法来循环遍历寻找。
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)
return e;
if (k == null)
expungeStaleEntry(i);
else
i = nextIndex(i, len);
e = tab[i];
}
return null;
}
同时我们发现, 在getEntryAfterMiss方法里面是通过循环遍历找一个Entry。
3.总结
这个ThreadLocal感觉也不是很难,可能是自己太菜了吧,很多的问题都没有发现。在这里对ThreadLocal做一个总结。
1. ThreadLocal实现线程的局部变量是通过Thread的一个ThreadLocalMap成员变量,因为每个线程对象都持有自己的ThreadLocalMap对象,所以线程之间不会有影响的。
2.ThreadLocalMap使用的存储结构与普通的Map结构非常相似,只是ThreadLocalMap使用的开放地址法来解决哈希碰撞的。
3.ThreadLocalMap的key使用的是WeakReference对象关联,所以会出现key为null,但是value不能被释放的情况。官方在每次的set和get方法里面,会对Entry进行清理。
关于内存泄露的问题,大家可以看看这篇文章:深入分析 ThreadLocal 内存泄漏问题