一、先搞懂最基础的:线程是什么?

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个核心参数:

参数

生活类比

技术含义

核心线程数(corePoolSize)

快递站固定的兼职快递员数量

即使没事干,也不会被销毁的线程数

最大线程数(maximumPoolSize)

快递站最多能找的快递员数量

线程池能容纳的最大线程数

空闲时间(keepAliveTime)

临时快递员没事干多久就走

非核心线程空闲后多久被销毁

工作队列(workQueue)

待送快递的堆放处

存放等待执行的任务的队列

拒绝策略(rejectedExecutionHandler)

快递太多了怎么办

任务太多时的处理方式(比如直接拒绝、让主线程自己执行)

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. 生活类比

快递站设一个信息中转站

  1. 你提交任务时:把自己小本子上的最新信息放到中转站;

  2. 快递员出发前:从中转站拿最新的信息,写到自己的小本子上;

  3. 快递员送完后:把自己的小本子擦干净,恢复到之前的状态——避免下一单看到脏数据。

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();
        }
    }
}

优点

  • 线程池场景下正确传递上下文;

  • 自动避免脏数据;

  • 父线程清理,避免内存泄漏。

十、总结:进化之路

工具

解决的问题

存在的痛点

适用场景

ThreadLocal

单线程内数据隔离

父子线程无法传递;线程池脏数据

单线程内存储上下文

InheritableThreadLocal

父子线程数据传递

只复制一次;线程池场景无效

简单父子线程(无线程池)

TransmittableThreadLocal

线程池场景下的上下文传递和脏数据

需引入依赖;需包装线程池

线程池、异步调用场景

如果需要更深入的TTL底层原理,或者线程池的拒绝策略详解,可以告诉我!

两块二每分钟