ThreadLocal,称之为线程本地变量,可以看成专属于线程的变量,不受其他线程干扰,保存着线程的专属数据。在多线程并发操作线程本地变量的时候,线程各自操作的是自己的本地值,从而规避了线程安全问题。
ThreadLocal 使用场景
- 线程隔离
ThreadLocal 的主要价值在于线程隔离,ThreadLocal 中的数据只属于当前线程,其本地值对别的线程是不可见的,在多线程环境下,可以防止自己的变量被其他线程篡改。另外,由于各个线程之间的数据相互隔离,避免了同步加锁带来的性能损失,大大提升了并发性的性能。
private static final ThreadLocal<Session> sessionThreadLocal = new ThreadLocal<>();
public static Session getSession() throw Exception {
Session session = sessionThreadLocal.get();
if (session == null) {
session = getSessionFactory().openSession();
sessionThreadLocal.set(session);
}
return session;
}
一个 Session 代表一个数据库连接,以上代码为每一个线程设置一个数据库连接,这样的话在使用完成之后关闭 Session 就不会影响到其他线程。
- 跨函数传递数据
通常用于同一个线程内,跨类、跨方法传递数据时,如果不用 ThreadLocal,那么相互之间的数据传递势必要靠返回值和参数,这样无形之中增加了这些类或者方法之间的耦合度。由于 ThreadLocal 的特性,同一线程在某些地方进行设置,在随后的任意地方都可以获取到。线程执行过程中所执行到的函数都能读写 ThreadLocal 变量的线程本地值,从而可以方便地实现跨函数的数据传递。使用 ThreadLocal 保存函数之间需要传递的数据,在需要的地方直接获取,也能避免通过参数传递数据带来的高耦合。
在“跨函数传递数据”场景中使用 ThreadLocal 的典型案例为:可以为每个线程绑定一个 Session(用户会话)信息,这样一个线程所有调用到的代码都可以非常方便地访问这个本地会话,而不需要通过参数传递。
ThreadLocal 源码分析
set(T value) 方法
public void set(T value) {
// 获取当前线程
Thread t = Thread.currentThread();
// 获取当前线程的 ThreadLocalMap 成员
ThreadLocalMap map = getMap(t);
if (map != null)
// value 绑定到 ThreadLocal 实例
map.set(this, value);
else
createMap(t, value);
}
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
set(T value)
方法主要作用就是设置线程当前值。具体流程如下:
- 获得当前线程,然后获得当前线程的 ThreadLocalMap 成员,暂存于 map 变量
- 如果 map 不为空,就将 Value 设置到 map 中,当前的 ThreadLocal 作为 Key
- 如果 map 为空,为该线程创建 map,然后设置第一个“Key-Value对”,Key 为当前的 ThreadLocal 实例, Value 为
set()
方法的参数 value 值
get() 方法
public T get() {
// 获取当前线程
Thread t = Thread.currentThread();
// 获取当前线程的 ThreadLocalMap 成员
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();
}
private T setInitialValue() {
T value = initialValue();
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
// 设置初始值
if (map != null)
map.set(this, value);
else
createMap(t, value);
return value;
}
remove() 方法
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
ThreadLocalMap 源码分析
ThreadLocal 的操作都是基于 ThreadLocalMap 展开的,而 ThreadLocalMap 是 ThreadLocal 的一个静态内部类,其实现了一套简单的 Map 结构。
static class ThreadLocalMap {
/**
* Map的条目类型,继承至WeakReference,key为ThreadLocal实例
*/
static class Entry extends WeakReference<ThreadLocal<?>> {
// 条目的值
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
/**
* Map的初始容量,默认为16
*/
private static final int INITIAL_CAPACITY = 16;
/**
* Map 的条目数组,作为哈希表使用
*/
private Entry[] table;
/**
* Map的条目数量
*/
private int size = 0;
/**
* 扩容因子
*/
private int threshold;
}
ThreadLocal 源码中的 get()
、set()
、remove()
方法都涉及 ThreadLocalMap 的方法调用,主要调用了 ThreadLocalMap 的如下几个方法:
set(ThreadLocal<?> key,Object value)
:向 Map 实例设置“Key-Value对”getEntry(ThreadLocal)
:从 Map 实例获取 Key(ThreadLocal 实例)所属的 Entryremove(ThreadLocal)
:根据 Key(ThreadLocal 实例)从 Map 实例移除所属的 Entry
set(ThreadLocal, Object) 方法
private void set(ThreadLocal<?> key, Object value) {
Entry[] tab = table;
int len = tab.length;
// 根据key的hashcode,找到key在数组上的槽点
int i = key.threadLocalHashCode & (len-1);
// 从槽点开始向后循环搜索,找到空闲位置或找到现有位置;若不存在现有位置,则必定有空位置,因为没有空间时会扩容
for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();
// 找到现有槽点,key值为ThreadLocal实例
if (k == key) {
e.value = value;
return;
}
// 槽点值已经被GC掉,重新设置key和value
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
// 找不到合适的槽点,增加新的Entry
tab[i] = new Entry(key, value);
// 设置ThreadLocal的数量
int sz = ++size;
// 清理key为null的无效Entry,没有可清理的Entry,并且现有条目数量大于扩容因子值,进行扩容
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
Entry 的 key 为什么使用弱引用
Entry 用于保存 ThreadLocalMap 的 Key-Value 对条目,但是 Entry 使用了对 ThreadLocal 实例进行包装之后的弱引用(WeakReference)作为 Key:
static class Entry extends WeakReference<ThreadLocal<?>> {
// 条目的值
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
为什么 Entry 的 key 使用弱引用呢?这是因为如果我们在一个方法中创建了 ThreadLocal 变量时,当方法执行结束(注意,并不是线程结束),栈帧就会销毁,此时如果 Entry 中的 key(也就是 ThreadLocal 变量)是强引用的话,就会导致 key 引用指向的 ThreadLocal 实例以及其值 value 得不到回收,就会造成内存泄漏。
在 Android 中,Handler 的使用不当也会造成内存泄漏。Handler 机制的组成结构如下:
public class Handler {
final Looper mLooper;
final MessageQueue mQueue;
}
public final class Looper {
static final ThreadLocal<Looper> sThreadLocal = new ThreadLocal<Looper>();
final MessageQueue mQueue;
}
public final class MessageQueue {
Message mMessages;
}
public final class Message implements Parcelable {
Handler target;
}
我们知道,在应用启动的时候,ActivityThread.main()
会初始化主线程的 Looper 并与主线程绑定在一起。调用步骤如下:
// android.app.ActivityThread
public static void main(String[] args) {
Looper.prepareMainLooper();
}
// android.os.Handler
public static void prepareMainLooper() {
prepare(false);
}
private static void prepare(boolean quitAllowed) {
sThreadLocal.set(new Looper(quitAllowed));
}
// java.lang.ThreadLocal
public void set(T value) {
Thread t = Thread.currentThread();
// 获取到Thread#threadLocals变量
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
即 mainThtead -> Thread#threadLocals -> Looper(作为Value,强引用) -> MessageQueue -> Massage -> Handler
,如果 Handler 作为 Activity 的内部类存在,那么 Handler -> Activity。在 Activity 退出之后,由于Massage 还存在,且主线程不会被销毁,因此会造成内存泄漏。解决方法之一就是在 Activity 退出之时情况所有的 Massage,断开 MessageQueue -> Massage -> Handler 链路,解决内存泄漏。
ThreadLocal 的使用原则
由于 ThreadLocal 使用不当会导致严重的内存泄漏问题,所以为了更好地避免内存泄漏问题的发生,我们使用 ThreadLocal 时遵守以下两个原则:
- 尽量使用 private static final 修饰 ThreadLocal 实例。使用 private 与 final 修饰符主要是为了尽可能不让他人修改、变更 ThreadLocal 变量的引用,使用 static 修饰符主要是为了确保 ThreadLocal 实例的全局唯一
- ThreadLocal 使用完成之后务必调用
remove()
方法。这是简单、有效地避免 ThreadLocal 引发内存泄漏问题的方法