Java线程间的通信
无锁的程序
public class NoneLock {
static class ThreadA implements Runnable {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println("Thread A " + i);
}
}
}
static class ThreadB implements Runnable {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println("Thread B " + i);
}
}
}
public static void main(String[] args) {
new Thread(new ThreadA()).start();
new Thread(new ThreadB()).start();
}
}
线程A和线程B各自独立工作,输出自己的打印值。
现在有一个需求,我想等A先执行完之后,再由B去执行,怎么办呢?最简单的方式就是使用一个“对象锁”
锁与同步
在Java中,锁(Lock)是用来保护资源的。当一个线程拿到锁后,其他线程就必须等待,直到这个线程释放锁。我们可以用婚姻来比喻:一个锁一次只能被一个线程“持有”或“结婚”,其他线程必须等待,直到这个锁被“释放”或“离婚”。
为了确保多个线程能按顺序执行,我们可以使用锁来实现线程同步。比如,如果我们不使用锁,两个线程可能会在控制台同时输出,导致结果是混乱的。但如果我们希望线程A先完成,线程B再开始,我们就可以用锁来确保顺序。
示例代码:
public class ObjectLock {
private static Object lock = new Object();
static class ThreadA implements Runnable {
@Override
public void run() {
synchronized (lock) {
for (int i = 0; i < 100; i++) {
System.out.println("Thread A " + i);
}
}
}
}
static class ThreadB implements Runnable {
@Override
public void run() {
synchronized (lock) {
for (int i = 0; i < 100; i++) {
System.out.println("Thread B " + i);
}
}
}
}
public static void main(String[] args) throws InterruptedException {
new Thread(new ThreadA()).start();
// 在主线程里使用sleep方法睡眠了10毫秒,是为了防止线程B先得到锁。因为如果同时start,线程A和线程B都是出于就绪状态,操作系统可能会先让B运行。这样就会先输出B的内容,然后B执行完成之后自动释放锁,线程A再执行
Thread.sleep(10);
new Thread(new ThreadB()).start();
}
}
在这个代码中,声明了一个名字为lock
的对象锁。我们在ThreadA
和ThreadB
内需要同步的代码块里,都是用synchronized
关键字加上了同一个对象锁lock
. 线程A和线程B都使用同一个对象锁lock
。当一个线程持有锁时,另一个线程必须等待锁被释放。这样,我们就确保了线程A和线程B不会同时执行。
等待/通知机制
锁虽然能确保线程同步,但可能会浪费资源,因为线程需要不停尝试获取锁。Java提供了另一种方式:等待/通知机制。
Java多线程的等待/通知机制是基于Object
类的wait()
方法和notify()
, notifyAll()
方法来实现的。
notify()方法会随机叫醒一个正在等待的线程,而notifyAll()会叫醒所有正在等待的线程。
一个锁同一时刻只能被一个线程持有。而假如线程A现在持有了一个锁lock
并开始执行,它可以使用lock.wait()
让自己进入等待状态。这个时候,lock
这个锁是被释放了的。
这时,线程B获得了lock
这个锁并开始执行,它可以在某一时刻,使用lock.notify()
,通知之前持有lock
锁并进入等待状态的线程A,说“线程A你不用等了,可以往下执行了”。
需要注意的是,这个时候线程B并没有释放锁
lock
,除非线程B这个时候使用lock.wait()
释放锁,或者线程B执行结束自行释放锁,线程A才能得到lock
锁。
代码示例:
public class WaitAndNotify {
private static Object lock = new Object();
static class ThreadA implements Runnable {
@Override
public void run() {
synchronized (lock) {
for (int i = 0; i < 5; i++) {
try {
System.out.println("ThreadA: " + i);
lock.notify();
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
lock.notify();
}
}
}
static class ThreadB implements Runnable {
@Override
public void run() {
synchronized (lock) {
for (int i = 0; i < 5; i++) {
try {
System.out.println("ThreadB: " + i);
lock.notify();
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
lock.notify();
}
}
}
public static void main(String[] args) throws InterruptedException {
new Thread(new ThreadA()).start();
Thread.sleep(1000);
new Thread(new ThreadB()).start();
}
}
在这个例子中,线程A和线程B交替执行。每次一个线程执行完后都会使用notify()
唤醒另一个线程,然后自己使用wait()
方法陷入等待并释放lock
锁
信号量
JDK提供了一个类似于“信号量”功能的类Semaphore
。这里我们介绍一种基于volatile
关键字的自己实现的信号量通信。
信号量是一种用于控制多个线程访问公共资源的机制。我们可以使用volatile
关键字来实现简单的信号量。volatile
保证变量的更新对其他线程可见。
volatile关键字能够保证内存的可见性,如果用volatile关键字声明了一个变量,在一个线程里面改变了这个变量的值,那其它线程是立马可见更改后的值的。
代码示例:
public class Signal {
private static volatile int signal = 0;
static class ThreadA implements Runnable {
@Override
public void run() {
while (signal < 5) {
if (signal % 2 == 0) {
System.out.println("threadA: " + signal);
signal++;
}
}
}
}
static class ThreadB implements Runnable {
@Override
public void run() {
while (signal < 5) {
if (signal % 2 == 1) {
System.out.println("threadB: " + signal);
signal++;
}
}
}
}
public static void main(String[] args) throws InterruptedException {
new Thread(new ThreadA()).start();
Thread.sleep(1000);
new Thread(new ThreadB()).start();
}
}
signal
变量控制线程A和线程B的交替执行。线程A在signal
为偶数时执行,线程B在signal
为奇数时执行。
我们使用了一个volatile
变量signal
来实现了“信号量”的模型。这里需要注意的是,volatile
变量需要进行原子操作。
需要注意的是,signal++
并不是一个原子操作,所以在实际开发中,会根据需要使用synchronized
给它“上锁”,或者是使用AtomicInteger
等原子类。并且上面的程序也并不是线程安全的,因为执行while
语句后,可能当前线程就暂停等待时间片了,等线程醒来,可能signal已经大于等于5了
管道
管道是一种线程间通信的方式,主要用于I/O流。
JDK提供了PipedWriter
、 PipedReader
、 PipedOutputStream
、 PipedInputStream
。其中,前面两个是基于字符的,后面两个是基于字节流的。
Java提供了基于字符和字节的管道流,我们可以使用这些管道在线程之间传递数据。
示例代码:
package com.artisan.thread;
import java.io.IOException;
import java.io.PipedReader;
import java.io.PipedWriter;
/**
* 定义Pipe类,用于演示PipedInputStream和PipedOutputStream如何进行数据传输
*/
public class Pipe {
/**
* 定义ReaderThread类,负责从PipedReader中读取数据
*/
static class ReaderThread implements Runnable {
private PipedReader reader;
/**
* 构造函数,初始化PipedReader
*
* @param reader PipedReader对象
*/
public ReaderThread(PipedReader reader) {
this.reader = reader;
}
/**
* 实现Runnable接口的run方法,在新线程中执行读取操作
*/
@Override
public void run() {
System.out.println("this is reader");
int receive = 0;
try {
// 循环读取数据,直到没有数据可读
while ((receive = reader.read()) != -1) {
System.out.print((char) receive);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
/**
* 定义WriterThread类,负责向PipedWriter中写入数据
*/
static class WriterThread implements Runnable {
private PipedWriter writer;
/**
* 构造函数,初始化PipedWriter
*
* @param writer PipedWriter对象
*/
public WriterThread(PipedWriter writer) {
this.writer = writer;
}
/**
* 实现Runnable接口的run方法,在新线程中执行写入操作
*/
@Override
public void run() {
System.out.println("this is writer");
int receive = 0;
try {
// 向PipedWriter写入测试数据
writer.write("test");
} catch (IOException e) {
e.printStackTrace();
} finally {
// 关闭PipedWriter
try {
writer.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
/**
* 主函数,用于启动ReaderThread和WriterThread的示例
*/
public static void main(String[] args) throws IOException, InterruptedException {
PipedWriter writer = new PipedWriter();
PipedReader reader = new PipedReader();
// 确保PipedWriter和PipedReader连接,以便数据可以传输
writer.connect(reader);
// 启动ReaderThread
new Thread(new ReaderThread(reader)).start();
// 等待一段时间后启动WriterThread,以确保线程启动顺序
Thread.sleep(1000);
// 启动WriterThread
new Thread(new WriterThread(writer)).start();
}
}
输出
this is reader
this is writer
test
- 线程ReaderThread开始执行,
- 线程ReaderThread使用管道reader.read()进入”阻塞“,
- 线程WriterThread开始执行,
- 线程WriterThread用writer.write(“test”)往管道写入字符串,
- 线程WriterThread使用writer.close()结束管道写入,并执行完毕,
- 线程ReaderThread接受到管道输出的字符串并打印,
- 线程ReaderThread执行完毕。
ReaderThread从管道中读取数据,而WriterThread则向管道中写入数据。管道用于在两个线程之间传递信息。
其它通信相关
除了上面介绍的通信方式,还有一些其他的方法可以用来实现线程间的通信。
join方法
join()
方法可以让一个线程等待另一个线程执行完成。例如,如果主线程希望等子线程执行完后再继续执行,就可以使用join()
方法。
示例代码:
package com.artisan.thread;
/**
* Join类用于演示Thread类的join方法的使用
*/
public class Join {
/**
* ThreadA是一个实现了Runnable接口的线程类
* 它在run方法中模拟了一个长时间运行的任务
*/
static class ThreadA implements Runnable {
/**
* run方法定义了线程执行时的行为
* 它模拟了一个子线程,在运行时先睡眠1秒,然后醒来
*/
@Override
public void run() {
try {
System.out.println("我是子线程,我先睡一秒");
Thread.sleep(1000); // 子线程睡眠1秒,模拟长时间运行的任务
System.out.println("我是子线程,我睡完了一秒");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
/**
* main方法是程序的入口
* 它创建并启动了一个ThreadA线程,并使用join方法确保主线程等待子线程执行完毕后再继续执行
* @param args 命令行参数
* @throws InterruptedException 如果主线程等待被中断
*/
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(new ThreadA()); // 创建子线程
thread.start(); // 启动子线程
thread.join(); // 确保主线程等待子线程执行完毕后再继续
System.out.println("如果不加join方法,我会先被打出来,加了就不一样了");
}
}
输出
我是子线程,我先睡一秒
我是子线程,我睡完了一秒
如果不加join方法,我会先被打出来,加了就不一样了
join 方法概述
join(long millis)
: 使当前线程等待调用此方法的线程终止,最长等待时间由millis
参数指定,单位为毫秒。join(long millis, int nanos)
: 除了接受一个毫秒数外,还接受一个额外的纳秒数(范围在0到999999之间),用于更细粒度的时间控制。但需注意,这个纳秒参数并不能保证绝对的精度。
底层实现细节
不论是 join(long)
还是 join(long, int)
,它们实际上都依赖于对象的监视器(monitor)来实现等待逻辑,具体是通过调用 Object
类的 wait(long timeout)
方法或其变体。这意味着调用 join
的线程会释放它当前持有的任何锁,并进入等待状态,直到目标线程执行完毕或者超时。
关于 join(long, int)
方法中的纳秒参数处理:JDK 实现中确实没有直接按纳秒精度去精确控制等待时间,这部分更多是一个兼容性设计,以保持 API 的一致性和向后兼容性。实际上,这个纳秒值会被合并到毫秒数中进行计算,然后传递给底层的等待方法,这导致了纳秒级别的精度无法直接体现。
小结
join
方法提供了让一个线程等待另一个线程完成的能力,主要通过两个重载形式实现不同时间精度的等待控制。尽管存在纳秒级别的参数输入,实际精度受限于 Java 虚拟机对线程调度和监控器操作的实现,通常并不保证达到纳秒级的精确控制。这些方法的底层机制涉及线程间的同步与等待,是通过监视器对象和 wait/notify
机制实现的。
sleep方法
sleep()
方法让当前线程暂停一段时间。它是Thread
类的静态方法,常用于模拟延迟或暂停执行。
需要注意的是,sleep()
方法不会释放锁,而wait()
方法会。
1. Thread.sleep
方法详解
Thread.sleep
是 Thread
类的一个静态方法,用于让当前线程暂停执行一段指定的时间。它有两个重载版本:
Thread.sleep(long millis)
:使当前线程暂停执行指定的毫秒数 (millis
)。Thread.sleep(long millis, int nanos)
:使当前线程暂停执行指定的毫秒数 (millis
) 加上指定的纳秒数 (nanos
)。
注意: sleep
方法并不精确到纳秒。JDK 1.8 的实现中,Thread.sleep(long millis, int nanos)
通过对纳秒数 (nanos
) 的简单处理,仍然调用的是 Thread.sleep(long millis)
,即实际暂停时间是一个接近于(但不一定精确)的值。
2. sleep
和 wait
的主要区别
1. 锁的释放:
-
Thread.sleep
方法不会释放当前线程持有的锁。这意味着如果线程在一个同步块或同步方法中调用了sleep
,其他线程无法获得这个锁,直到该线程重新获得CPU资源并退出同步块或同步方法。 -
Object.wait
方法会释放当前线程持有的锁。这意味着调用wait
的线程会放弃锁,允许其他线程进入同步块或同步方法。线程会进入等待状态,直到被其他线程通过notify
或notifyAll
唤醒,并重新获得锁。
2. CPU资源的释放:
-
两者都会释放 CPU 资源,意味着这段时间内不会执行任何代码。
-
wait
方法会将线程放入对象的等待池中,并且必须配合notify
或notifyAll
来唤醒。 -
sleep
只是单纯地让线程休眠一段时间,时间到后自动进入可运行状态(Runnable
)。
3. 使用位置和使用场景:
-
Thread.sleep
可以在任意位置调用,并不要求在同步块或同步方法中。它是一个线程控制的方法,常用于在多线程编程中制造延迟。 -
Object.wait
必须在同步块或同步方法中调用。这是因为wait
需要当前线程持有对象的锁,它通常用于线程间的通信。
3. 实际代码示例
Thread.sleep
示例:
public class SleepExample {
public static void main(String[] args) {
Thread thread = new Thread(() -> {
System.out.println("Thread is going to sleep");
try {
Thread.sleep(2000); // 睡眠2秒
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Thread has woken up");
});
thread.start();
}
}
Object.wait
示例:
public class WaitExample {
private static final Object lock = new Object();
public static void main(String[] args) {
Thread thread1 = new Thread(() -> {
synchronized (lock) {
System.out.println("Thread 1 is waiting");
try {
lock.wait(); // 线程1等待,释放锁
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Thread 1 is resumed");
}
});
Thread thread2 = new Thread(() -> {
synchronized (lock) {
System.out.println("Thread 2 is notifying");
lock.notify(); // 唤醒等待的线程1
}
});
thread1.start();
try {
Thread.sleep(1000); // 确保thread1先启动并等待
} catch (InterruptedException e) {
e.printStackTrace();
}
thread2.start();
}
}
在这个例子中,thread1
在 lock
对象上调用了 wait()
,使其进入等待状态并释放锁。随后,thread2
在相同的 lock
对象上调用了 notify()
,唤醒了 thread1
。
ThreadLocal类
ThreadLocal
为每个线程提供独立的变量副本,确保各线程互不干扰。它常用于保存线程独立的数据,如数据库连接或Session。
package com.artisan.thread;
public class ThreadLocalDemo {
// 实现Runnable接口的内部类ThreadA,用于演示ThreadLocal的使用
static class ThreadA implements Runnable {
private ThreadLocal<String> threadLocal;
// 构造函数,初始化ThreadLocal实例
public ThreadA(ThreadLocal<String> threadLocal) {
this.threadLocal = threadLocal;
}
@Override
public void run() {
// 在当前线程中,为ThreadLocal变量设置一个值
threadLocal.set("A");
try {
// 让线程睡眠1秒,模拟一段时间内的操作
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 打印当前线程中ThreadLocal变量的值
System.out.println("ThreadA输出:" + threadLocal.get());
}
}
// 实现Runnable接口的内部类ThreadB,与ThreadA类似,但设置不同的值
static class ThreadB implements Runnable {
private ThreadLocal<String> threadLocal;
// 构造函数,初始化ThreadLocal实例
public ThreadB(ThreadLocal<String> threadLocal) {
this.threadLocal = threadLocal;
}
@Override
public void run() {
// 在当前线程中,为ThreadLocal变量设置一个值
threadLocal.set("B");
try {
// 让线程睡眠1秒,模拟一段时间内的操作
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 打印当前线程中ThreadLocal变量的值
System.out.println("ThreadB输出:" + threadLocal.get());
}
}
// 主函数,创建ThreadLocal实例,并启动两个使用该实例的线程
public static void main(String[] args) {
// 创建ThreadLocal实例,用于在不同线程中传递数据
ThreadLocal<String> threadLocal = new ThreadLocal<>();
// 创建并启动ThreadA实例所在的线程
new Thread(new ThreadA(threadLocal)).start();
// 创建并启动ThreadB实例所在的线程
new Thread(new ThreadB(threadLocal)).start();
}
}
输出
ThreadB输出:B
ThreadA输出:A
ThreadLocal
的基本概念
ThreadLocal
是 Java 中提供的一种机制,用于在多线程环境下实现线程局部变量。简单来说,ThreadLocal
为每个线程提供了一个独立的变量副本,这意味着每个线程都可以独立地访问和修改自己的副本,而不会影响其他线程的副本。
- 线程局部变量:每个线程都有自己的
ThreadLocal
变量副本,其他线程无法访问或修改这个副本。 - 隔离性:
ThreadLocal
通过提供线程专属的变量副本,确保了多线程环境中的数据隔离,避免了线程间的数据冲突。
ThreadLocal
的主要方法
set(T value)
: 设置当前线程的ThreadLocal
变量的值。get()
: 获取当前线程的ThreadLocal
变量的值。如果当前线程没有设置过值,且ThreadLocal
变量有初始值,会返回初始值。remove()
: 删除当前线程的ThreadLocal
变量值,释放资源。
ThreadLocal
的使用场景
ThreadLocal
常用于在多线程编程中需要保持线程独立状态的场景。典型的使用场景包括:
-
用户会话管理:在 Web 应用中,每个请求都可能在不同的线程中处理,可以使用
ThreadLocal
来存储每个线程独立的用户会话信息。 -
数据库连接:在多线程环境下,通过
ThreadLocal
可以为每个线程创建一个独立的数据库连接,避免线程间的资源冲突。 -
事务管理:当处理事务时,
ThreadLocal
可以用来存储每个线程的事务上下文信息,确保线程独立处理事务。
示例代码
以下是一个简单的 ThreadLocal
使用示例:
public class ThreadLocalDemo {
private static ThreadLocal<Integer> threadLocal = ThreadLocal.withInitial(() -> 0);
public static void main(String[] args) {
Runnable task = () -> {
int value = threadLocal.get();
value += 1;
threadLocal.set(value);
System.out.println(Thread.currentThread().getName() + ": " + threadLocal.get());
};
Thread thread1 = new Thread(task, "Thread 1");
Thread thread2 = new Thread(task, "Thread 2");
thread1.start();
thread2.start();
}
}
输出示例:
Thread 1: 1
Thread 2: 1
在这个示例中,threadLocal
为每个线程维护了一个独立的整数副本。即使它们使用了同一个 ThreadLocal
实例,两个线程仍然拥有独立的值。
为什么使用 ThreadLocal
在某些情况下,使用 ThreadLocal
是更为优雅和方便的解决方案。例如:
-
避免参数传递的复杂性:当多个方法或类需要共享某个变量时,通过
ThreadLocal
可以隐式地传递该变量,而不需要在每个方法中显式地传递参数。 -
确保线程安全:在并发环境中,
ThreadLocal
可以确保每个线程操作的变量是独立的,从而避免了线程安全问题。
注意事项
- 内存泄漏:如果没有在适当的时机调用
remove()
方法,ThreadLocal
可能会导致内存泄漏,特别是在使用线程池的情况下,因为线程池中的线程是重用的。
小结
ThreadLocal
是一种为每个线程提供独立变量副本的机制,用于在多线程环境中实现线程间数据隔离。它简化了线程独立状态的管理,常用于会话管理、数据库连接等场景,但使用时需注意内存管理和合理性。
InheritableThreadLocal
基本概念
InheritableThreadLocal
是 Java 中的一个特殊版本的 ThreadLocal
,用于在子线程中继承父线程的 ThreadLocal
变量值。与普通的 ThreadLocal
不同,InheritableThreadLocal
允许一个线程的子线程访问父线程中已经设置的本地变量副本。
-
继承机制:当一个线程创建了子线程时,子线程会自动继承父线程中
InheritableThreadLocal
的值。这使得子线程可以共享父线程的上下文信息,例如用户身份、事务上下文等。 -
与
ThreadLocal
的区别:普通的ThreadLocal
变量在子线程中是不可见的,而InheritableThreadLocal
使得子线程能够访问父线程在InheritableThreadLocal
中设置的值。
InheritableThreadLocal
的使用场景
InheritableThreadLocal
主要用于在父线程和子线程之间传递一些需要共享的上下文信息,例如:
-
用户身份信息:在父线程中保存用户身份信息,子线程可以直接访问而不需要显式传递。
-
事务管理:在父线程中开始一个事务,并在子线程中继续使用相同的事务上下文。
-
日志跟踪:在多线程环境中,传递和记录与父线程相关的上下文信息,如日志中的追踪ID。
示例代码
package com.artisan.thread;
/**
* InheritableThreadLocalDemo 类演示了如何使用 InheritableThreadLocal
* 在父线程和子线程之间传递值
*/
public class InheritableThreadLocalDemo {
// 创建一个 InheritableThreadLocal 对象,用于在不同线程之间传递字符串值
private static InheritableThreadLocal<String> inheritableThreadLocal = new InheritableThreadLocal<>();
/**
* 程序的入口点
* 在主线程中设置 InheritableThreadLocal 的值,并启动一个子线程来展示值的继承
*
* @param args 命令行参数
*/
public static void main(String[] args) {
// 在主线程中设置 InheritableThreadLocal 的值
inheritableThreadLocal.set("Parent Thread Value");
// 创建并启动一个子线程,子线程会继承主线程的 InheritableThreadLocal 值
Thread childThread = new Thread(() -> {
// 子线程中打印 InheritableThreadLocal 的值
System.out.println("Child Thread InheritableThreadLocal Value: " + inheritableThreadLocal.get());
});
childThread.start();
// 主线程中打印 InheritableThreadLocal 的值
System.out.println("Main Thread InheritableThreadLocal Value: " + inheritableThreadLocal.get());
}
}
输出示例:
Main Thread InheritableThreadLocal Value: Parent Thread Value
Child Thread InheritableThreadLocal Value: Parent Thread Value
在这个示例中,父线程设置了 InheritableThreadLocal
的值 "Parent Thread Value"
。当创建并启动子线程时,子线程能够继承并访问这个值。
自定义继承行为
如果希望在子线程中修改继承的值或进行特殊处理,可以通过重写 InheritableThreadLocal
的 childValue(T parentValue)
方法来定制子线程的值。例如:
InheritableThreadLocal<String> inheritableThreadLocal = new InheritableThreadLocal<String>() {
@Override
protected String childValue(String parentValue) {
return parentValue + " - Modified in Child Thread";
}
};
Demo:
public class InheritableThreadLocalDemo {
// 创建一个 InheritableThreadLocal 对象,用于在不同线程之间传递字符串值
// private static InheritableThreadLocal<String> inheritableThreadLocal = new InheritableThreadLocal<>();
private static InheritableThreadLocal<String> inheritableThreadLocal = new InheritableThreadLocal<String>() {
@Override
protected String childValue(String parentValue) {
return parentValue + " - Modified in Child Thread";
}
};
/**
* 程序的入口点
* 在主线程中设置 InheritableThreadLocal 的值,并启动一个子线程来展示值的继承
*
* @param args 命令行参数
*/
public static void main(String[] args) {
// 在主线程中设置 InheritableThreadLocal 的值
inheritableThreadLocal.set("Parent Thread Value");
// 创建并启动一个子线程,子线程会继承主线程的 InheritableThreadLocal 值
Thread childThread = new Thread(() -> {
// 子线程中打印 InheritableThreadLocal 的值
System.out.println("Child Thread InheritableThreadLocal Value: " + inheritableThreadLocal.get());
});
childThread.start();
// 主线程中打印 InheritableThreadLocal 的值
System.out.println("Main Thread InheritableThreadLocal Value: " + inheritableThreadLocal.get());
}
}
输出:
Main Thread InheritableThreadLocal Value: Parent Thread Value
Child Thread InheritableThreadLocal Value: Parent Thread Value - Modified in Child Thread
注意事项
-
继承限制:虽然子线程可以继承父线程的
InheritableThreadLocal
值,但子线程对值的修改不会影响父线程的值。 -
内存泄漏风险:和
ThreadLocal
类似,如果不适当地使用InheritableThreadLocal
,也可能会导致内存泄漏问题,尤其是在线程池中使用时,需谨慎处理remove()
操作。
小结
InheritableThreadLocal
扩展了 ThreadLocal
的功能,使得父线程的本地变量可以被其子线程继承。这对于需要在多个线程之间共享上下文信息的场景非常有用,如用户身份管理、事务管理等。然而,使用时需要注意避免内存泄漏和其他潜在问题。
TransmittableThreadLocal
背景
在 Java 的并发编程中,ThreadLocal
和 InheritableThreadLocal
提供了线程本地变量的功能,但它们在一些复杂场景中存在局限性:
- 线程池复用问题:在使用线程池时,线程被复用,导致子线程执行时可能使用的是之前其他任务的线程局部变量,
InheritableThreadLocal
不能保证每次任务的隔离性。 - 异步任务问题:在线程池或异步框架(如
CompletableFuture
、ForkJoinPool
)中,子线程可能在不同的时间段被执行,父子线程之间的上下文传递变得复杂且不可靠。
为了解决这些问题,阿里巴巴开发了 TransmittableThreadLocal
(简称 TTL
),它能够确保在线程池复用和异步任务场景下,线程上下文信息能够正确传递。
TTL(TransmittableThreadLocal
)是阿里巴巴开源的一个增强版 ThreadLocal
,专门用于解决在使用线程池、异步任务或其他线程复用场景下,InheritableThreadLocal
失效的问题。它在跨线程任务时,能够自动传递线程上下文信息,确保父线程的 ThreadLocal
变量在子线程中也能正确获取。
TransmittableThreadLocal
的特点
- 线程池复用安全:
TTL
可以正确地将父线程的ThreadLocal
变量值传递给子线程,即使子线程是从线程池中复用的。 - 跨线程传递:即使线程在不同时间点执行,
TTL
也能确保父线程的上下文信息被子线程正确继承。 - 自动清理机制:避免了
ThreadLocal
可能导致的内存泄漏问题。
使用方法
import com.alibaba.ttl.TransmittableThreadLocal;
import com.alibaba.ttl.TtlRunnable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class TTLExample {
private static TransmittableThreadLocal<String> ttl = new TransmittableThreadLocal<>();
public static void main(String[] args) {
ttl.set("Parent Value");
ExecutorService executorService = Executors.newFixedThreadPool(2);
// 通过 TtlRunnable 包装 Runnable,以传递 ThreadLocal 变量
Runnable task = TtlRunnable.get(() -> {
System.out.println(Thread.currentThread().getName() + ": " + ttl.get());
});
executorService.submit(task);
executorService.submit(task);
executorService.shutdown();
}
}
输出示例:
pool-1-thread-1: Parent Value
pool-1-thread-2: Parent Value
TransmittableThreadLocal
确保了即使线程池复用线程,每个线程仍然能够获取到父线程设置的 ThreadLocal
变量值。
适用场景
- 分布式系统上下文传递:在微服务或分布式系统中,传递上下文(如请求追踪ID、用户身份信息)是非常常见的需求,
TTL
可以确保这些上下文信息在异步和多线程场景下被正确传递。 - 日志跟踪:可以确保在不同的线程中,日志记录的信息具有一致性,比如同一个请求的所有日志都包含相同的追踪ID。
- 异步任务处理:在使用异步框架或多线程处理任务时,
TTL
能确保子任务能够继承父任务的上下文。
总结
TransmittableThreadLocal
是阿里巴巴为了解决线程池复用、异步任务上下文传递问题而开发的工具,扩展了 ThreadLocal
的功能,使得在复杂并发场景下也能保持上下文信息的一致性。它适用于需要在父子线程间传递信息的场景,如分布式追踪、日志管理等,但使用时应注意可能的性能影响和复杂性。