一、什么是ThreadLocal?为什么要使用它?
概念:ThreadLocal即线程本地变量,每个线程都会有这个变量的一个本地拷贝副本,多个线程操作该变量时,实际是在操作自己本地内存里的变量,起到了线程隔离的作用,避免了线程安全问题。
作用:并发场景下会存在多个线程同时修改一个共享变量的场景,这就可能出现线程安全问题。为了解决这一问题,我们可以使用加锁的方式(synchronized\Lock)。但是加锁的方式可能导致系统变慢,还有另一种方案,就是使用空间换时间的方式,使用ThreadLocal。
二、ThreadLocal原理
2.1 ThreadLocal内存结构
Thread类中维护了ThreadLocal.ThreadLocalMap的成员变量,ThreadLocalMap内部维护了Entry数组,每个Entry代表一个完整的对象,key是ThreadLocal本身,value是ThreadLocal的泛型对象的值。
2.2 源码分析性
2.2.1 Thread类中维护ThreadLocalMap
public class Thread implements Runnable {
//ThreadLocal.ThreadLocalMap是Thread的属性
ThreadLocal.ThreadLocalMap threadLocals = null;
}
2.2.2 ThreadLocalMap源码
static class ThreadLocalMap {
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
//Entry数组
private Entry[] table;
// ThreadLocalMap的构造器,ThreadLocal作为key
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);
}
}
2.2.3 ThreadLocal set()
public void set(T value) {
Thread t = Thread.currentThread(); //获取当前线程t
ThreadLocalMap map = getMap(t); //根据当前线程获取到ThreadLocalMap
if (map != null) //如果获取的ThreadLocalMap对象不为空
map.set(this, value); //K,V设置到ThreadLocalMap中
else
createMap(t, value); //创建一个新的ThreadLocalMap
}
ThreadLocalMap getMap(Thread t) {
return t.threadLocals; //返回Thread对象的ThreadLocalMap属性
}
void createMap(Thread t, T firstValue) { //调用ThreadLocalMap的构造函数
t.threadLocals = new ThreadLocalMap(this, firstValue); this表示当前类ThreadLocal
}
2.2.4 ThreadLocal get()
public T get() {
Thread t = Thread.currentThread();//获取当前线程t
ThreadLocalMap map = getMap(t);//根据当前线程获取到ThreadLocalMap
if (map != null) { //如果获取的ThreadLocalMap对象不为空
//由this(即ThreadLoca对象)得到对应的Value,即ThreadLocal的泛型值
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue(); //初始化threadLocals成员变量的值
}
private T setInitialValue() {
T value = initialValue(); //初始化value的值
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t); //以当前线程为key,获取threadLocals成员变量,它是一个ThreadLocalMap
if (map != null)
map.set(this, value); //K,V设置到ThreadLocalMap中
else
createMap(t, value); //实例化threadLocals成员变量
return value;
}
- Thread线程类中维护一个ThreadLocal.ThreadLocalMap的实例变量,每个线程都有个属于自己的ThreadLocalMap
- ThreadLocalMap内部维护着Entry数组,每个Entry代表一个完整的对象,key是ThreadLocal本身,value是ThreadLocal的泛型值
- 并发场景下,每个线程Thread在往ThreadLocal里面设置值的时候,都是往自己的ThreadLocalMap里存,读取也是以某个ThreadLocal作为引用,在自己的map里找对应的key,从而实现了线程隔离。
三、问题
3.1 为什么不直接用线程id作为ThreadLocalMap的key呢?
例如:
public class TianLuoThreadLocalTest {
private static final ThreadLocal<String> threadLocal1 = new ThreadLocal<>();
private static final ThreadLocal<String> threadLocal2 = new ThreadLocal<>();
}
一个使用类中有两个ThreadLocalMap类型的共享变量,如果用线程id作为ThreadLocalMap的key就无法区分哪个ThreadLocal成员变量,因为他们线程id都是一样的。每个ThreadLocal对象,都可以由threadLocalHashCode属性唯一区分的,每个ThreadLocal对象都可以由这个对象名字唯一区分
3.2 弱引用导致的内存泄漏
ThreadLocal引用示意图:
关于ThreadLocal内存泄漏,网上比较流行的说法是这样的:
ThreadLocalMap使用ThreadLocal的弱引用作为key,当ThreadLocal变量被手动设置为null,即一个ThreadLocal没有外部强引用来引用它,当系统GC时,ThreadLocal一定会被回收。这样的话,ThreadLocalMap中就会出现key为null的Entry,就没有办法访问这些key为null的Entry的value;如果当前线程再迟迟不结束的话,这些key为null的Entry的value就会一直存在一条强引用链:Thread变量 -> Thread对象 -> ThreadLocalMap -> Entry -> value -> Object 永远无法回收,造成内存泄漏
ThreadLocal变量被手动设置为null后的引用链图:
3.3 key是弱引用,GC回收会影响ThreadLocal的正常工作吗?
弱引用:具有弱引用的对象拥有更短暂的生命周期,如果一个对象只有弱引用存在,则下次GC将会回收掉该对象 结论:不会影响,因为有ThreadLoca变量引用它,是不会被GC回收的,除非手动把ThreadLocal变量设置为null。
3.4 Enter的key为什么要设计成弱引用?
我们先来回忆一下四种引用:
- 强引用:我们平时
new了一个对象就是强引用,例如Object obj = new Object();即使在内存不足的情况下,JVM宁愿抛出OutOfMemory错误也不会回收这种对象。 - 软引用:如果一个对象只具有软引用,则内存空间足够,垃圾回收器就不会回收它;如果内存空间不足了,就会回收这些对象的内存。
- 弱引用:具有弱引用的对象拥有更短暂的生命周期。如果一个对象只有弱引用存在了,则下次GC将会回收掉该对象(不管当前内存空间足够与否)。
- 虚引用:如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收。虚引用主要用来跟踪对象被垃圾回收器回收的活动。
下面我们分情况讨论:
- 如果
Key使用强引用:当ThreadLocal的对象被回收了,但是ThreadLocalMap还持有ThreadLocal的强引用的话,如果没有手动删除,ThreadLocal就不会被回收,会出现Entry的内存泄漏问题。 - 如果
Key使用弱引用:当ThreadLocal的对象被回收了,因为ThreadLocalMap持有ThreadLocal的弱引用,即使没有手动删除,ThreadLocal也会被回收。value则在下一次ThreadLocalMap调用set,get,remove的时候会被清除。
由此可以发现使用弱引用作为Entry的key可以多一层保证;弱引用ThreadLocal不会轻易内存泄漏
实际上内存泄漏的根本原因是,不再被使用的Entry,没有从线程的ThreadLocalMap中删除。一般删除不再使用Entry有两种方式:
- 一种是使用完ThreadLocal,手动调用remove(),把Entry从ThreadLocalMap中删除
- 另外一种就是:ThreadLocalMap的自动清除机制去清除过期Entry(ThreadLocalMap的get(),set(时都会差法过期Entry的清除))
四、ThreadLocal使用场景和注意点
- ThreadLocal使用完要手动调用remove()
- 使用日期工具类,用到SimpleDateFormat,使用ThreadLocal保证线程安全
- 全局存储用户信息(用户信息存入ThreadLocal,那么当前线程在任何地方需要时,都可以使用)
- 保证同一个线程,获取的数据库连接Connection是同一个,使用ThreadLocal来解决线程安全问题
参考: