ThreadLocal
约 7107 字大约 24 分钟
2025-03-12
介绍一下 ThreadLocal
Thread类有两个变量:threadLocals和inheritableThreadLocals
这两个变量默认为null,只有当该线程调用了ThreadLocal类的get/set方法时才会创建他们,而调用ThreadLocal的get/set实际上是调用ThreadLocalMap的get/set
ThreadLocalMap可理解成给ThreadLocal定制化的HashMap
最终的变量放在了线程的ThreadLocalMap中,而不是ThreadLocal中,ThreadLocal只是对其进行封装,向其传递变量值
假设一个请求中,A方法调->B方法->调C方法->调D方法的情况。
假设D方法需要使用在入口处拿到的userid参数,那没有ThreadLocal时,我们需要把userid一步一步通过方法参数的方式,传递给D; 而用threadlocal的方式,在A方法存进去,D方法直接从threadlocal取就行,不用通过中间方法的传递。
所以ThreadLocal一般用来存储userId,traceId等在整个线程内随时可能存取的内容
ThreadLocal 内存泄露问题了解吗
ThreadLocal 内存泄露也是个老生常谈的问题了,网上部分资料,包括好多面试官都把这个问题出现的主要原因归结为 ThreadLocalMaps 里 Entry Key(ThreadLocal 对象本身)使用了弱引用导致的,但是我们仔细看看引用结构,脑补一下内存泄露的场景就不难发现,真正导致内存泄露的主要原因,其实是 Thread 强引用 ThreadLocalMaps,如果 Thread 一直存在,ThreadLocalMaps Entry 中的 value 这个强引用一直存在,不被回收才是导致发生内存泄露的真正原因。
因为 ThreadLocal 本身不存储对象,是调用 Thread 中的 ThreadLocalMaps 来保存,而 Thread 强引用 ThreadLocalMaps 对象,如果 Thread 对象生命周期过长,不能及时被回收,就会导致 ThreadLocalMaps 对象里 Entry 的 value 存在内存泄露的可能
当然 ThreadLocalMaps 在设计的时候也考虑过这个问题,所以 ThreadLocalMaps的Key 采用了弱引用的方式,并且在 set、remove、rehash 的时候会主动清理 ThreadLocalMaps 中 Key 为 Null 的 value,但是如果 ThreadLocal 已经不被使用了,set、remove、rehash 方法也不会被调用,与此同时,如果这个线程又一直存活、不终止的话,那么一ThreadLocalMaps 中的 Value 强引用就会一直存在,也就避免不了 Value 的内存泄漏。
弱引用一般是用来描述非必需对象的,被弱引用关联的对象只能生存到下一次垃圾收集发生之前。当垃圾收集器工作时,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。
实际开发中,当我们不需要 threadlocal 后,为了 GC 将 threadlocal 变量置为 null,没有任何强引用指向堆中的 threadlocal 对象时,堆中的 threadlocal 对象将会被 GC 回收,假设现在 Key 持有的是 threadlocal 对象的强引用,如果当前线程仍然在运行,那么从当前线程一直到 threadlocal 对象还是存在强引用,由于当前线程仍在运行的原因导致 threadlocal 对象无法被 GC,这就发生了内存泄漏 。相反,弱引用就不存在此问题,当栈中的 threadlocal 变量置为 null 后,堆中的 threadlocal 对象只有一个 Key 的弱引用关联,下一次 GC 的时候堆中的 threadlocal 对象就会被回收,使用弱引用对于 threadlocal 对象而言是不会发生内存泄漏的。
那为什么不把value也设置为弱引用呢?这样不就可以和key一起回收了
假设 key 所引用的 ThreadLocal 对象还被其他的引用对象强引用着,那么这个 ThreadLocal 对象就不会被 GC 回收,但如果 value 是弱引用且不被其他引用对象引用着,那 GC 的时候就被回收掉了,那线程通过 ThreadLocal 来获取 value 的时候就会获得 null,显然这不是我们希望的结果。因为对我们来说,value 才是我们想要保存的数据,ThreadLocal 只是用来关联 value 的,如果 value 都没了,还要 ThreadLocal 干嘛呢?所以 value 不能是弱引用。
key是自变量,value是因变量,所以应该变key,不要主动变value。更重要的原因是key是共享的,而value是独享的,所以把value置为null后强引用就消失了,就可以被GC,就不会导致内存泄漏。把key置为null后当前线程依然强引用着key,这就导致了内存泄漏。
ThreadLocal在调用set、remove、rehash方法的时候实际上是先获得当前线程的threadlocalmap,利用map的set、remove、rehash方法来清除key为null的value。
为什么用ThreadLocal不用线程成员变量?
如果用成员变量,那么成员变量必须在Thread里,不能在Runnable里,因为一个Runnable对象可以被多个Thread执行。
而如果在Thread中添加成员变量,就要加强Thread和Runnable的耦合,将Thread作为Runnable的成员变量,并在Runnable中调用具体的Thread变量,如果执行Runnable的Thread可能有很多子类,不同子类有不同的成员变量,则要在run方法中进行复杂处理,扩展性较低,不利于维护。而ThreadLocal就是将成员变量统一为一个Map放到线程里。
就是变量在Thread里,但又需要在Runnable任务中执行时使用,就得传进去。
本质就是在讲,现在有a,b,c三个变量值,你需要在run方法中使用,那这三变量放哪里比较合适。
是放threadlocal还是作为thread的成员变量,这两种方式在run方法中进行使用。
然后这里探讨的是这两种方法使用上的差别。
其实使用thread成员变量的方式很少,这里实在难理解就过吧,了解下就行。
所以说实际上thread就是通过成员变量的形式来存储线程独有的数据,只是提供好了threadlocal这样完善的一个线程数据存取工具,隐藏了保证线程间的隔离性的具体细节等,我们直接通过threadlocal来进行存取。是这样子吗
解释一下ThreadLocal
ThreadLocal
是 Java 中用于为每个线程提供独立变量副本的类。每个线程都可以独立地访问和修改这个变量,而不会影响其他线程的值。这在多线程编程中非常有用,特别是在需要线程隔离的场景下,ThreadLocal
可以提供一种简便的方式来避免线程间共享变量引起的并发问题。
1. ThreadLocal
的基本概念
ThreadLocal
类允许我们为每个线程存储独立的变量副本。每个线程可以通过 ThreadLocal
的实例来获取和修改自己独立的变量副本,而不用担心其他线程的干扰。
- 每个线程都有自己的局部变量副本,不同线程之间的副本相互独立。
ThreadLocal
提供了线程安全的变量存储机制,避免了传统同步机制的复杂性。
2. 工作原理
ThreadLocal
的工作原理是基于每个线程维护的一个独立的 ThreadLocalMap
对象。这个 ThreadLocalMap
保存在每个线程的 Thread
实例中,它用 ThreadLocal
作为键,用线程对应的值作为值来存储每个线程的变量副本。
具体步骤:
- 当一个线程第一次调用
ThreadLocal.set()
时,线程会在自己的ThreadLocalMap
中存储该ThreadLocal
对象和对应的值。 - 当同一线程再次调用
ThreadLocal.get()
时,线程会从ThreadLocalMap
中查找对应的值。 - 不同线程都有自己的
ThreadLocalMap
,因此每个线程访问和修改ThreadLocal
中的值都是独立的,互不影响。
3. ThreadLocal
的常用方法
set(T value)
: 设置当前线程的本地变量副本的值。get()
: 获取当前线程的本地变量副本的值。如果该线程是第一次调用get()
,且没有调用过set()
方法,则返回initialValue()
方法的返回值(默认是null
,除非重写initialValue()
方法)。remove()
: 移除当前线程的本地变量副本,避免内存泄漏。initialValue()
: 用于返回当前线程的初始值。可以通过重写此方法来为每个线程提供默认值。
4. 使用示例
以下是一个使用 ThreadLocal
来存储每个线程独立变量的简单示例:
public class ThreadLocalExample {
// 创建一个 ThreadLocal 对象,用于存储每个线程的独立变量副本。注意没有new!
private static ThreadLocal<Integer> threadLocalValue = ThreadLocal.withInitial(() -> 0);
public static void main(String[] args) {
// 创建两个线程,每个线程都会独立地操作自己的 threadLocalValue
Thread thread1 = new Thread(() -> {
threadLocalValue.set(100);
System.out.println(Thread.currentThread().getName() + " initial value: " + threadLocalValue.get());
threadLocalValue.set(threadLocalValue.get() + 1);
System.out.println(Thread.currentThread().getName() + " updated value: " + threadLocalValue.get());
});
Thread thread2 = new Thread(() -> {
threadLocalValue.set(200);
System.out.println(Thread.currentThread().getName() + " initial value: " + threadLocalValue.get());
threadLocalValue.set(threadLocalValue.get() + 1);
System.out.println(Thread.currentThread().getName() + " updated value: " + threadLocalValue.get());
});
thread1.start();
thread2.start();
}
}
输出示例:
Thread-0 initial value: 100
Thread-0 updated value: 101
Thread-1 initial value: 200
Thread-1 updated value: 201
在上面的示例中,两个线程 thread1
和 thread2
各自有自己独立的 threadLocalValue
,它们的值相互独立,互不影响。ThreadLocal
确保每个线程有自己的变量副本。
5. 典型使用场景
ThreadLocal
通常用于以下场景:
5.1 数据库连接或会话管理
在数据库操作时,ThreadLocal
可以用来管理每个线程的数据库连接,使得每个线程使用自己的数据库连接,而不是共享同一个连接,避免了线程安全问题。
Spring提供了事务相关的操作,而我们知道事务是得保证一组操作同时成功或失败的。这意味着我们一次事务的所有操作需要在同一个数据库连接上,但是在我们日常写代码的时候是不需要关注这点的。Spring就是用的ThreadLocal来实现,ThreadLocal存储的类型是一个Map,Map中的key 是DataSource,value 是Connection(为了应对多数据源的情况,所以是一个Map),用了ThreadLocal保证了同一个线程获取一个Connection对象,从而保证一次事务的所有操作需要在同一个数据库连接上。
public class DatabaseConnectionManager {
private static ThreadLocal<Connection> connectionHolder = ThreadLocal.withInitial(() -> {
// 初始化数据库连接
return createNewConnection();
});
public static Connection getConnection() {
return connectionHolder.get();
}
}
5.2 用户上下文
在 web 应用中,ThreadLocal
可以用来存储每个线程的用户上下文信息,例如用户的 session
或 request
对象,这样可以避免显式传递这些信息。
public class UserContext {
private static ThreadLocal<String> currentUser = new ThreadLocal<>();
public static void setCurrentUser(String user) {
currentUser.set(user);
}
public static String getCurrentUser() {
return currentUser.get();
}
}
5.3 避免参数传递
有时在需要多层方法调用中传递数据,可以通过 ThreadLocal
来避免通过方法参数显式传递数据。每个线程可以在调用链中共享 ThreadLocal
中的数据。
6. ThreadLocal
的内存泄漏问题
ThreadLocal
存在潜在的内存泄漏风险,尤其是在使用线程池的场景下。由于线程池中的线程会被重复使用,ThreadLocal
对象可能会继续持有某些线程的局部变量,即使它们已经不再需要。为了避免这种情况,应该在每次使用完 ThreadLocal
后调用 remove()
方法,显式地清除线程的本地变量。
示例:正确清理 ThreadLocal
try {
// 使用 ThreadLocal
threadLocalValue.set(someValue);
// 执行逻辑
} finally {
// 清理 ThreadLocal,避免内存泄漏
threadLocalValue.remove();
}
7. 总结:ThreadLocal
的优缺点
优点:
- 提供了线程安全的局部变量副本,避免了使用锁的复杂性。
- 每个线程拥有自己独立的变量,不会受到其他线程的影响,非常适合处理线程隔离的数据。
缺点:
- 如果不正确使用
remove()
,在长时间运行的应用中可能会导致内存泄漏,尤其是在线程池中。 - 使用过多的
ThreadLocal
可能使代码难以调试和维护,因为变量的值是隐式在线程内部管理的。
总结
ThreadLocal
是一种为每个线程提供独立的局部变量副本的机制,避免了线程间的共享和竞争。它适用于需要线程隔离的场景,比如数据库连接、用户会话和上下文信息。在使用 ThreadLocal
时,要注意清理工作,以防止内存泄漏。
在内存中,ThreadLocal、Thread实例、threadLocals变量和ThreadLocalMap到底是怎么存储的?它们的关系是什么?
在 Java 中,ThreadLocal
的存储机制设计得非常精妙,它通过每个 Thread
实例中的 ThreadLocalMap
来存储线程的局部变量。了解 ThreadLocal
、Thread
实例、threadLocals
变量以及 ThreadLocalMap
的存储结构和它们的关系,有助于理解 ThreadLocal
的工作原理,尤其是如何做到每个线程有独立的局部变量。
基本关系与存储结构
ThreadLocal
实例:
ThreadLocal
实例本身并不直接存储数据,它只是一个工具类,提供对线程局部变量的访问方式。- 每个
ThreadLocal
实例为当前线程的局部变量创建一个唯一的键,这个键用于访问存储在线程中的变量副本。
Thread
实例:
- 每个线程都有一个
Thread
对象实例,它包含线程的各种属性和状态信息。 Thread
对象中有一个ThreadLocalMap
,用于存储线程的局部变量副本。这个ThreadLocalMap
是ThreadLocal
机制的核心存储结构。
threadLocals
变量:
- 在
Thread
类中,有一个名为threadLocals
的成员变量,它的类型是ThreadLocalMap
。 threadLocals
是ThreadLocalMap
的引用,每个线程都有自己独立的ThreadLocalMap
实例,用于存储该线程的所有ThreadLocal
变量。
ThreadLocalMap
:
ThreadLocalMap
是一个专门设计的映射结构,存储当前线程的所有ThreadLocal
变量及其对应的值。ThreadLocalMap
使用ThreadLocal
作为键,存储值为线程的局部变量副本。- 其内部存储的每个键值对称为
Entry
,Entry
的键是弱引用的ThreadLocal
实例,值是该线程对应的变量副本。
存储过程的解释
当一个线程第一次访问 ThreadLocal
时,它的局部变量是存储在该线程的 ThreadLocalMap
中的。这个存储过程如下:
- 线程启动时:
- 每个线程启动时,JVM 会为该线程创建一个
Thread
实例。这个Thread
实例包含一个名为threadLocals
的字段,最初threadLocals
是null
,意味着线程尚未存储任何ThreadLocal
变量。
- 每个线程启动时,JVM 会为该线程创建一个
- 第一次调用
ThreadLocal.set()
时:- 当线程第一次调用
ThreadLocal.set()
,ThreadLocal
先检查当前线程的threadLocals
是否为null
。 - 如果
threadLocals
为null
,说明当前线程还没有初始化ThreadLocalMap
,此时会为该线程创建一个新的ThreadLocalMap
实例,并将其赋值给threadLocals
。 - 然后,
ThreadLocal
使用自己作为键,将需要存储的值放入ThreadLocalMap
中。
- 当线程第一次调用
- 访问时 (
ThreadLocal.get()
):- 当线程调用
ThreadLocal.get()
获取值时,ThreadLocal
会通过当前线程的ThreadLocalMap
查找对应的值。 - 通过
ThreadLocal
实例作为键,查询ThreadLocalMap
中的Entry
,找到该线程对应的变量副本并返回。
- 当线程调用
内存中存储结构的关系
在内存中,这些对象和它们的关系如下:
Thread
实例:每个线程都对应一个Thread
实例,这个实例中包含一个ThreadLocalMap
。ThreadLocalMap
实例:ThreadLocalMap
是Thread
的一个内部类,它存储该线程的所有ThreadLocal
变量。ThreadLocalMap
由键值对(Entry
)组成,每个Entry
包含一个ThreadLocal
作为键,和该ThreadLocal
对应的值。ThreadLocal
实例:ThreadLocal
是存储线程局部变量的工具。每个ThreadLocal
实例通过ThreadLocalMap
存储或获取当前线程的变量副本。- 存储在
ThreadLocalMap
中的Entry
:Entry
的键是ThreadLocal
的弱引用,值是当前线程的变量副本。- 键为弱引用意味着,当
ThreadLocal
对象不再使用时,GC 可以回收这个引用,避免内存泄漏。
示意图:
+------------------+
| Thread | Thread 1
| |-------------------> threadLocals (ThreadLocalMap)
| threadLocals | |
+------------------+ |
v
+-----------------+
| ThreadLocalMap |
| Entry[] |
+-----------------+
| Key: ThreadLocal |--> Weak Reference
| Value: data |
+-----------------+
+------------------+
| Thread | Thread 2
| |-------------------> threadLocals (ThreadLocalMap)
| threadLocals | |
+------------------+ |
v
+-----------------+
| ThreadLocalMap |
| Entry[] |
+-----------------+
| Key: ThreadLocal |--> Weak Reference
| Value: data |
+-----------------+
具体示例解释
public class ThreadLocalExample {
// threadLocal 就是ThreadLocal实例,也就是ThreadLocalMap中的Entry中的key。
private static ThreadLocal<String> threadLocal = ThreadLocal.withInitial(() -> "Initial Value");
public static void main(String[] args) {
Thread thread1 = new Thread(() -> {
System.out.println(Thread.currentThread().getName() + " - " + threadLocal.get());
threadLocal.set("Thread 1 Value");
System.out.println(Thread.currentThread().getName() + " - " + threadLocal.get());
});
Thread thread2 = new Thread(() -> {
System.out.println(Thread.currentThread().getName() + " - " + threadLocal.get());
threadLocal.set("Thread 2 Value");
System.out.println(Thread.currentThread().getName() + " - " + threadLocal.get());
});
thread1.start();
thread2.start();
}
}
执行过程:
- 线程 1 启动时:
threadLocal.get()
会初始化线程的ThreadLocalMap
,并设置初始值"Initial Value"
。ThreadLocalMap
将ThreadLocal
实例作为键,"Initial Value"
作为值存入Entry
中。- 然后调用
threadLocal.set("Thread 1 Value")
,此时线程 1 的ThreadLocalMap
会将值更新为"Thread 1 Value"
。
- 线程 2 启动时:
threadLocal.get()
会为线程 2 创建自己的ThreadLocalMap
,并存储"Initial Value"
。- 然后调用
threadLocal.set("Thread 2 Value")
,更新其ThreadLocalMap
中的值为"Thread 2 Value"
。
每个线程的 ThreadLocalMap
是独立的,它们之间互不干扰。
内存泄漏风险与解决方案
由于 ThreadLocalMap
使用 ThreadLocal
的弱引用作为键,一旦 ThreadLocal
对象没有被引用时,GC 会回收该 ThreadLocal
,但对应的值如果没有被清理,可能会导致内存泄漏。
- 内存泄漏的风险:当
ThreadLocal
被回收后,ThreadLocalMap
中的值可能仍然存在,无法被 GC 清理,从而导致内存泄漏。 - 解决方案:在使用完
ThreadLocal
后,应该主动调用remove()
方法,显式地清理ThreadLocalMap
中的值,避免内存泄漏。
threadLocal.remove(); // 显式清除,防止内存泄漏
总结
ThreadLocal
是为每个线程提供独立变量副本的机制,解决了多线程并发访问时的共享数据问题。Thread
实例 包含一个ThreadLocalMap
,用于存储该线程的所有ThreadLocal
变量。ThreadLocalMap
使用ThreadLocal
的弱引用作为键,值是线程的局部变量副本。- 为防止内存泄漏,应该在适当的时候调用
ThreadLocal.remove()
方法清理资源。
刚初始化完ThreadLocalMap时,ThreadLocalMap中有几个Entry?
在 ThreadLocalMap
初始化完成时,ThreadLocalMap
中的 Entry
数量为 1 或 0,具体情况取决于 ThreadLocal
的使用方式。
- 如果在初始化
ThreadLocalMap
时,已经调用了ThreadLocal.set()
或ThreadLocal.withInitial()
,那么ThreadLocalMap
中会包含 1 个Entry
,这个Entry
存储的是当前线程的ThreadLocal
实例作为键,以及对应的初始值作为值。 - 如果只创建了
ThreadLocal
实例,但没有调用set()
或withInitial()
,ThreadLocalMap
并不会立即初始化。因此,ThreadLocalMap
为空,此时ThreadLocalMap
中没有Entry
。
详细解释
调用 ThreadLocal.withInitial()
或 ThreadLocal.set()
的情况
如果你在使用 ThreadLocal
时调用了 ThreadLocal.withInitial()
或 ThreadLocal.set()
,会触发 ThreadLocalMap
的初始化过程。此时,ThreadLocalMap
中会包含一个 Entry
,存储当前线程的 ThreadLocal
实例和它对应的初始值。
public class ThreadLocalExample {
private static ThreadLocal<String> threadLocal = ThreadLocal.withInitial(() -> "Initial Value");
public static void main(String[] args) {
Thread thread = new Thread(() -> {
System.out.println(threadLocal.get());
});
thread.start();
}
}
在这个例子中:
- 当
ThreadLocal.withInitial()
被调用时,ThreadLocal
会初始化ThreadLocalMap
,并将"Initial Value"
存储到当前线程的ThreadLocalMap
中。 - 因此,
ThreadLocalMap
中会有 1 个Entry
。
未调用 ThreadLocal.set()
或 withInitial()
的情况
如果你只是创建了 ThreadLocal
实例,但没有调用 set()
或 withInitial()
,则 ThreadLocalMap
不会立即被创建,直到你显式调用 ThreadLocal.set()
或 get()
时,ThreadLocalMap
才会被初始化。此时,如果你从未设置任何值,ThreadLocalMap
为空。
public class ThreadLocalExample {
private static ThreadLocal<String> threadLocal = new ThreadLocal<>();
public static void main(String[] args) {
Thread thread = new Thread(() -> {
System.out.println(threadLocal.get()); // 没有初始化,输出 null
});
thread.start();
}
}
在这个例子中:
- 因为没有调用
ThreadLocal.set()
或withInitial()
,ThreadLocalMap
在没有显式初始化时不会创建。 - 调用
threadLocal.get()
时,返回null
,ThreadLocalMap
中没有Entry
。
总结
- 如果调用了
ThreadLocal.set()
或ThreadLocal.withInitial()
:ThreadLocalMap
会在第一次调用时被初始化,并包含 1 个Entry
。 - 如果没有调用
set()
或withInitial()
:ThreadLocalMap
中没有Entry
,初始化时为空。
remove方法清除的是entry中的key还是value还是把整个entry清除了?
ThreadLocal
的 remove()
方法会清除整个 Entry
中的 value
,但并不会完全删除整个 Entry
对象。实际上,remove()
方法只会将 Entry
的 key
和 value
清除为 null
,并保留 Entry
对象本身。
具体来说:
- 清除
Entry
的key
:ThreadLocalMap
中的Entry
键(key
)是ThreadLocal
对象的弱引用。- 当
remove()
方法被调用时,它会清除当前线程ThreadLocalMap
中与该ThreadLocal
实例对应的Entry
,也就是将这个Entry
的键设为null
。 - 由于键是弱引用,如果
ThreadLocal
实例没有其他强引用,垃圾收集器(GC)会自动回收ThreadLocal
对象。
- 清除
Entry
的value
:remove()
方法会显式将Entry
中的value
设为null
,以便释放这个值占用的内存,防止内存泄漏。
- 保留
Entry
:Entry
本身并不会被立即移除,而是保持在ThreadLocalMap
数组中,等待后续的垃圾回收或ThreadLocalMap
的整理操作。ThreadLocalMap
采用的是延迟清理机制,当发现Entry
的键为null
且value
为空时,会在合适的时机清除这些无效的Entry
。
源码解析
下面是 ThreadLocal
类中 remove()
方法的简化实现:
public void remove() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t); // 获取当前线程的 ThreadLocalMap
if (map != null)
map.remove(this); // 在 ThreadLocalMap 中移除与当前 ThreadLocal 相关的 Entry
}
ThreadLocalMap
的 remove()
方法大致如下:
private void remove(ThreadLocal<?> key) {
Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len - 1); // 根据 key 计算位置
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();
if (k == key) {
e.clear(); // 将当前 Entry 的 key 和 value 设为 null
expungeStaleEntry(i); // 清除无效的 Entry
return;
}
if (k == null) // 若发现键为 null 的 Entry,进行垃圾清理
expungeStaleEntry(i);
}
}
e.clear()
:清除Entry
,即将其key
和value
设为null
。expungeStaleEntry()
:尝试清理已经失效的Entry
,比如键为null
的情况。
结论
ThreadLocal.remove()
方法会将当前线程的ThreadLocalMap
中与该ThreadLocal
对应的Entry
的 键 和 值 置为null
。- 这个操作不会立即移除整个
Entry
对象,但会标记它为可清除的。当 GC 发现Entry
的键为null
时,会进行清理,最终释放内存。
父子线程怎么共享数据?
在 Java 中,父线程和子线程之间的数据共享是一个常见的问题,尤其是在并发编程的场景下。尽管每个线程都有自己的独立执行路径,但有多种方式可以让父线程和子线程共享数据。以下是几种常见的实现方法:
1. 使用共享的变量
最直接的方法是让父线程和子线程访问相同的共享变量。这些共享变量可以是全局变量、静态变量、或者是在父线程中创建的对象并传递给子线程。共享变量的方式比较简单,但需要注意线程安全问题,通常需要使用同步机制(如 synchronized
、volatile
、Lock
等)来防止数据竞争和不一致性。
示例:使用共享变量
public class SharedVariableExample {
// 共享变量
private static int sharedData = 0;
public static void main(String[] args) {
Thread parentThread = new Thread(() -> {
// 父线程设置共享变量
sharedData = 100;
System.out.println("Parent Thread sets sharedData to: " + sharedData);
});
Thread childThread = new Thread(() -> {
// 子线程访问共享变量
System.out.println("Child Thread reads sharedData: " + sharedData);
});
parentThread.start();
try {
parentThread.join(); // 确保父线程执行完后再启动子线程。重点。
} catch (InterruptedException e) {
e.printStackTrace();
}
childThread.start();
}
}
线程安全注意事项
如果共享变量是可变的(如 sharedData
),多个线程同时读写它可能会产生竞态条件,导致不一致或不可预料的结果。为了解决这个问题,可以使用以下同步机制:
synchronized
块:确保共享变量的读写操作是原子的。volatile
:用于保证共享变量的可见性,确保所有线程能够看到共享变量的最新值。Atomic
类(如AtomicInteger
):提供了一些线程安全的操作。
private static volatile int sharedData = 0;
2. 通过构造函数传递共享对象
父线程可以通过创建一个共享对象**(如自定义的共享类或集合)并将其传递给子线程。这种方式避免了全局变量的使用**,并且可以通过控制访问权限来提高代码的可维护性。
示例:通过共享对象传递数据
public class SharedObjectExample {
public static void main(String[] args) {
// 共享对象
SharedData sharedData = new SharedData();
// 父线程设置共享对象的值
Thread parentThread = new Thread(() -> {
sharedData.setValue(100);
System.out.println("Parent Thread sets value to: " + sharedData.getValue());
});
// 子线程读取共享对象的值
Thread childThread = new Thread(() -> {
System.out.println("Child Thread reads value: " + sharedData.getValue());
});
parentThread.start();
try {
parentThread.join(); // 确保父线程执行完后再启动子线程
} catch (InterruptedException e) {
e.printStackTrace();
}
childThread.start();
}
// 共享类
static class SharedData {
private int value;
public synchronized void setValue(int value) {
this.value = value;
}
public synchronized int getValue() {
return value;
}
}
}
3. 使用 ThreadLocal
ThreadLocal
提供了一种为每个线程存储局部变量的方式。虽然 ThreadLocal
主要用于提供线程隔离的数据存储,但在某些情况下,也可以通过自定义的传递机制让父线程和子线程共享数据。
使用 InheritableThreadLocal
InheritableThreadLocal
是 ThreadLocal
的一个子类,它允许子线程继承父线程的 ThreadLocal
值。当父线程在 ThreadLocal
中设置了某个值时,子线程可以直接从 InheritableThreadLocal
中获取这个值。
示例:使用 InheritableThreadLocal
public class InheritableThreadLocalExample {
// 使用 InheritableThreadLocal 来允许子线程继承父线程的值
private static InheritableThreadLocal<Integer> threadLocalValue = new InheritableThreadLocal<>();
public static void main(String[] args) {
// 父线程设置 ThreadLocal 值
Thread parentThread = new Thread(() -> {
threadLocalValue.set(100);
System.out.println("Parent Thread sets value: " + threadLocalValue.get());
// 在父线程内启动子线程。重点。注意是启动不是只创建。
Thread childThread = new Thread(() -> {
System.out.println("Child Thread reads value: " + threadLocalValue.get());
});
childThread.start();
});
parentThread.start();
}
}
在这个例子中,父线程通过 InheritableThreadLocal
设置的值被子线程自动继承,子线程可以直接读取父线程设置的值。
4. 使用阻塞队列
在生产者-消费者模型中,父线程可以作为生产者,子线程作为消费者。父线程将数据放入共享的阻塞队列中,子线程从队列中获取数据进行处理。这种方式特别适用于需要异步处理数据的场景。
示例:使用 BlockingQueue
共享数据
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
public class BlockingQueueExample {
public static void main(String[] args) {
// 创建阻塞队列用于数据共享
BlockingQueue<Integer> sharedQueue = new LinkedBlockingQueue<>();
// 父线程(生产者)向队列中放数据
Thread parentThread = new Thread(() -> {
try {
sharedQueue.put(100);
System.out.println("Parent Thread put value: 100");
} catch (InterruptedException e) {
e.printStackTrace();
}
});
// 子线程(消费者)从队列中取数据
Thread childThread = new Thread(() -> {
try {
int value = sharedQueue.take();
System.out.println("Child Thread took value: " + value);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
parentThread.start();
childThread.start();
}
}
在这个例子中,父线程将数据放入阻塞队列,子线程从队列中获取数据,队列负责协调数据的传递并确保线程安全。
5. 使用 Future
或 Callable
Future
和 Callable
是 Java 中用于线程任务结果的机制。父线程可以提交一个 Callable
任务给线程池或者自己创建的子线程,子线程执行任务后返回结果给父线程。
示例:使用 Callable
和 Future
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
public class FutureExample {
public static void main(String[] args) {
// 创建一个 Callable 子线程任务
Callable<Integer> callableTask = () -> {
System.out.println("Child Thread is computing...");
return 100;
};
// 使用 FutureTask 来获取子线程的结果
FutureTask<Integer> futureTask = new FutureTask<>(callableTask);
// 启动子线程
Thread childThread = new Thread(futureTask);
childThread.start();
// 父线程等待子线程的计算结果
try {
Integer result = futureTask.get(); // 阻塞等待子线程结果
System.out.println("Parent Thread gets result: " + result);
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
}
}
在这个例子中,父线程通过 FutureTask
提交子线程任务,并阻塞等待子线程的执行结果。
总结
父子线程之间共享数据的方式有很多种,选择合适的方法取决于具体的使用场景和需求:
- 共享变量:简单直接,但需要处理线程安全问题。
- 通过共享对象传递:将共享对象传递给子线程,适用于需要在多个线程之间共享数据的情况。
ThreadLocal
/InheritableThreadLocal
:适用于需要为每个线程提供独立副本或传递父线程上下文的场景。- 阻塞队列:适用于生产者-消费者模型,可以在父子线程之间传递任务和数据。
Future
和Callable
:适用于父线程等待子线程执行结果的场景。
选择合适的方式,既能有效地共享数据,又能保证线程安全。