1.ThreadLocal
ThreadLocal是一个线程内部的数据存储类,通过它可以在指定的线程中存储数据,数据存储以后,只有在指定的线程中才可以访问,其他线程无法获取。因此在多个线程执行时,为了互不干扰资源,可以使用ThreadLocal。
使用场景:
①某些数据是以线程为作用域且不同线程之间具有不同数据副本;
Android的Handler消息机制中,Looper的作用域就是线程且不同线程之间具有不同的Looper,这里使用的就是ThreadLocal对Looper与线程进行关联,如果不使用ThreadLocal,那么系统就必须提供一个全局的Hash表供Handler查找指定线程的Looper,这要比ThreadLocal复杂多了。
②当复杂逻辑下进行对象传递时,也可以考虑使用ThreadLocal。
例如一个线程中执行的任务比较复杂,需要一个监听器去贯穿整个任务的执行过程,如果不使用ThreadLocal,那么就需要将这个监听器从函数调用栈层层传递,这对程序设计来说是不可接受的,这时可以使用ThreadLocal让监听器作为线程内的全局对象而存在,在线程内部只需要通过get方法就可以获取到监听器。每个监听器对象都在自己的线程内部存储。
2.ThreadLocal用法
public class MainActivity extends Activity {
private ThreadLocal<Object> mThreadLocal = new ThreadLocal<>();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
//主线程
mThreadLocal.set("123");
new Thread() {
@Override
public void run() {
super.run();
//子线程
mThreadLocal.set("456");
mThreadLocal.set(false);
Log.i("ThreadLocal子线程",String.valueOf(mThreadLocal.get()));
}
}.start();
try {
getMainLooper().getThread().sleep( 3000); //保证子线程先去更新值
Log.i("ThreadLocal主线程",String.valueOf(mThreadLocal.get()));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
执行结果:
ThreadLocal子线程: false
ThreadLocal主线程: 123
通过这个例子就可以看到ThreadLocal的隔离性。主线程和子线程共享一个ThreadLocal,主线程设置ThreadLocal保存值123,所以get出来是123,而子线程先设置值为456,又设置值为false,导致false把之前设置的456覆盖掉了,子线程打印出来就是false。
所以,不同的线程访问同一个ThreadLocal获取到的值是不一样的。也就是说ThreadLocal在当前线程操作数据只对当前线程有效。
3.源码分析
①构造方法
public ThreadLocal() {}
②set方法
public void set(T value) {
Thread t = Thread.currentThread(); //获取当前线程
ThreadLocalMap map = getMap(t); // 拿到当前线程的ThreadLocalMap对象threadLocals,每个线程都有一个该对象
if (map != null)
map.set(this, value); //设置K为当前ThreadLocal对象,V为要存储的值
else
createMap(t, value); //创建新的ThreadLocalMap对象并赋值
}
当调用set(T value) 方法时,方法内部会获取当前线程中的ThreadLocalMap,然后进行判断,如果不为空就调用ThreadLocalMap的set方法(其中key为当前ThreadLocal对象,value为当前赋的值);如果为空就让当前线程创建新的ThreadLocalMap并设值。也就是说每个线程都有一个map,key是threadLocal变量,value是该变量在当前线程中的值。
其中getMap()与createMap()方法具体代码如下:
//创建线程t的ThreadLocalMap属性
void createMap(Thread t, firstValue){
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
//获取当前线程t的ThreadLocalMap属性
ThreadLocalMap getMap(Thread t){
return t.threadLocals;
}
ThreadLocal中所有的数据操作都与线程中的ThreadLocalMap有关。ThreadLocalMap是ThreadLocal类里定义的内部类。
③get方法
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t); //拿到线程中的ThreadLocalMap
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this); //根据key(ThreadLocal)对象,获取存储的entry数据
if(e != null){
T result = (T)e.value;
return result;
}
}
return setInitialValue(); //如果当前线程的ThreadLocalMap为空,就为当前线程创建新的ThreadLocalMap对象
}
其实ThreadLocal的get方法其实很简单,就是获取当前线程中的ThreadLocalMap对象,如果没有就创建,如果有就根据当前的 key(当前ThreadLocal对象)获取相应的数据。其中内部调用了ThreadLocalMap的getEntry()方法区获取数据。
④ThreadLocalMap源码
ThreadLocalMap是ThreadLocal中的一个静态内部类,它是为了维护线程私有值创建的自定义哈希表HashMap。其中线程的私有数据都是非常大且使用寿命长的数据。
static class ThreadLocalMap {
//ThreadLocalMap中存储的数据为Entry,且key为弱引用
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
private Entry[] table; //用于存储数据
private int size = 0;//table中entry的个数
private int threshold; // 负载因子,用于数组扩容
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
//第一次放入Entry数据时,初始化数组长度,定义扩容阈值
table = new Entry[INITAL_CAPACITY];
int i = firstKey.threadLocalHashCode & (INITAL_CAPACITY - 1);
table[i] = new Entry(firstKey, firstValue);
size = 1;
setThreshold(INITAL_CAPACITY);
}
//set方法,把值存储到特定线程的Map里面
private void set(ThreadLocal<?> key, Object value) {
Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1); //计算出key的hash值计算位置
//判断当前位置是否有数据,如果key值相同就替换,如果不同则找空位放数据
for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();
if (k == key) { //key值相同,直接覆盖
e.value = value;
return;
}
if (k == null) { //当前Entry对象对应key值为null,则清空所有key为null的数据
replaceStaleEntry(key, value, i);
return;
}
}
//以上情况都不满足,则直接添加
tab[i] = new Entry(key, value);
int sz = ++size;
//如果当前数组达到阈值,就扩容
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
//getEntry(),获取当前线程的Map里的变量
private Entry getEntry(ThreadLocal<?> key){
//根据当前key哈希后计算的位置,去找数组中对应位置是否有数据,如果有就直接将数据放回,如果没有则调用getEntryAfterMiss()方法
int i = key.threadLocalHashCode & (table.length -1); //获取hash值
Entry e = table[i];
if(e != null && e.get() == key)
return e;
else
return getEntryAfterMiss(key, i, e);
}
private Entry getEntryAfterMiss( ThreadLocal<?> key, int i, Entry e) {
//如果从数组中获取的key==null的情况下,get方法内部也会调用expungeStaleEntry()方法,去清除当前位置所有key==null的数据
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
}
}
可以发现,不管是调用ThreadLocal的set()还是get()方法,都会去清除Map中key==null的Entry数据。
虽然ThreadLocalMap是一个自定义哈希表,但是它与传统的HashMap等哈希表内部结构不一样。ThreadLocalMap内部仅仅维护了Entry[]数组。其中Entry实体中对应的key为弱引用,在第一次放入数据时会初始化数组长度(为16),定义数组扩容阀值(当前默认数组长度的2/3)。
ThreadLocalMap的set()方法主要分为三步:
①计算出当前ThreadLocal在table数组的位置,然后向后遍历,直到遍历到的Entry为null则停止,遍历到Entry的key与当前threadLocal实例的相等,直接更替value;
②如果遍历到Entry已过期(Entry的key为null),则调用replaceStaleEntry函数进行替换。
③在遍历结束后,未出现1和2两种情况,则直接创建新的Entry,保存到数组最后侧没有Entry的位置。
比如:第一种情况, Key值相同,直接覆盖
第二种情况,如果当前位置对应Entry的Key值为null
从图中可以看出,当添加新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); (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函数主要分为两次遍历,以当前过期的Entry为分割线,一次向前遍历,一次向后遍历。
主要对四种情况进行了判断,具体情况如下图表所示:
第三种情况,当前对应位置为null
图上为了方便理解清空上下数据的情况,并没有重新计算位置(希望大家注意!!!)
清除key==null的数据,判断当前数据的长度是不是到达阀值(默认没扩容前为INITIAL_CAPACITY *2/3,其中INITIAL_CAPACITY = 16),如果达到了重新计算数据的位置。
所以,ThreadLocalMap存储的是一个数组,可以用下面的图来表示Thread、ThreadLocal以及ThreadLocalMap之间的关系:
其中key是当前ThreadLocal对象的Hash,value 是ThreadLocal对象的存储的值。
可以发现整个ThreadLocal的使用都涉及到线程中ThreadLocalMap,虽然在外部调用的是ThreadLocal.set(value)方法,但本质上调用的是线程中的ThreadLocalMap中的set(key,value)方法。
总结一下ThreadLocal的原理:
Thread类里面有一个成员变量ThreadLocalMap,ThreadLocalMap类定义在ThreadLocal类内部,ThreadLocalMap类里存储的事Entry数组,Entry是一个键值对,而且它的key是弱引用。当调用ThreadLocal.set()方法时实际上调用的当前线程内ThreadLocalMap对象的set()方法,往Map的Entry数组里添加数据,而ThreadLocalMap只属于该线程独有的成员变量,因此就可以做到数据在线程之间的相互隔离。
存入Entry的值以键值对的形式存在,key为ThreadLocal,value是存入的值,因此当一个线程需要让多个数据隔离的时候,需要new出多个ThreadLocal对象,并存入entry数组(一个线程中是可以有多个ThreadLocal对象的。如果不new多个对象的话,由于key都是同一个ThreadLocal对象,会导致数据覆盖。也就是说一个ThreadLocal对象只能确保一个数据与其他线程隔离)。
所以通过ThreadLocal可以在不同线程中维护一套数据的副本并且相互不干扰。ThreadLocal为不同线程维护了不同的变量副本,是一种空间换时间的策略,提供了一种简单的多线程的实现方式。
4.ThreadLocal内存泄露问题
ThreadLocal可能会导致内存泄露。因为往Entry是以键值对的形式存放的,threadlocal为key,存放的需要进行线程隔离的值为value。而Entry的key是WeakReference弱引用类型,因此在GC执行垃圾回收的时候ThreadLocal对象可能会被回收,也就是ThreadLocalMap中的某个key会变为null,此时该map已经无法通过key取到被回收的threadlocal所对应的value,然而map又指向了该value,因此该value不会被回收,该内存区域依然被占据,因此造成内存泄漏。
也就是ThreadLocal存入到ThreadLocalMap之后,如果key被GC回收,这个ThreadLocal对象保存的内容将永远无法被使用,并且由于线程还存活,所以ThreadLocalMap不会被销毁,最终导致ThreadLocal的内容一直在内存里。
设计者在设计上避免了这个问题,就是当再次调用get()、remove()、set()方法时,都会自动清理掉线程ThreadLocalMap中所有Entry用key为null的value,并将整个Entry设置为null,这样在下次回收时就能将Entry和value都回收。也就是说,ThreadLocal会在get和set的时候进行查询,如果发现key被回收了就会清理value,因此value也不是无限增长的情况。
所以,当线程执行完毕之后,如果用到了ThreadLocal,那么开发者要在最后添加ThreadLocal.remove();这样就能保证value被回收而不会引发内存泄露。
5.源码中的应用
Handler消息机制中,当在一个线程中使用Looper的时候,需要调用Looper的prepare方法和loop方法,不然就会出现异常。这里Looper的存取就使用到了ThreadLocal。由于Looper的作用域是线程且不同线程之间具有不同的Looper,所以使用了ThreadLocal对Looper与线程进行关联。
看看这两个方法的源码:
public static void prepare() {
prepare(true);
}
private static void prepare(boolean quitAllowed){
if (sThreadLocal.get() != null) {
throw new RuntimeException("Only one Looper may be created per thread");
}
sThreadLocal.set(new Looper(quitAllowed));
}
loop方法内部先调用了myLooper方法:
public static Looper myLooper() {
return sThreadLocal.get();
}
Looper的prepare方法先创建Looper,并使用ThreadLocal存储即与当前的线程进行关联,然后loop方法开启消息机制的时候,使用ThreadLocal方法获取到当前线程的Looper方法。
6.使用InheritableThreadLocal继承父线程的局部存储
在业务开发的过程中,可能希望子线程可以访问主线程中的ThreadLocal数据,然而ThreadLocal是线程隔离的,包括在父子线程之间也是线程隔离的。为此,ThreadLocal提供了一个相似的子类InheritableThreadLocal,ThreadLocal和 InheritableThreadLocal分别对应于线程对象上的两块内存区域:
①ThreadLocal字段: 在所有线程间隔离;
②InheritableThreadLocal字段: 子线程会继承父线程的InheritableThreadLocal数据。父线程在创建子线程时,会批量将父线程的有效键值对数据拷贝到子线程的InheritableThreadLocal,因此子线程可以复用父线程的局部存储。
InheritableThreadLocal中可以重写childValue()方法修改拷贝到子线程的数据。
public class InheritableThreadLocal<T> extends ThreadLocal<T> {
// 参数:父线程的数据
// 返回值:拷贝到子线程的数据,默认为直接传递
protected T childValue(T parentValue) {
return parentValue;
}
}
注意:
①InheritableThreadLocal区域在拷贝后依然是线程隔离的: 在完成拷贝后,父子线程对InheritableThreadLocal的操作依然是相互独立的。子线程对InheritableThreadLocal的写不会影响父线程的InheritableThreadLocal,反之亦然;
②拷贝过程在父线程执行: 这是容易混淆的点,虽然拷贝数据的代码写在子线程的构造方法中,但是依然是在父线程执行的。子线程是在调用start()后才开始执行的。