1.ThreadLocal概述
本文源码基于android 27。
1.1 简介
ThreadLocal
提供了线程内的局部变量,这个局部变量是只存在于当前线程的。同时,它是独立于其他线程,即其他线程无法访问。这样就不会存在线程安全的问题了。
1.2 常用方法
public void set(T value) //设置当前线程中变量的副本
public T get() //获取在当前线程中保存的变量副本
public void remove() //移除当前线程中变量的副本
2.ThreadLocal源码分析
下面逐一对这三个方法的源码进行分析。
2.1 ThreadLocal的set()源码分析
直接看源码吧~
2.1.1 ThreadLocal的set()
public void set(T value) {
//获取当前线程
Thread t = Thread.currentThread();
//获取一个map
ThreadLocalMap map = getMap(t);
if (map != null)
//map不为null的话,以ThreadLocal.this为key,保存值
map.set(this, value);
else
//map为null,则直接创建map
//注意,这里传的是当前线程t
createMap(t, value);
}
set()
方法中就是先获取一个ThreadLocalMap
对象,如果这个ThreadLocalMap
不为null
的话就直接把数据保存到这个map
中,否则的话先创建map
出来。
所以,ThreadLocal
的数据本质是保存到一个map
中。需要注意,这个map
是以ThreadLocal.this
为key
来保存值的。
然后我们来看下这里面调用到的三个方法:
getMap(t)
createMap(t, value)
map.set(this, value)
2.1.2 ThreadLocal的getMap()
ThreadLocalMap getMap(Thread t) {
//返回当前线程t中的一个成员变量threadLocals
return t.threadLocals;
}
public class Thread implements Runnable {
//...
ThreadLocal.ThreadLocalMap threadLocals = null;
//...
}
可以看到,这个map
实际上是Thread
的一个成员属性。即map
跟线程绑定在一起了。这就可以解释了为何ThreadLocal
只是线程的局部变量了。
2.1.3 ThreadLocal的createMap()
void createMap(Thread t, T firstValue) {
//还是以ThreadLocal.this为key来保存值
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
就是new一个ThreadLocalMap
出来。
我们来看看这个ThreadLocalMap
是怎么样的。
2.1.4 ThreadLocal的ThreadLocal类
ThreadLocalMap
是ThreadLocal
的内部类。
static class ThreadLocalMap {
//Entry继承自弱引用
//GC时一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。
//即回收后,ThreadLocal这个key值就会变成null
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value;
//使用ThreadLocal作为key
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
//初始容量为16
private static final int INITIAL_CAPACITY = 16;
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
//创建一个容量为16的数组
table = new Entry[INITIAL_CAPACITY];
//根据key的哈希值算出索引位置
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
//创建一个Entry,并将其放入数组中
table[i] = new Entry(firstKey, firstValue);
//存放元素的个数设为1
size = 1;
//调整阈值的大小,这个是用来判断扩容的
setThreshold(INITIAL_CAPACITY);
}
}
需要注意的是,ThreadLocalMap
里面的Entry
继承了弱引用
,这个很重要,后面的一系列操作都是基于这个弱引用去实现的。那么为什么要使用弱引用呢?这是因为使用弱引用能够减少内存的使用。我们知道,弱引用很容易给回收,这样就能够让ThreadLocalMap
保持尽量的小。
同时,也可以看到,这个ThreadLocalMap
跟HashMap
这些不一样,没有去实现Map
接口。ThreadLocalMap
内部是由数组去实现的,并且其key
只能是ThreadLocal
类型。
2.1.5 ThreadLocalMap的set()
再来看下ThreadLocalMap
的set()
方法:
private void set(ThreadLocal<?> key, Object value) {
Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1);
//这里是使用nextIndex,即索引加1来解决哈希碰撞的
//跟HashMap不同,HashMap是使用链表来解决
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();
//如果Entry已存在相同的key,覆盖掉旧值
if (k == key) {
e.value = value;
return;
}
//如果k为null,则替换过期的Entry
//这是因为Entry继承了弱引用后,GC后就会出现k为null的情况
if (k == null) {
//替换过期的Entry
replaceStaleEntry(key, value, i);
return;
}
}
//如果在数组中都没找到已存在的key或者null值,则新建一个加入到数组中
tab[i] = new Entry(key, value);
int sz = ++size;
//由于弱引用,key值可能为null,因此先要清理掉这些没用的Entry,再去判断数组的大小有没超过阈值
//如果不用清除Entry并且达到阀值,那么就执行扩容操作
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();//重新调整位置,如果超过阀值,就扩容
}
这里的set()
方法,实际就是对以下三种情况做处理:
map
中已存在相同的key
值,则直接覆盖掉旧的值;- 如果根据哈希后的索引位置在map中找到一个
key
值为null
的Entry
,则替换掉这个无用的Entry
,具体操作看下面的分析;- 以上两种情况都没,则直接新建
Entry
添加到map
中,同时执行清除没用的Entry
以及考虑是否要扩容。
2.1.6 ThreadLocalMap的replaceStaleEntry()
replaceStaleEntry()
的作用是替换掉过期没用的Entry
,看下面的分析:
//key:set的key值
//value:set的value值
//staleSlot:过期Entry的索引位置
private void replaceStaleEntry(ThreadLocal<?> key, Object value,
int staleSlot) {
Entry[] tab = table;
int len = tab.length;
Entry e;
//擦除位置指向过期Entry的索引位置
int slotToExpunge = staleSlot;
//数组往前找到第一个不为null的Entry
for (int i = prevIndex(staleSlot, len);
(e = tab[i]) != null;
i = prevIndex(i, len))
if (e.get() == null)
//擦除位置指向找到的key为null的索引位置
slotToExpunge = i;
//从过期Entry的索引位置开始往后找
for (int i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();
//如果在遍历中找到跟要set的key值相等的,那么交换它们
//因为ThreadLocalMap使用了nextIndex来解决哈希碰撞,即要set的key值可能会出现在当前索引的后面
//因此,这个key存在的话,则应该找出来,擦除掉,并在当前索引中设置新的
//注意,当前索引位置tab[staleSlot]的key值为null
//这里采用了交换的方式去实现
if (k == key) {
//设置新值,并交换数据
e.value = value;
tab[i] = tab[staleSlot];
tab[staleSlot] = e;
//如果擦除位置指向过期Entry的索引位置
//那么修改为交换后的索引位置
if (slotToExpunge == staleSlot)
slotToExpunge = i;
//expungeStaleEntry:擦除从指定位置开始的一些过期数据,并重新调整位置
//cleanSomeSlots:清理一段过期的数据
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
return;
}
//找到下一个为null的key值
if (k == null && slotToExpunge == staleSlot)
//擦除位置指向新的
slotToExpunge = i;
}
//数组中没找到相同key的话,则直接新建一个放进数组当前索引位置中
tab[staleSlot].value = null;
tab[staleSlot] = new Entry(key, value);
//如果存在其他的过期数据,那么清理掉这一段的过期数据
if (slotToExpunge != staleSlot)
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
}
替换过期数据同样也可以分为两种情况:
- 由于哈希碰撞的关系,先从当前过期数据的索引位置往后找,如果能找到相同
key
的话,就设置新值,并交换它们的位置;- 如果上面的过程找不到相同的
key
,即原来的map
中没有这一key
值,那么直接新建一个键值对,放到当前过期数据的索引位置中。
这一期间会包含清理过期数据的操作。
下面来看下这两个方法:
expungeStaleEntry(slotToExpunge)
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len)
2.1.7 ThreadLocalMap的expungeStaleEntry()
expungeStaleEntry()
的作用是擦除从指定位置开始的一些过期数据,并重新调整位置。
private int expungeStaleEntry(int staleSlot) {
Entry[] tab = table;
int len = tab.length;
//擦除staleSlot指向的过期数据,
tab[staleSlot].value = null;
tab[staleSlot] = null;
size--;
// Rehash until we encounter null
Entry e;
int i;
//往后找,知道遇到null为止
//擦除k == null的数据
//重新调整k != null的位置
for (i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();
if (k == null) {
//擦除过期数据
e.value = null;
tab[i] = null;
size--;
} else {
//如果可以的话,重新调整位置,
//因为出现了擦除数据后留下的空坑
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;
}
expungeStaleEntry()
同样可以分为以下三步:
- 先擦除当前指定位置的数据;
- 然后往后遍历,如果遇到
key
值为null
的数据就擦除掉,并看下key
值不为null
的数据能不能挪下位置。因为哈希碰撞的关系,一些数据会存放到比较后的位置,如果前面出现空位,那么将它们往前移一下,可以提高一下查找速度。- 如果遇到
Entry
为null
,则退出循环。
2.1.7 ThreadLocalMap的expungeStaleEntry()
expungeStaleEntry()
的作用是清理一段过期的数据。
private boolean cleanSomeSlots(int i, int n) {
boolean removed = false;
Entry[] tab = table;
int len = tab.length;
do {
//往后加1的索引位置
i = nextIndex(i, len);
Entry e = tab[i];
//找到要擦除的数据
if (e != null && e.get() == null) {
n = len;
removed = true;
//expungeStaleEntry见上面的分析
i = expungeStaleEntry(i);
}
} while ( (n >>>= 1) != 0);//执行(log2 n)+1次循环
return removed;
}
while ( (n >>>= 1) != 0)
控制了循坏次数,所以有可能只扫描了这个数组的其中一段;当然也有可能整个都扫描了。这个要看要擦除的数据是否比较多了,因为找到要擦除的数据后,n
会重新被赋值为len
。
2.1.8 小结
至此,ThreadLocal.set()
所涉及到的都分析完毕了。
简单总结一下,在每个线程内部都有一个成员变量threadLocals
,这个是threadLocals
是ThreadLocal.ThreadLocalMap
类型,它能够保存以ThreadLocal
为key
的数据。因此,一个线程中是可以有多个不同的ThreadLocal
对象的。
2.2 ThreadLocal的get()源码分析
我们再来看下get()
方法:
2.2.1 ThreadLocal的get()
public T get() {
//获取当前线程
Thread t = Thread.currentThread();
//获取map
ThreadLocalMap map = getMap(t);
if (map != null) {
//以ThreadLocal.this为key获取Entry
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
//从Entry中获取value值返回
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
//如果map为null或Entry为null,会初始化一个null值返回
return setInitialValue();
}
通过get()
来获取值,如果map
中存在这个key
值就返回对应的value
值;否则就初始化一个null
值返回。
分别来看下以下这两个方法:
map.getEntry(this)
setInitialValue()
2.2.2 ThreadLocalMap的getEntry()
private Entry getEntry(ThreadLocal<?> key) {
int i = key.threadLocalHashCode & (table.length - 1);
Entry e = table[i];
//如果根据哈希后的索引位置能找到的话,直接返回
//否则调用getEntryAfterMiss继续找
if (e != null && e.get() == key)
return e;
else
return getEntryAfterMiss(key, i, e);
}
如果能根据哈希后的索引位置找到Entry
的话,就直接返回;否则的话就调用getEntryAfterMiss
继续找。
2.2.3 ThreadLocalMap的getEntryAfterMiss()
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
Entry[] tab = table;
int len = tab.length;
//同样是使用nextIndex来查找下一个,直到null为止
while (e != null) {
ThreadLocal<?> k = e.get();
if (k == key)
//找到返回
return e;
if (k == null)
//如果找到有null值,擦除一发
expungeStaleEntry(i);
else
//继续找下一个
i = nextIndex(i, len);
e = tab[i];
}
return null;
}
同样,由于哈希碰撞的关系,往后找。能找到就返回结果,否则就返回null
。
2.2.4 ThreadLocalMap的setInitialValue()
再来看下setInitialValue()
,就是初始化value
然后返回。
private T setInitialValue() {
//初始化Value值,为null
T value = initialValue();
//获取当前线程
Thread t = Thread.currentThread();
//获取map
ThreadLocalMap map = getMap(t);
//如果map不为null,就把null值设置进去
//如果map为null,先创建一个map,并且也把null值设置进去
if (map != null)
map.set(this, value);
else
createMap(t, value);
return value;
}
再来看下initialValue()
。
2.2.5 ThreadLocalMap的initialValue()
protected T initialValue() {
//返回null,
return null;
}
就是返回一个默认值:null
。需要注意的是,这个是protected
方法,如果需要修改这个默认的返回值,可以继承之后去重写。
2.2.6 小结
get()
方法很简单,就是从map
中找,能找到就返回结果,找不到就话就返回一个默认的值:null
。另外,我们可以通过重写initialValue()
去修改这个默认返回值。
2.3 ThreadLocal的remove()源码分析
如果我们不使用ThreadLocal
了,可以使用remove()
来移除掉。
来看下源码~
2.3.1 ThreadLocal的remove()
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
//如果Map不为null,从Map中移除以以ThreadLocal.this为key的键值对
if (m != null)
m.remove(this);
}
再来看下ThreadLocalMap
的remove()
。
2.3.2 ThreadLocalMap的remove()
private void remove(ThreadLocal<?> key) {
Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1);
//通过nextIndex来逐一遍历,如果找到就清除掉
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
if (e.get() == key) {
//清除key值
e.clear();
//替换过期的Entry
expungeStaleEntry(i);
return;
}
}
}
remove()
过程还是很简单。
2.3.3 其他
虽然在使用set()
和get()
时有概率会进行一些清理回收操作。但是还是建议在不使用时,手动调用一下remove()
方法,避免出现内存泄露。
3.其他的一些问题
3.1 ThreadLocal为什么会内存泄漏?
ThreadLocalMap
使用ThreadLocal
的弱引用作为key
,如果一个ThreadLocal
没有外部强引用来引用它,那么系统 GC 的时候,这个ThreadLocal
势必会被回收,这样一来,ThreadLocalMap
中就会出现key
为null
的Entry
,就没有办法访问这些key
为null
的Entry
的value
,如果当前线程再迟迟不结束的话,这些key
为null
的Entry
的value
就会一直存在一条强引用链:Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value
永远无法回收,造成内存泄漏。
推荐一篇文章:深入分析 ThreadLocal 内存泄漏问题
3.2 为什么ThreadLocal要使用弱引用?
官方文档的说法:
To help deal with very large and long-lived usages, the hash table entries use WeakReferences for keys.
为了应对非常大和长时间的用途,哈希表使用弱引用的 key。
补充一下:使用弱引用能够减少内存的使用。因为弱引用很容易给回收,这样就能够让ThreadLocalMap
保持尽量的小。
3.3 如何防止弱引用被回收从而找不到值?
可以使用static
来修饰ThreadLocal
,从而延长ThreadLocal
的生命周期。但是并不能保证不会内存泄漏。