4.8 ThreadLocal
线程局部变量。
4.8.1 常见面试题
- ThreadLocal中ThreadLocalMap的数据结构和关系?
- ThreadLocal的key是弱引用,为什么?
- ThreadLocal内存泄漏问题是什么?
- ThreadLocal中最后为什么要加remove方法?
4.8.2 ThreadLocal简介
ThreadLocal提供线程局部变量。这些变量与正常的变量不同,因为每一个线程在访问ThreadLocal实例的时候(通过其get或set方法)都有自己的、独立初始化的变量副本。ThreadLocal实例通常是类中的私有静态字段
,使用它的目的是希望将状态(例如,用户ID或事务ID)与线程关联起来。
必须回收自定的ThreadLocal变量,尤其在线程池的场景下,线程经常会被复用,如果不清理自定义的ThreadLocal变量,可能影响后续业务和造成内存泄漏问题i。尽量在代码中使用try-finally块进行回收
objectThreadLocal.set(userInfo);try{}finally{objectThreadLocal.remove();
}
因为每个Thread内有自己的实例副本且该副本只由当前线程自己使用
既然其它Thread不可访问,那就不存在多线程间共享的问题。
统一设置初始值,但是每个线程对这个值的修改都是各自线程互相独立的
如何保证不争抢:
- 加锁
- ThreadLocal: 每个线程一份数据
4.8.3 ThreadLocal源码分析
// ThreadLocal类/*** Returns the value in the current thread's copy of this* thread-local variable. If the variable has no value for the* current thread, it is first initialized to the value returned* by an invocation of the {@link #initialValue} method.** @return the current thread's value of this thread-local*/public T get() {Thread t = Thread.currentThread();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();}/*** Sets the current thread's copy of this thread-local variable* to the specified value. Most subclasses will have no need to* override this method, relying solely on the {@link #initialValue}* method to set the values of thread-locals.** @param value the value to be stored in the current thread's copy of* this thread-local.*/public void set(T value) {Thread t = Thread.currentThread();ThreadLocalMap map = getMap(t);if (map != null) {map.set(this, value);} else {createMap(t, value);}}/*** Removes the current thread's value for this thread-local* variable. If this thread-local variable is subsequently* {@linkplain #get read} by the current thread, its value will be* reinitialized by invoking its {@link #initialValue} method,* unless its value is {@linkplain #set set} by the current thread* in the interim. This may result in multiple invocations of the* {@code initialValue} method in the current thread.** @since 1.5*/public void remove() {ThreadLocalMap m = getMap(Thread.currentThread());if (m != null) {m.remove(this);}}ThreadLocalMap getMap(Thread t) {return t.threadLocals;}/*** Create the map associated with a ThreadLocal. Overridden in* InheritableThreadLocal.** @param t the current thread* @param firstValue value for the initial entry of the map*/void createMap(Thread t, T firstValue) {t.threadLocals = new ThreadLocalMap(this, firstValue);}static class ThreadLocalMap {/*** The entries in this hash map extend WeakReference, using* its main ref field as the key (which is always a* ThreadLocal object). Note that null keys (i.e. entry.get()* == null) mean that the key is no longer referenced, so the* entry can be expunged from table. Such entries are referred to* as "stale entries" in the code that follows.*/static class Entry extends WeakReference<ThreadLocal<?>> {/** The value associated with this ThreadLocal. */Object value;Entry(ThreadLocal<?> k, Object v) {super(k);value = v;}}...}
ThreadLocalMap实际上就是一个以ThreadLocal实例为key,任意对象为value的Entry集合。
JVM内部维护了一个线程版的Map<ThreadLocal,Value>
(通过ThreadLocal对象的set方法,结果把ThreadLocal对象自己当做key,放进了ThreadLoalMap中),每个线程要用到这个ThreadLocal的时候,用当前的线程去Map里面获取,通过这样让每个线程都拥有了自己独立的变量,人手一份,竟争条件被彻底消除,在并发模式下是绝对安全的变量。
4.8.4 ThreadLocaln内存泄漏问题
必须回收自定义的 ThreadLocal 变量,尤其在线程池场景下,线程经常会被复用如果不清理自定义的 ThreadLocal 变量,可能会影响后续业务逻辑和造成内存泄露等问题。尽量在代理中使用 try-finally 块进行回收。
// 正例
objectThreadLocal.set(userInfo);
try{//...
}finally{objectThreadLocal.remove();
}
什么是内存泄漏:不再会使用的对象或者变量占用着内存,一直不被回收,就是内存泄漏
static class Entry extends WeakReference<ThreadLocal<?>> { // 为什么使用弱引用,不用会怎么样?/** The value associated with this ThreadLocal. */Object value;Entry(ThreadLocal<?> k, Object v) {super(k); // 关键!将 key (k) 传递给 WeakReference<ThreadLocal<?>> 的构造函数, value = v;}
}
ThreadLocalMap
从字面上就可以看出这是一个保存ThreadLocal对象的map(以ThreadLocal为Key),不过是经过了两层包装的ThreadLocal对象:
-
第一层包装是使用
WeakReference<ThreadLocal<?>>
将ThreadLocal对象变成一个弱引用的对象 -
第二层包装是定义了一个专门的类Entry来扩展
WeakReference<ThreadLocal<?>>
;
引用类型 | 被垃圾回收(GC)的时机 | 是否可通过 get() 方法获取对象 |
典型应用场景 |
---|---|---|---|
强引用 (Strong Reference) | 永不回收(只要强引用存在) | 是 | 日常编程中的默认引用,用于持有需要长期存在的对象。 |
软引用 (SoftReference) | 内存不足时(在抛出 OutOfMemoryError 之前) |
是 | 实现内存敏感的缓存(如图片缓存、网页缓存),在内存紧张时自动释放。 |
弱引用 (WeakReference) | 下一次 GC 发生时(无论内存是否充足) | 是 | 实现非强制的映射关系(如 WeakHashMap ),防止因无用的条目积累导致内存泄漏。 |
虚引用 (PhantomReference) | 对象被 GC 时,但其回收过程会被跟踪 | 否(get() 总是返回 null ) |
跟踪对象被垃圾回收的时机,以便在回收后执行一些清理操作,如释放堆外内存。必须和引用队列ReferenceQueue结合使用 |
为什么使用弱引用?
public void func(){ThreadLocal<String> threadLocal = new ThreadLocal<>();threadLocal.set("123");threadLocal.get();
}
当方法func
执行完之后,栈帧销毁,强引用threadLocal也就没有了。但此时线程的ThreadLocalMap ThreadLocal.ThreadLocalMap threadLocals
里某个entry的key引用还指向threadLocal这个对象。
- 如果entry的这个key是强引用,就会导致key指向的ThreadLocal对象以及value不能被gc回收,造成内存泄漏
- 如果entry的这个key是弱引用,就可以使ThreadLocal对象在方法执行完毕后顺利被回收且Entry的key引用指向null。
为什么Entry里value不使用弱引用?
考量维度 | 如果Value使用弱引用 | 当前设计(Value为强引用) |
---|---|---|
数据可靠性 | 极低:Value可能在任何时候被GC回收,导致get() 返回null ,业务逻辑出错。 |
高:只要ThreadLocal强引用存在且未调用remove() ,就能保证随时取到值,业务稳定。 |
内存管理主动性 | 被动依赖GC,不可预测。 | 主动可控:通过get /set /remove 方法清理无效Entry,或在线程结束时统一释放。 |
ThreadLocalMap使用ThreadLocal的弱引用作为key,如果一个ThreadLocal没有外部强引用引用他,那么系统gc的时候,这个ThreadLocal势必会被回收,这样一来,ThreadLocalMap中就会出现key为null的Entry,就没有办法访问这些key为null的Entry的value,如果当前线程再迟迟不结束的话(比如正好用在线程池),这些key为null的Entry的value就会一直存在一条强引用链。
4.8.5 最佳实践
- ThreadLocal.withInitial(()->初始化值) ; 一定要进行初始化,避免空指针异常
- 建议把ThreadLocal修饰为static:
ThreadLocal
实例在类加载时只初始化一次 - 用完记得手动remove
总结:
- ThreadLocal并不解决线程间共享数据的问题
- ThreadLocal适用于变量在线程间隔离且在方法间共享的场景
- ThreadLocal通过隐式的在不同线程内创建独立实例副本避免了实例线程安全的问题
- 每个线程持有一个只属于自己的专属Map并维护了ThreadLocal对象与具体实例的映射,该Map由于只被持有它的线程访问,故不存在线程安全以及锁的问题
- ThreadLocalMap的Entry对ThreadLocal的引用为弱引用,避免了ThreadLocal对象无法被回收的问题
- 都会通过expunaeStaleEntry,cleanSomeSlots,replaceStaleEntrv这三个方法回收键为 null 的 Entry对象的值(即为具体实例)以及Entry对象本身从而防止内存泄漏,属于安全加固的方法
- 群雄逐鹿起纷争,人各一份天下安