ThreadLocal应用与实现
什么是ThreadLocal以及如何使用
ThreadLocal 直译过来就是线程本地变量的意思。它是一个容器类,可以存入其他对象类型。
它的作用就是控制多个线程对同一个ThreadLocal变量读取和写入时,可以做到相互独立,互不影响。并且对多次get()操作返回的值是同一个。
ThreadLocal有两种初始化方式:
- 使用ThreadLocal的set(Object)方法设置初始值。
- 重写initialValues()方法,返回初始值。当调用get()方法获取值时,如果发现没有用set(Object)设置过值,则会调用initialValues()获取初始值。
示例代码如下:
//方式1
//在主线程中创建一个ThreadLocal变量,并初始化为1
ThreadLocal<Integer> threadLocal = new ThreadLocal<Integer>();
threadLocal.set(1);
//那么在主线程可以获得该值: value = 1
Integer value = threadLocal.get();
//在子线程中无法获取该值: valueChild = null
Integer valueChild = threadLocal.get();
//方式2
//在主线程通过复写initialValue()初始化ThreadLocal
AtomicInteger nextId = new AtomicInteger(0);
ThreadLocal<Integer> threadLocal2 = new ThreadLocal<Integer>() {
@override
protected Integer initialValue() {
return nextId.getAndInrement();
}
};
//在子线程第一次调用get()方法时会回调initialValue()获得初始值,并且多次调用不变
Integer id = threadLocal2.get();
源码实现
ThreadLocal实现线程变量隔离的核心在于get()方法,那么接下来通过get()方法的源码追踪其实现原理。
public T get() {
//获取当前调用线程对象
Thread t = Thread.currentThread();
//根据线程对象获取其关联的ThreadLocal字典,说是字典,其实是数组实现的
//这个数组存储了在当前线程环境创建的ThreadLocal变量
//数组下标通过ThreadLocal对象的哈希表计算出来的
//这个ThreadLocalMap变量被存储到了Thread对象的threadLocal变量中。
ThreadLocalMap map = getMap(t);
//如果ThreadLocalMap不为空,则以ThreadLocal为键取出其中的值。
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
//如果ThreadLocalMap为空,或者ThreadLocal对应的值不存在,则调用setInitialValue()方法
return setInitialValue();
}
private T setInitialValue() {
//回调initialValue()获取初始值
T value = initialValue();
Thread t = Thread.currentThread();
//再次获取ThreadLocalMap
ThreadLocalMap map = getMap(t);
//如果ThreadLocalMap不为空,则将初始值存进去
if (map != null)
map.set(this, value);
//如果ThreadLocalMap为空,则创建一个ThreadLocalMap,并赋值给当前Thread对象的threadLocal变量
else
createMap(t, value);
//最终返回初始值
return value;
}
下面我们着重关注几个操作:
创建ThreadLocalMap
创建ThreadLocalMap非常简单。就是新建一个ThreadLocalMap对象,并赋值给线程的threadLocals变量。
//第一次创建ThreadLocalMap
//t 为当前线程对象
//fisrtValue 为第一次存入的值
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
ThreadLocalMap定义如下:
private Entry[] table;
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
table = new Entry[INITIAL_CAPACITY];
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
table[i] = new Entry(firstKey, firstValue);
size = 1;
setThreshold(INITIAL_CAPACITY);
}
ThreadLocalMap内部是通过Entry数组存储ThreadLocal变量的。数组初始化容量为16,触发扩容的阈值为容量的2/3,扩容后的容量为原容量的2倍。
向ThreadLocalMap存入
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;
//注意:因为Entry数组的容量也总是2的幂次方,所以计算数组下标的方式与HashMap一致。
int i = key.threadLocalHashCode & (len-1);
//遍历数组中已有的Entry
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();
//如果key相同则直接替换value
if (k == key) {
e.value = value;
return;
}
//如果键为空,说明被回收(Entry中的key采用了弱引用)了。
//则在该数组位置上存入新的Key和Value,同时还会清除其他位置上key为空的Entry
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
//再,如果key不为空,并且也不同,说明发生的冲突,那么继续循环寻找下一个空位置nextIndex()
}
//如果没有相同key,则新建Entry存入Entry数组中
tab[i] = new Entry(key, value);
int sz = ++size;
//最后做一遍无效key的Entry清理。
//如果没有需要清理的,并且数组长度超过了阈值,就会触发扩容
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
从ThreadLocalMap读取
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);
}
相对存入,读取过程就比较简单了,计算出数组下标就可以拿到对应的Entry对象了。
不过,由于弱引用的存在,有可能在读取时key被回收了,所以还需要走一遍清除的流程。
应用场景
Android中的Looper
如果要是一个变量在一个线程中只能存在一份,并且与其他线程不能共用,可以使用ThreadLocal。
示例:Android中的Looper对象。
static final ThreadLocal<Looper> sThreadLocal = new ThreadLocal<Looper>();
//不同的线程创建不同的Looper,一个线程只能存在一个Looper
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));
}
//在线程内部多次调用myLooper()返回的是同一个Looper对象。
public static @Nullable Looper myLooper() {
return sThreadLocal.get();
}
Spring中的事务管理
事务是和线程绑定起来的,Spring框架在事务开始时会给当前线程绑定一个Jdbc Connection,在整个事务过程都是使用该线程绑定的connection来执行数据库操作,实现了事务的隔离性
注意事项
为什么Entry的key采用弱引用的方式?
防止内存泄漏。
因为ThreadLocal对象是存储到线程对象Thread中的,所以它的生命周期是和当前线程一致的。如果当前线程被缓存住了(线程池),会导致当前线程会一直持有ThreadLocal引用,从而产生内存泄漏。
当ThreadLocal采用弱引用后,在GC时会回收ThreadLocal的引用,从而导致Entry的key为null,这样在下次get()或set()操作时就会清除掉该Entry。
有人会问,如果之后不再调用get()或set()方法了,是不是就会产生内存泄漏?
是的,尽管ThreadLocal由于弱引用的原因可以被回收,但是value是强引用,所以value引用的对象还是会产生泄漏。
所以当不再使用ThreadLocal后,要主动调用remove()方法,从ThreadLocalMap中移除掉对应的Entry。
如果Entry被回收了,ThreadLocal的get()方法会返回null吗?
不存在这种情况,Entry被回收了,说明没有引用指向ThreadLocal了,也就没有办法调用get()方法了。
如果在Entry存入数组中时,发生了冲突怎么办?
不像HashMap中的Entry是一个链表,这里的Entry就是一个单个对象,所以发生冲突时就需要在从发生冲突的位置开始,向后寻找空的位置,如果到达了数组默认,从返回数组开始位置继续寻找。
因为当数组长度到达容量的2/3时就会发生扩容,所以当前时间点肯定存在空位置。