1.ThreadLocal
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
@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保存值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); // 取出当前线程的threadLocals,每个线程唯一
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
//创建线程t的ThreadLocalMap属性
void createMap(Thread t, firstValue){
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
//获取线程t的ThreadLocalMap属性
ThreadLocalMap getMap(Thread t){
return t.threadLocals;
}
ThreadLocalMap是ThreadLocal类里定义的内部类。
③get方法
public T get() {
Thread t = Thread.currentThread();//获取当前线程
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if(e != null){
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
④ThreadLocalMap源码
ThreadLocalMap是ThreadLocal的静态内部类。
static class ThreadLocalMap {
static class Entry extends WeakReference
Object value;
Entry(ThreadLocal> k, Object v) {
super(k);
value = v;
}
}
private Entry[] table; //实际存储数据的地方
private int size = 0;//table中entry的个数
private int threshold; // 默认为0
ThreadLocalMap(ThreadLocal> firstKey, Object firstValue) {
table = new Entry[INITAL_CAPACITY]; //16
int i = firstKey.threadLocalHashCode & (INITAL_CAPACITY - 1);
table[i] = new Entry(firstKey, firstValue);
size = 1;
setThreshold(INITAL_CAPACITY);
}
//set方法,把一个对象/变量存储到特定线程的内存里面
private void set(ThreadLocal> key, Object value) {
Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1); //计算出key的hash值
//插入新的Entry或替换旧的Entry的value值
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();
}
//get方法,获取当前线程的内存里的变量
private Entry getEntry(ThreadLocal> key) {
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);
}
……
}
}
其中Entry继承自WeakReference中,每次存储一个值,都会把值的HashCode作为key,并且这个 key是一个WeakReference对象。
所以,ThreadLocalMap存储的是一个数组,每个数组的元素如下:
其中key是当前ThreadLocal对象的Hash,value 是ThreadLocal对象的存储的值。
这里可能会有一个疑问:一个ThreadLocal对象只能为每个线程存储一个值,为什么ThreadLocalMap里面却维护了一个Entry数组呢?其实,这个数组不是为了当前线程准备的,而是为了各个线程准备的。不同线程使用ThreadLocal,不同的线程对应不同的hashcode,与运算出来的数组下标是不一样的,也正因为如此才实现了线程隔离的特性。
总结一下ThreadLocal的原理:Thread类里面有一个成员变量ThreadLocalMap,而ThreadLocalMap类里有成员变量entry数组,当用ThreadLocal.set值得时候,实际上就是往entry数组添加内容,而entry数组又只属于该线程(Thread)独有的成员变量,因此就可以做到数据在线程之间的相互隔离。存入entry的值以键值对的形式存在,key为ThreadLocal,value是存入的值,因此当一个线程需要让多个数据隔离的时候,需要new出多个ThreadLocal对象,并存入entry数组(一个线程中是可以有多个ThreadLocal对象的。如果不new多个对象的话,由于key都是同一个ThreadLocal对象,会导致数据覆盖。也就是说一个ThreadLocal对象只能确保一个数据与其他线程隔离)。
所以通过ThreadLocal可以在不同线程中维护一套数据的副本并且相互不干扰。ThreadLocal为不同线程维护了不同的变量副本,是一种空间换时间的策略,提供了一种简单的多线程的实现方式。
4.ThreadLocal可能导致内存泄露
ThreadLocal可能会导致内存泄露。因为往Entry数组里面set ThreadLocal的时候是以键值对的形式存放的,threadlocal为key,存放的需要进行线程隔离的值为value,而源码里面ThreadLocalMap的Entry的key是WeakReference弱引用类型,这就表明在gc执行垃圾回收的时候ThreadLocal会被回收,也就是Map中的某个key会为null,此时该map已经无法通过key取到被回收的threadlocal所对应的value,然而map又指向了该value,因此该value不会被回收,该内存区域依然被占据,因此造成内存泄漏。(也就是ThreadLocal存入到ThreadLocalMap之后,如果key被GC回收,这个ThreadLocal对象保存的内容将永远无法被使用,并且由于线程还存活,所以ThreadLocalMap不会被销毁,最终导致ThreadLocal的内容一直在内存里。)
但是设计者在设计上避免了这个问题,就是当再次调用get()、remove()、set()方法的时候,会自动清理key为null的对象。也就是说,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方法。