为什么ThreadLocal使用频率如此之高?
-
ThreadLocal是什么?
ThreadLocal
是 Java 中的一个类,它提供了线程局部变量的存储。使用ThreadLocal
可以创建线程局部变量,这些变量是线程安全的,并且每个线程都拥有自己的独立副本,不会与其他线程共享。下面是一个简单的
ThreadLocal
使用示例:public class ThreadLocalExample { private static final ThreadLocal<Integer> threadLocal = new ThreadLocal<Integer>() { @Override protected Integer initialValue() { return 0; // 提供一个初始值 } }; public static void main(String[] args) { new IncrementThread().start(); new IncrementThread().start(); } static class IncrementThread extends Thread { @Override public void run() { for (int i = 0; i < 10; i++) { int value = threadLocal.get(); threadLocal.set(value + 1); System.out.println(Thread.currentThread().getName() + " : " + value); } } } }
在这个例子中,每个线程都会独立地对
ThreadLocal
中的值进行操作,互不影响。 -
ThreadLocal应用场景
ThreadLocal
在 Java 多线程编程中有多种应用场景,以下是一些常见的使用情况:- 线程局部变量存储:
ThreadLocal
可以用来存储线程内部的局部变量,使得同一个线程的不同函数或组件能够访问到相同的变量,而无需在函数之间传递该变量,从而减少了参数传递的复杂性16。 - 解决线程安全问题:在高并发场景下,多个线程可能会同时修改一个共享变量,使用
ThreadLocal
可以避免这种情况,因为每个线程都会有变量的独立副本,从而避免了线程间的数据竞争。 - 代替显式参数传递:在一些复杂的业务逻辑中,可以将一些参数存储在
ThreadLocal
中,避免在多层函数调用中显式传递这些参数。 - 全局存储用户信息:在前后端分离的系统中,用户信息通常存储在 Session 或 Token 中。使用
ThreadLocal
可以在拦截器中获取用户信息并存储,之后在需要的地方通过ThreadLocal
获取,简化了用户信息的获取过程。 - 管理线程私有的数据库连接:在 Spring 等框架中,可以通过
ThreadLocal
为每个线程管理自己的数据库连接,避免了多线程环境下的数据库连接冲突问题。 - 工具类对象的线程局部化:一些工具类对象,如
SimpleDateFormat
和Random
,不适合在多线程中共享使用。使用ThreadLocal
可以为每个线程创建这些工具类的实例,避免线程安全问题。 - 计数器:可以使用
ThreadLocal
作为线程安全的计数器,每个线程可以独立地对自己的计数器进行操作,互不影响。 - 与特定线程相关的状态信息:在网络编程中,如 Netty,
ThreadLocal
可以用来存储与特定Channel
相关的状态信息,因为FastThreadLocal
是ThreadLocal
的一个变种,它与 Netty 的线程模型结合使用,提供了更高效的线程局部变量存储。
需要注意的是,
ThreadLocal
不适用于线程池等场景,因为线程池中的线程是可复用的,如果ThreadLocal
使用不当,可能会导致内存泄漏。此外,使用完ThreadLocal
后,应该调用remove()
方法来清除数据,防止内存泄漏。 - 线程局部变量存储:
-
ThreadLocal原理解析
核心方法
- set(T value) :为当前线程设置一个线程局部变量的值。
- get() :获取当前线程所对应的线程局部变量。
- remove() :移除当前线程绑定的局部变量,这通常在线程结束时调用,以避免内存泄漏。
实现原理
- set 方法:将当前线程的
Thread
对象与ThreadLocal
变量的值关联起来。ThreadLocalMap
作为Thread
对象的一个属性,以ThreadLocal
实例为键,以变量的值为值进行存储。 - get 方法:从当前线程的
ThreadLocalMap
中获取与调用ThreadLocal
实例关联的值。 - initialValue 方法:返回该线程局部变量的初始值,这个方法是一个受保护的方法,可以被子类覆盖以提供不同的初始值。
- remove 方法:从
ThreadLocalMap
中移除当前ThreadLocal
实例对应的条目。
源码分析
以下是
ThreadLocal
的简化版源码分析:public void set(T value) { Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) map.set(this, value); else createMap(t, value); } 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(); } public void remove() { ThreadLocalMap m = getMap(Thread.currentThread()); if (m != null) m.remove(this); } ThreadLocalMap getMap(Thread t) { return t.threadLocals; } void createMap(Thread t, T firstValue) { t.threadLocals = new ThreadLocalMap(this, firstValue); }
在这个简化的源码中:
set()
方法首先获取当前线程对象,然后获取或创建ThreadLocalMap
,最后将ThreadLocal
实例和值存入该映射。get()
方法同样获取当前线程对象和ThreadLocalMap
,然后尝试找到与当前ThreadLocal
关联的条目,并返回其值。如果没有找到,则调用setInitialValue()
方法来设置初始值。remove()
方法移除当前线程的ThreadLocalMap
中与ThreadLocal
实例关联的条目。ThreadLocalMap
是一个特殊的HashMap
,它使用ThreadLocal
实例作为键,而线程局部变量的值作为值。
注意事项
-
由于
ThreadLocal
存储的是线程的局部变量,因此它不适用于线程池等多线程环境中的线程复用场景,否则可能导致内存泄漏。 -
应当在线程结束时调用
remove()
方法,以清除ThreadLocalMap
中的条目,避免内存泄漏。 -
ThreadLocal
适合用于保存不需要在线程间共享的数据,如线程的局部计数器、用户信息等。
-
ThreadLocal的内存泄露
ThreadLocal
导致的内存泄漏主要可以归纳为以下三种场景:- 线程池中的线程复用:在使用线程池的情况下,线程会重复执行不同的任务,而不会结束。如果
ThreadLocal
变量用于这些线程,并且没有在任务结束时通过remove()
方法清除,那么ThreadLocalMap
中的Entry
对象将保留对原始值的强引用,导致内存泄漏。 - 静态
ThreadLocal
实例:当ThreadLocal
被声明为静态变量时,它的生命周期与应用程序相同,这意味着存储在ThreadLocal
中的对象将一直存在,直到应用程序结束,从而增加了内存泄漏的风险。 - 未及时移除
ThreadLocal
值:在线程执行完毕后,如果未调用ThreadLocal
的remove()
方法来移除ThreadLocalMap
中的条目,那么即使ThreadLocal
对象本身被垃圾回收,其值仍然会被ThreadLocalMap
强引用,无法被回收,导致内存泄漏。
ThreadLocal
为了防止内存泄露,采取了以下措施:- 使用弱引用作为键(Key) :
ThreadLocalMap
中的键是ThreadLocal
实例的弱引用。当ThreadLocal
实例没有外部强引用时,它将被垃圾回收器回收,这有助于减少内存泄露的风险,因为一旦ThreadLocal
对象被回收,对应ThreadLocalMap
中的条目最终将变成孤立的条目,可以被清理。 - 提供
remove()
方法:ThreadLocal
提供了一个remove()
方法,允许开发者在不再需要某个线程局部变量时显式地移除它。这会从ThreadLocalMap
中删除对应的条目,从而释放值对象,避免内存泄露。 - 清理过期条目:在
ThreadLocalMap
的set()
、get()
和remove()
方法中,一旦检测到键(Key)为null
的条目,ThreadLocalMap
会进行清理,移除这些过期的条目,以避免内存泄露。 expungeStaleEntry()
方法:这是ThreadLocalMap
中用于清理单个过期条目(即键已经被垃圾回收的条目)的方法。它不仅清除了过期条目的值,而且还尝试重新分配其他有效条目,填补由于删除过期条目而留下的空白。cleanSomeSlots()
方法:这个方法在ThreadLocalMap
进行插入操作时被调用,尝试清理一定数量的过期条目,以维持ThreadLocalMap
的清洁。rehash()
方法:在必要时,ThreadLocalMap
会重新计算哈希表的大小,并重新分配所有条目到新的哈希表中。在这个过程中,过期的条目会被排除在外,从而实现清理。- 避免在线程池中使用:由于线程池中的线程会长时间存活并被复用,推荐在使用线程池时格外小心使用
ThreadLocal
,或者在任务执行完毕后及时清理ThreadLocal
变量。 - 建议最佳实践:建议将
ThreadLocal
变量声明为private static
,这样可以保证ThreadLocal
实例的生命周期受控,并且可以在任何时候通过其弱引用来访问和清除Entry
的值。
- 线程池中的线程复用:在使用线程池的情况下,线程会重复执行不同的任务,而不会结束。如果
-
ThreadLocal的变种
-
InheritableThreadLocal
InheritableThreadLocal
是 Java 中的一个类,它继承自ThreadLocal
类。正如其名称所示,InheritableThreadLocal
提供了一种机制,允许子线程访问父线程中设置的局部变量,即实现了线程局部变量的继承。这在多线程环境中非常有用,尤其是在需要在父线程和子线程之间传递数据时。以下是
InheritableThreadLocal
的一些关键特性:-
继承性:
InheritableThreadLocal
的关键特性是它允许子线程继承父线程中的数据。这是通过在子线程创建时复制父线程的InheritableThreadLocal
值来实现的。 -
线程隔离:尽管子线程可以访问父线程的
InheritableThreadLocal
值,但是每个线程仍然拥有自己的变量副本,互不影响,从而保持了线程间的隔离性。 -
使用场景:
InheritableThreadLocal
常用于如下场景:- 需要在线程及其子线程间传递数据,如用户身份验证信息、事务信息、日志记录上下文等。
- 服务器端处理请求时,请求上下文信息需要传递给该请求生成的任何异步处理任务。
-
内存管理:与
ThreadLocal
类似,为了避免内存泄漏,应当在适当的时候调用InheritableThreadLocal
的remove()
方法来清除不再需要的局部变量。 -
线程池中的使用:在线程池中,由于线程是可复用的,
InheritableThreadLocal
的继承特性不会自动生效。因此,如果需要在线程池中使用InheritableThreadLocal
,可能需要额外的逻辑来显式传递值。
下面是一个简单的
InheritableThreadLocal
使用示例:public class InheritableThreadLocalExample { private static final InheritableThreadLocal<String> ITL = new InheritableThreadLocal<>(); public static void main(String[] args) { ITL.set("Parent Value"); new Thread(() -> { String value = ITL.get(); // 应该能够获取到 "Parent Value" System.out.println("Value in child thread: " + value); }).start(); } }
以下是
InheritableThreadLocal
继承父线程变量副本的关键源码分析: -
ThreadLocalMap
的使用:InheritableThreadLocal
使用了ThreadLocalMap
来存储每个线程的局部变量副本。但是,与ThreadLocal
相比,InheritableThreadLocal
使用了线程的inheritableThreadLocals
字段而不是threadLocals
字段。-
childValue(T parentValue)
方法:此方法允许子线程获取父线程中InheritableThreadLocal
变量的值。如果没有重写此方法,子线程将直接继承父线程的值。protected T childValue(T parentValue) { return parentValue; }
-
getMap(Thread t)
和createMap(Thread t, T firstValue)
方法的重写:InheritableThreadLocal
重写了这两个方法,以便操作inheritableThreadLocals
而不是threadLocals
。ThreadLocalMap getMap(Thread t) { return t.inheritableThreadLocals; } void createMap(Thread t, T firstValue) { t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue); }
-
Thread
类的init()
方法:在Thread
类的构造函数中调用的init()
方法负责初始化线程的inheritableThreadLocals
。如果inheritThreadLocals
参数为true
且父线程的inheritableThreadLocals
不为null
,则子线程将获得父线程inheritableThreadLocals
的副本。private void init(ThreadGroup g, Runnable target, String name, long stackSize, AccessControlContext acc, boolean inheritThreadLocals) { // ... Thread parent = currentThread(); if (inheritThreadLocals && parent.inheritableThreadLocals != null) this.inheritableThreadLocals = ThreadLocal.createInheritedMap(parent.inheritableThreadLocals); // ... }
-
ThreadLocal.createInheritedMap(ThreadLocalMap parentMap)
静态方法:此方法用于创建一个从父线程的inheritableThreadLocals
继承而来的ThreadLocalMap
。static ThreadLocalMap createInheritedMap(ThreadLocalMap parentMap) { return new ThreadLocalMap(parentMap); }
-
清理:为了避免内存泄漏,应当在适当的时候调用
InheritableThreadLocal
的remove()
方法来清除不再需要的局部变量。
-
-
TransmittableThreadLocal
TransmittableThreadLocal
是由 Alipay(支付宝,阿里巴巴集团的金融服务部门)开源的一个 Java 工具类,它是InheritableThreadLocal
的一个增强版本,旨在解决线程池等线程复用场景下线程局部变量的传递问题。 在标准的 Java 中,InheritableThreadLocal
允许父线程的变量值传递给子线程,这在新线程由父线程直接创建时是有用的。但是,在线程池这种线程复用的环境中,线程的创建和使用是分离的,因此InheritableThreadLocal
无法满足在任务提交时传递ThreadLocal
值到任务执行时的需求。TransmittableThreadLocal
的主要特点包括:
- 跨线程传递:它支持在提交任务到线程池时捕获
ThreadLocal
值,并在任务执行时恢复这些值,从而实现跨线程传递。 - 无侵入:它可以通过 Java Agent 技术无侵入地修改 JDK 线程池实现,使得业务代码无需关心线程局部变量的传递。
- 简单易用:提供了简洁的 API 来设置、获取和移除变量,同时提供了工具类
TtlRunnable
和TtlCallable
来包装任务,以便在线程池中使用。 - 透明执行:通过重写
childValue
方法,可以在子线程中定制变量值的传递行为,而不是简单地继承父线程的值。 - 生命周期管理:提供了
beforeExecute
和afterExecute
等生命周期回调方法,允许在任务执行前后进行额外的处理。TransmittableThreadLocal
是中间件和框架开发者的一个有用工具,它解决了多线程环境中的上下文传递问题,特别适用于需要在异步任务或分布式系统中保持上下文信息的场景。
TransmittableThreadLocal
(TTL)的实现原理涉及到几个关键的类和方法,以下是对这些关键组件的源码分析:TransmittableThreadLocal
类的实现:TransmittableThreadLocal
继承自InheritableThreadLocal
,重写了childValue
方法来定制子线程中变量值的传递行为。它使用一个静态的WeakHashMap
来持有TransmittableThreadLocal
实例的引用。holder
静态变量:holder
是一个InheritableThreadLocal<WeakHashMap<TransmittableThreadLocal<Object>, Object>>
,用来存储TransmittableThreadLocal
实例和它们的值。使用WeakHashMap
是为了确保当TransmittableThreadLocal
实例没有其他强引用时,它可以被垃圾回收,防止内存泄漏。addThisToHolder
方法: 当TransmittableThreadLocal
被设置时,通过addThisToHolder
方法将当前实例添加到holder
中。Transmitter
内部类的实现:Transmitter
内部类提供capture
、replay
和restore
三个静态方法,用于处理TransmittableThreadLocal
值的捕获、回放和恢复。capture
方法:capture
方法遍历所有TransmittableThreadLocal
实例,并将它们的值存储到Snapshot
对象中。replay
方法:replay
方法接收一个Snapshot
对象,将存储的值回放到当前线程的TransmittableThreadLocal
中,并返回一个包含当前线程状态的Snapshot
对象。restore
方法:restore
方法使用replay
方法返回的Snapshot
对象来恢复线程的状态。TtlRunnable
和TtlCallable
类: 这些类包装了提交给线程池的任务,确保在任务执行前,父线程的TransmittableThreadLocal
值被传递到子线程。- Java Agent 技术: TTL 支持通过 Java Agent 技术无侵入地修改 JDK 线程池实现,这是通过字节码增强实现的,可以在不修改线程池代码的情况下实现
TransmittableThreadLocal
的传递。
以下是
TransmittableThreadLocal
和相关类的简化示例代码:public class TransmittableThreadLocal<T> extends InheritableThreadLocal<T> { // ... 省略其他代码 ... private static final InheritableThreadLocal<Map<TransmittableThreadLocal<?>, Object>> holder = new InheritableThreadLocal<>(); @Override protected T childValue(T parentValue) { // 子线程获取父线程的值 return parentValue; } public void set(T value) { super.set(value); // 添加到 holder 中 addThisToHolder(); } private void addThisToHolder() { Map<TransmittableThreadLocal<?>, Object> map = holder.get(); if (map == null) { map = new HashMap<>(); holder.set(map); } map.put(this, null); } // ... 省略其他代码 ... } public static class Transmitter { public static Object capture() { // 创建 Snapshot 对象,捕获当前线程的所有 TTL 值 return new Snapshot(captureTtlValues(), captureThreadLocalValues()); } public static Object replay(Object captured) { // 回放之前捕获的 TTL 值 return new Snapshot(replayTtlValues(captured), replayThreadLocalValues(captured)); } public static void restore(Object backup) { // 恢复之前备份的 TTL 值 restoreTtlValues((Snapshot) backup); } // ... 省略其他代码 ... } final static class Snapshot { private final Map<TransmittableThreadLocal<?>, Object> ttl2Value; // ... 省略其他代码 ... } // 使用 TransmittableThreadLocal 的示例 TransmittableThreadLocal<String> context = new TransmittableThreadLocal<>(); context.set("parentValue"); Object captured = TransmittableThreadLocal.Transmitter.capture(); // 执行线程池任务 TransmittableThreadLocal.Transmitter.replay(captured); try { // 执行业务逻辑 } finally { // 恢复之前的 TransmittableThreadLocal 值 TransmittableThreadLocal.Transmitter.restore(captured); }
在实际使用中,开发者需要确保在任务执行完毕后调用
restore
方法来清理和恢复线程状态,避免影响线程池中其他任务的执行。-
FastThreadLocal
FastThreadLocal
是 Netty 框架中的一个类,它是一个特殊的ThreadLocal
变体,旨在提供比 JDK 自带的ThreadLocal
更高的访问性能。FastThreadLocal
通过使用数组和固定索引的方式,而不是 JDKThreadLocal
中的哈希表,从而减少了哈希冲突和提高了访问速度。这种方式特别适合于高并发场景下频繁访问线程局部变量的情况。以下是
FastThreadLocal
的一些关键特点:- 性能优势:由于内部使用数组和固定索引,
FastThreadLocal
可以提供接近常数时间复杂度 O(1) 的读写操作,这比 JDK 的ThreadLocal
快,后者可能因为哈希冲突而导致性能下降。 - 内存池化:
FastThreadLocal
使用内存池来减少内存占用和性能开销,通过重用线程局部变量的实例来避免频繁的创建和销毁操作。 - 安全清理:
FastThreadLocal
提供了remove()
方法来主动清除不再使用的线程局部变量,并且在线程执行完毕时会自动清理所有绑定在当前线程上的所有FastThreadLocal
对象,减少了内存泄漏的风险。 - 使用条件:为了充分利用
FastThreadLocal
的性能优势,访问它的线程必须是FastThreadLocalThread
或其子类。Netty 通过DefaultThreadFactory
创建的线程默认就是FastThreadLocalThread
类型。 - 适用场景:
FastThreadLocal
适用于那些需要频繁读写线程局部变量的场景,尤其是在网络服务器和客户端等高性能网络应用中。 - 字节填充:
FastThreadLocal
利用字节填充来解决伪共享问题,提高缓存效率。 - 测试结果:在 Netty 的测试中,
FastThreadLocal
的吞吐量可以达到 JDK 原生ThreadLocal
的 3 倍左右,但在普通线程中使用可能更慢。 - 注意:从 Netty 的某个版本开始,
FastThreadLocal
不再使用ObjectCleaner
处理内存泄漏,而是建议在必要时重写onRemoval
方法。
FastThreadLocal
之所以比 JDK 的ThreadLocal
快,主要原因如下:- 数组和固定索引:
FastThreadLocal
使用一个数组indexedVariables
和每个FastThreadLocal
实例的固定索引来存储和访问线程局部变量,这种方法减少了哈希冲突的可能性,并且访问速度更快。 - 减少内存开销:由于避免了哈希表的使用,
FastThreadLocal
减少了内存开销,因为它不需要为每个ThreadLocal
对象分配哈希表条目。 - 内存池化:
FastThreadLocal
可以使用内存池技术来减少内存分配和垃圾收集的开销。 - 自动清理:在
FastThreadLocalThread
执行完��后,FastThreadLocal
会自动清理所有绑定在当前线程上的所有FastThreadLocal
变量,减少了内存泄漏的风险。 - 避免伪共享:
FastThreadLocal
使用内存填充(padding)来避免 CPU 缓存伪共享问题。
具体源码分析
以下是
FastThreadLocal
的一些关键源码片段,展示了它是如何实现上述功能的:FastThreadLocal
的构造函数:为每个FastThreadLocal
实例分配一个唯一的索引。
public class FastThreadLocal<T> { final int index; // 每个 FastThreadLocal 实例都有一个唯一的索引 final InternalThreadLocalMap<T> map; // 持有 InternalThreadLocalMap 的引用 public FastThreadLocal() { index = InternalThreadLocalMap.nextVariableIndex(); // 分配一个索引 map = InternalThreadLocalMap.get(); // 获取当前线程的 InternalThreadLocalMap } // ... }
InternalThreadLocalMap
的get()
方法:获取当前线程的InternalThreadLocalMap
实例。
public final class InternalThreadLocalMap<T> { // 存储所有 FastThreadLocal 变量的数组 static final Object UNSET = new Object(); private Object[] indexedVariables = newIndexedVariableTable(); public static <V> InternalThreadLocalMap<V> get() { Thread currentThread = Thread.currentThread(); if (currentThread instanceof FastThreadLocalThread) { return ((FastThreadLocalThread) currentThread).threadLocalMap(); } else { return slowGet(); // 非 FastThreadLocalThread 线程的回退逻辑 } } private static Object[] newIndexedVariableTable() { Object[] array = new Object[32]; for (int i = 0; i < array.length; i++) { array[i] = UNSET; } return array; } // ... }
FastThreadLocal
的get()
和set()
方法:通过索引快速获取和设置线程局部变量的值。
public final T get() { Object[] variables = map.indexedVariables; return (T) variables[index]; } public final void set(T value) { Object[] variables = map.indexedVariables; variables[index] = value; }
- 避免伪共享:
FastThreadLocal
对象通过内存填充来确保每个对象的大小超过 CPU 缓存行的大小,从而避免伪共享。
// 伪共享的填充示例(具体实现可能会有所不同) public class FastThreadLocal<T> { // ... // 填充字段,以确保对象大小超过缓存行大小 private long p0, p1, p2, p3, p4, p5, p6, p7; // ... }
remove()
方法:移除线程局部变量,减少内存泄漏的风险。
public final void remove() { map.remove(index); }
通过上述源码分析,我们可以看到
FastThreadLocal
在设计上避免了ThreadLocal
的一些性能瓶颈,如哈希冲突和锁竞争,同时通过内存池化和自动清理机制,提高了性能并减少了内存泄漏的风险。此外,通过内存填充避免伪共享,进一步提高了缓存效率。 -
-
总结
ThreadLocal
是JDK自带的实现,用于提供线程局部变量存储,适用于在单个线程内传递数据的场景。ThreadLocal
无法满足在子线程间进行数据传递,而往往现在的应用服务都会出现在父子线程的情况,因此要注意评估。同时,要注意评估内存泄露的问题。内存泄露大多数发生在不被销毁的线程、复用的线程,例如线程池的复用线程,虽然key是虚引用,但是value值还是强引用,虽然会通过get、set、remove进行主动探测清理,但仍需评估极端情况,在使用上注意及时显示remove。InheritableThreadLocal
也是JDK自带的实现,它主要解决了父子线程间的数据传递问题,但是在线程池中,由于线程是可复用的,InheritableThreadLocal
的继承特性不会自动生效。因此,实际上用的并不是很多。TransmittableThreaLocal
是阿里开源的一个用于解决线程池传递ThreadLocal数据问题的实现。它通过提前存储数据、然后通过TtlRunnable
和TtlCallable
对任务进行包装,在run的时候进行数据的回放设置到当前线程中,在线程执行结束后又通过restore恢复线程之前状态。实现了复用线程的ThreadLocal数据传递。TransmittableThreaLocal
是 Netty 框架中的一个类,它的主要特点是旨在提供比 JDK 自带的ThreadLocal
更高的访问性能。它通过使用数组和固定索引的方式,而不是 JDKThreadLocal
中的哈希表,从而减少了哈希冲突和提高了访问速度。通过字节填充避免伪共享。
转载自:https://juejin.cn/post/7362057695975440423