一、先搞懂最基础的:线程是什么?
1. 生活类比
想象你要给3个朋友送礼物:
自己送:你先送朋友A,再送朋友B,最后送朋友C——这叫单线程(只有你一个人干活)。
找3个朋友帮忙送:你、朋友甲、朋友乙同时送——这叫多线程(多个人同时干活,更快)。
2. 技术概念
线程:程序中“同时干活的小工人”,每个线程独立执行自己的任务。
主线程:程序启动时自动创建的“主工人”(比如你自己)。
子线程:你手动创建的“帮忙工人”(比如朋友甲)。
3. 简单代码样例
public class ThreadBasicDemo {
public static void main(String[] args) {
// 主线程:自己送朋友A
System.out.println("主线程:送礼物给朋友A");
// 创建子线程:让朋友甲送朋友B
Thread friendA = new Thread(() -> {
System.out.println("子线程(朋友甲):送礼物给朋友B");
});
friendA.start(); // 启动子线程
// 主线程继续送朋友C
System.out.println("主线程:送礼物给朋友C");
}
}二、第一个工具:ThreadLocal——“每个工人的随身小本子”
1. 为什么需要它?
假设每个送礼物的工人需要记录“当前要送的朋友名字”:
如果用公共的黑板写名字,大家都能改,会乱(比如你刚写“朋友A”,朋友甲就改成“朋友B”)。
所以需要每个工人自己的随身小本子——只有自己能写能看,别人碰不到。
2. 技术概念
ThreadLocal:就是“每个线程自己的小本子”,存储的数据只有当前线程能访问,其他线程看不到。
存储结构:每个线程内部有个
ThreadLocalMap(类似小本子的内页),ThreadLocal本身是“小本子的封面”,数据存在内页里。
3. 简单代码样例
public class ThreadLocalDemo {
// 定义一个“随身小本子”,用来记录当前要送的朋友名字
private static final ThreadLocal<String> FRIEND_NAME_HOLDER = new ThreadLocal<>();
public static void main(String[] args) {
// 主线程:在自己的小本子上写“朋友A”
FRIEND_NAME_HOLDER.set("朋友A");
System.out.println("主线程的小本子:" + FRIEND_NAME_HOLDER.get());
// 子线程:朋友甲
new Thread(() -> {
// 子线程的小本子是空的,看不到主线程的内容
System.out.println("子线程(朋友甲)的小本子:" + FRIEND_NAME_HOLDER.get());
// 子线程在自己的小本子上写“朋友B”
FRIEND_NAME_HOLDER.set("朋友B");
System.out.println("子线程(朋友甲)的小本子:" + FRIEND_NAME_HOLDER.get());
}).start();
}
}运行结果:
主线程的小本子:朋友A
子线程(朋友甲)的小本子:null
子线程(朋友甲)的小本子:朋友B三、ThreadLocal的第一个痛点:父子线程传递问题
1. 生活场景
你让朋友甲帮忙送礼物,你自己的小本子上写了“朋友B”,但朋友甲的小本子是空的——他不知道要送谁!
2. 技术痛点
普通ThreadLocal中,父线程(你)设置的值,子线程(朋友甲)无法直接获取。
四、第一个解决方案:InheritableThreadLocal——“创建工人时抄一份小本子”
1. 生活类比
你找朋友甲帮忙时,把自己小本子上的内容抄一份给他——这样他一开始就知道要送谁。
2. 技术概念
InheritableThreadLocal:继承自ThreadLocal,当父线程创建子线程时,自动把父线程的InheritableThreadLocal数据复制到子线程。
3. 简单代码样例
public class InheritableThreadLocalDemo {
// 用“可继承的小本子”
private static final InheritableThreadLocal<String> FRIEND_NAME_HOLDER = new InheritableThreadLocal<>();
public static void main(String[] args) {
// 父线程:写“朋友B”
FRIEND_NAME_HOLDER.set("朋友B");
System.out.println("父线程的小本子:" + FRIEND_NAME_HOLDER.get());
// 创建子线程:自动复制父线程的内容
new Thread(() -> {
System.out.println("子线程的小本子:" + FRIEND_NAME_HOLDER.get()); // 能看到“朋友B”!
}).start();
}
}运行结果:
父线程的小本子:朋友B
子线程的小本子:朋友B五、InheritableThreadLocal的两个致命痛点
痛点1:只抄一次,后续修改不更新
生活场景
你刚把“朋友B”抄给朋友甲,突然想改成“朋友C”——但朋友甲的小本子还是“朋友B”,不会自动更新!
代码样例
public class InheritableThreadLocalLimit1 {
private static final InheritableThreadLocal<String> FRIEND_NAME_HOLDER = new InheritableThreadLocal<>();
public static void main(String[] args) throws InterruptedException {
FRIEND_NAME_HOLDER.set("朋友B");
Thread friendA = new Thread(() -> {
System.out.println("子线程初始:" + FRIEND_NAME_HOLDER.get());
try {
Thread.sleep(1000); // 等父线程修改
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("子线程后续:" + FRIEND_NAME_HOLDER.get()); // 还是“朋友B”!
});
friendA.start();
Thread.sleep(500);
FRIEND_NAME_HOLDER.set("朋友C"); // 父线程修改
System.out.println("父线程修改后:" + FRIEND_NAME_HOLDER.get());
}
}痛点2:完全无法解决“快递站(线程池)”的问题
这是最核心的痛点,我们先引入“线程池”的概念,再讲为什么不行。
六、重要扩展:线程池——“专业的快递站”
1. 为什么需要线程池?
生活场景
如果你每次送礼物都临时找新的朋友帮忙:
找朋友需要时间(创建线程耗时);
朋友送完就走,下次还要重新找(销毁线程浪费资源)。
所以你需要专业的快递站:
快递站有固定数量的兼职快递员(核心线程);
快递多的时候,再找几个临时快递员(非核心线程);
快递员送完一单,不离开快递站,等待下一个任务(线程复用)。
技术概念
线程池:管理线程的“池子”,避免频繁创建和销毁线程,提高效率,节省资源。
2. 线程池的核心参数(快递站的配置)
用“快递站”类比,理解5个核心参数:
3. 简单代码样例:创建一个线程池
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
public class ThreadPoolDemo {
public static void main(String[] args) {
// 创建一个快递站(线程池)
ThreadPoolExecutor executor = new ThreadPoolExecutor(
2, // 核心线程数:2个固定快递员
5, // 最大线程数:最多5个快递员
60, // 空闲时间:临时快递员没事干60秒就走
TimeUnit.SECONDS, // 时间单位:秒
new LinkedBlockingQueue<>(10) // 工作队列:最多放10个待送快递
);
// 提交任务:让快递员送快递
for (int i = 1; i <= 5; i++) {
final int taskId = i;
executor.execute(() -> {
System.out.println("快递员" + Thread.currentThread().getName() + ":送第" + taskId + "个快递");
});
}
executor.shutdown(); // 关闭快递站
}
}七、回到痛点:InheritableThreadLocal在快递站(线程池)里完全没用
1. 生活场景
快递站的兼职快递员是早就招好的(线程池线程提前创建),你现在让他送快递,没法再把自己的小本子抄给他——因为他已经在快递站了!
而且,快递员送完上一单,小本子没擦,下一单就会看到上一单的信息(脏数据)!
2. 代码样例:InheritableThreadLocal在快递站里失败
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class InheritableThreadLocalPoolFail {
private static final InheritableThreadLocal<String> FRIEND_NAME_HOLDER = new InheritableThreadLocal<>();
public static void main(String[] args) throws InterruptedException {
// 创建一个只有1个快递员的快递站
ExecutorService executor = Executors.newFixedThreadPool(1);
// 任务1:你让快递员送朋友B
FRIEND_NAME_HOLDER.set("朋友B");
executor.execute(() -> System.out.println("任务1:送" + FRIEND_NAME_HOLDER.get()));
Thread.sleep(1000);
// 任务2:你让快递员送朋友C,但快递员的小本子没更新!
FRIEND_NAME_HOLDER.set("朋友C");
executor.execute(() -> System.out.println("任务2:送" + FRIEND_NAME_HOLDER.get())); // 可能还是朋友B,甚至null!
executor.shutdown();
}
}八、终极解决方案:TransmittableThreadLocal(TTL)——“快递站的信息中转站”
1. 生活类比
快递站设一个信息中转站:
你提交任务时:把自己小本子上的最新信息放到中转站;
快递员出发前:从中转站拿最新的信息,写到自己的小本子上;
快递员送完后:把自己的小本子擦干净,恢复到之前的状态——避免下一单看到脏数据。
2. 技术概念
TransmittableThreadLocal(TTL):阿里开源的工具,专门解决线程池场景下的ThreadLocal上下文传递和脏数据问题。
核心原理:用
TtlExecutors包装线程池,在提交任务时自动复制父线程的TTL数据,任务执行完后自动恢复线程池线程的状态。
3. 简单代码样例:TTL的使用
步骤1:引入依赖
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>transmittable-thread-local</artifactId>
<version>2.14.2</version>
</dependency>步骤2:完整代码
import com.alibaba.ttl.TransmittableThreadLocal;
import com.alibaba.ttl.threadpool.TtlExecutors;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class TtlDemo {
// 用TTL的“小本子”
private static final TransmittableThreadLocal<String> FRIEND_NAME_HOLDER = new TransmittableThreadLocal<>();
public static void main(String[] args) throws InterruptedException {
// 1. 创建普通快递站
ExecutorService originalExecutor = Executors.newFixedThreadPool(1);
// 2. 用TTL包装快递站(关键!)
ExecutorService ttlExecutor = TtlExecutors.getTtlExecutorService(originalExecutor);
try {
// 任务1:送朋友B
FRIEND_NAME_HOLDER.set("朋友B");
ttlExecutor.execute(() -> System.out.println("任务1:送" + FRIEND_NAME_HOLDER.get()));
Thread.sleep(1000);
// 任务2:送朋友C(能拿到最新信息!)
FRIEND_NAME_HOLDER.set("朋友C");
ttlExecutor.execute(() -> System.out.println("任务2:送" + FRIEND_NAME_HOLDER.get()));
} finally {
// 3. 父线程手动清理自己的小本子(避免内存泄漏)
FRIEND_NAME_HOLDER.remove();
ttlExecutor.shutdown();
}
}
}运行结果:
任务1:送朋友B
任务2:送朋友C九、最佳实践:对比非最佳实践
1. 非最佳实践(反面教材)
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class BadPractice {
private static final ThreadLocal<String> USER_ID_HOLDER = new ThreadLocal<>();
public static void main(String[] args) throws InterruptedException {
ExecutorService executor = Executors.newFixedThreadPool(1);
// 任务1:设置用户ID,不清理
USER_ID_HOLDER.set("1001");
executor.execute(() -> System.out.println("任务1:用户ID=" + USER_ID_HOLDER.get()));
Thread.sleep(1000);
// 任务2:不设置,直接拿——脏数据!
executor.execute(() -> System.out.println("任务2:用户ID=" + USER_ID_HOLDER.get())); // 拿到1001!
executor.shutdown();
}
}问题:
线程池场景下脏数据;
父子线程无法传递;
父线程不清理,可能内存泄漏。
2. 最佳实践(正面教材)
import com.alibaba.ttl.TransmittableThreadLocal;
import com.alibaba.ttl.threadpool.TtlExecutors;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class GoodPractice {
// 1. 用TTL替代普通ThreadLocal
private static final TransmittableThreadLocal<String> USER_ID_HOLDER = new TransmittableThreadLocal<>();
public static void main(String[] args) throws InterruptedException {
// 2. 创建普通线程池,并用TTL包装
ExecutorService originalExecutor = Executors.newFixedThreadPool(1);
ExecutorService ttlExecutor = TtlExecutors.getTtlExecutorService(originalExecutor);
try {
// 任务1:设置用户ID
USER_ID_HOLDER.set("1001");
ttlExecutor.execute(() -> System.out.println("任务1:用户ID=" + USER_ID_HOLDER.get()));
Thread.sleep(1000);
// 任务2:设置新的用户ID
USER_ID_HOLDER.set("2002");
ttlExecutor.execute(() -> System.out.println("任务2:用户ID=" + USER_ID_HOLDER.get()));
} finally {
// 3. 父线程手动清理TTL
USER_ID_HOLDER.remove();
ttlExecutor.shutdown();
}
}
}优点:
线程池场景下正确传递上下文;
自动避免脏数据;
父线程清理,避免内存泄漏。
十、总结:进化之路
如果需要更深入的TTL底层原理,或者线程池的拒绝策略详解,可以告诉我!