Java ThreadLocal 类简析
ThreadLocal
ThreadLocal 类的作用就是实现每一个线程都有自己的专属本地变量。
使用
简单示例
public class Demo01 implements Runnable {
// SimpleDateFormat 不是线程安全的,所以每个线程都要有自己独立的副本
private static final ThreadLocal<SimpleDateFormat> formatter = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyyMMdd HHmm"));
public static void main(String[] args) throws InterruptedException {
Demo01 obj = new Demo01();
for(int i=0 ; i<10; i++){
Thread t = new Thread(obj, ""+i);
Thread.sleep(new Random().nextInt(1000));
t.start();
}
}
@Override
public void run() {
System.out.println("Thread Name= "+Thread.currentThread().getName()+" default Formatter = "+formatter.get().toPattern());
try {
Thread.sleep(new Random().nextInt(1000));
} catch (InterruptedException e) {
e.printStackTrace();
}
// formatter 被重新设置,但是其它的线程不受影响,它们仍然持有原始的那一份副本
formatter.set(new SimpleDateFormat());
System.out.println("Thread Name= "+Thread.currentThread().getName()+" formatter = "+formatter.get().toPattern());
}
}
上述代码中,Thread-0 使用 set 方法改变了 formatter,但是这个改变只对自己生效。其它线程看到的仍然是最开始的值。
ThreadLocal 原理
初始化
我们一般调用 withInitial 方法进行 ThreadLocal 对象的初始化,可以看到它的返回值的实际类型为 SuppliedThreadLocal :
public static <S> ThreadLocal<S> withInitial(Supplier<? extends S> supplier) {
// 返回的是 SuppliedThreadLocal<> 类型的对象
return new SuppliedThreadLocal<>(supplier);
}
继续跟进,SuppliedThreadLocal 不过是 ThreadLocal 的静态内部类,继承了 ThreadLocal 结构也很简单:
static final class SuppliedThreadLocal<T> extends ThreadLocal<T> {
// 我们初始化的数据就存储在这个 supplier 对象中
private final Supplier<? extends T> supplier;
// withInitial 方法调用的构造器
SuppliedThreadLocal(Supplier<? extends T> supplier) {
this.supplier = Objects.requireNonNull(supplier);
}
// 这是一个被重载的方法,很重要
@Override
protected T initialValue() {
// 返回 supplier 中我们实际提供的对象
return supplier.get();
}
}
private static final ThreadLocal<SimpleDateFormat> formatter = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyyMMdd HHmm"));
我们的 formatter 对象的真实类型应该是 SuppliedThreadLocal,这点很重要。
get
先来看看 Thread 类的源码,每个Thread 都有两个 ThreadLocalMap 类型的字段,且访问权限为 friendly,即同包下的类可以访问:
摆上几个关键类的关系:
当我们调用 ThreadLocal 的 get 方法时:
会首先使用 getMap 方法获取当前线程的 ThreadLocalMap,由于 ThreadLocal 和 Thread 类在同一个包下(java/lang),所以可以直接访问 Thread 对象(当前线程)的 threadLocals 字段:
如果当前线程的 ThreadLocalMap 的字段为 null,那么就需要 setInitialValue 函数来进行当前 Thread 中 ThreadLocalMap 字段的初始化:
private T setInitialValue() {
T value = initialValue(); // 1,2
Thread t = Thread.currentThread(); // t = 当前线程
ThreadLocalMap map = getMap(t); // 获取当前线程的 map,为 null
if (map != null)
map.set(this, value);
else
createMap(t, value); // 创建 map
return value; // 返回 null 或者已经初始化的值
}
// 1
// ThreadLocal 中的 initialValue 方法
// 不是用 withInitial 初始化的 ThreadLocal 调用
protected T initialValue() {
return null;
}
// 2
// SuppliedThreadLocal 中的 initialValue
// 用 withInitial 初始化的 ThreadLocal 调用
@Override
protected T initialValue() {
return supplier.get(); // 返回我们存储的对象
}
// t 是当前线程,firstValue 为 null
void createMap(Thread t, T firstValue) {
// 赋值给 Thread 对象
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
如果当前线程的 ThreadLocalMap 字段不为 null,则直接使用 ThreadLocalMap 的 get 方法获取 ThreadLocalMap.Entry,get 方法的参数就是当前的 ThreadLocal 对象,而该方法的结果是一个一个弱引用对象:
// ThreadLocal 类中的 ThreadLocalMap 内部类的 Entry 内部类
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value; // 就是我们赋予给 ThreadLocal 对象的值
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
set
// 设置 ThreadLocal 的值
public void set(T value) {
// 获取当前线程,作为 KEY
Thread t = Thread.currentThread();
// 以当前线程为 key 查找 map
ThreadLocalMap map = getMap(t);
if (map != null)
// 如果当前线程存在对应的 map,将新的对象设置进去,key 为当前的 ThreadLocal 对象
map.set(this, value);
else
// 否则就创建新的 ThreadLocalMap,并且设置值
createMap(t, value);
}
简单总结
也就是说每个 Thread 对象中(线程)存储着一个 map(ThreadLocalMap),这个 map 的 key 值为 ThreadLocal 对象,value 为我们想赋予的值。
当我们使用 ThreadLocal 的 get 方法时,ThreadLocal 对象会根据当前线程获取到当前线程的 map,并且再以自己(ThreadLocal 对象)为 key 找到我们所赋予的值。
内存泄露问题
在谈及 ThreadLocal 内存泄露问题之前,我们需要先看一下 ThreadLocalMap 的具体实现。
ThreadLocalMap
表项
表项的 key 为 对 ThreadLocal 对象的弱引用,value 为我们赋予的值:
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
ThreadLocal 构造器
调用 ThreadLocal 的 createMap 方法会直接初始化一个 ThreadLocalMap:
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
// 创建新的 map 表项
table = new Entry[INITIAL_CAPACITY];
// 计算当前 ThreadLocal 对象的 hash 值
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
// 塞进去
table[i] = new Entry(firstKey, firstValue);
size = 1;
setThreshold(INITIAL_CAPACITY);
}
getEntry
ThreadLocal 的 get 方法将会调用 getEntry 方法:
private Entry getEntry(ThreadLocal<?> key) {
// 计算 hash 值
int i = key.threadLocalHashCode & (table.length - 1);
// 直接获取目标对象
Entry e = table[i];
// 目标对象不为 null
if (e != null && e.get() == key)
// 返回 Entry
return e;
else
return getEntryAfterMiss(key, i, e);
}
set
较为复杂,暂不涉及
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;
int i = key.threadLocalHashCode & (len-1);
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();
}
remove
private void remove(ThreadLocal<?> key) {
Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1);
for (Entry e = tab[i];
e != null;
// 获取下一个 Entry
e = tab[i = nextIndex(i, len)]) {
// e 是 Reference 对象,get 方法将获取弱引用的 ThreadLocal 对象
if (e.get() == key) {
e.clear();
expungeStaleEntry(i);
return;
}
}
}
原因
ThreadLocalMap
存储表项的数据结构就是一个简单的数组,通过 ThreadLocal 的 hash 值来计算索引并进行存储。ThreadLocalMap
中使用的 key 为 ThreadLocal
的弱引用,而 value 是强引用。
所以,如果 ThreadLocal
没有被外部强引用的情况下,在垃圾回收的时候,key 会被清理掉,而 value 不会被清理掉。这样一来,ThreadLocalMap
中就会出现 key 为 null 的 Entry。
解决
假如我们不做任何措施的话,value 永远无法被 GC 回收,这个时候就可能会产生内存泄露。ThreadLocalMap 实现中已经考虑了这种情况,在调用 set()
、get()
、remove()
方法的时候,会清理掉 key 为 null 的记录。
使用完 ThreadLocal
方法后 最好手动调用remove()
方法。