《SpringAI 智能面试平台+RAG 知识库》

ThreadLocal

ThreadLocal

ThreadLocal 有什么用?

ThreadLocal 有什么用?

通常情况下,我们创建的变量可以被任何一个线程访问和修改。这在多线程环境中可能导致数据竞争和线程安全问题。那么,如果想让每个线程都有自己的专属本地变量,该如何实现呢?

JDK 中提供的 ThreadLocal 类正是为了解决这个问题。ThreadLocal 类允许每个线程绑定自己的值,可以将其形象地比喻为一个“存放数据的盒子”。每个线程都有自己独立的盒子,用于存储私有数据,确保不同线程之间的数据互不干扰。

ThreadLocal
ThreadLocal

当你创建一个 ThreadLocal 变量时,每个访问该变量的线程都会拥有一个独立的副本。这也是 ThreadLocal 名称的由来。线程可以通过 get() 方法获取自己线程的本地副本,或通过 set() 方法修改该副本的值,从而避免了线程安全问题。

ThreadLocal
ThreadLocal
get()
set()

举个简单的例子:假设有两个人去宝屋收集宝物。如果他们共用一个袋子,必然会产生争执;但如果每个人都有一个独立的袋子,就不会有这个问题。如果将这两个人比作线程,那么 ThreadLocal 就是用来避免这两个线程竞争同一个资源的方法。

ThreadLocal
public class ThreadLocalExample {
    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() + " Value: " + threadLocal.get());
        };

        Thread thread1 = new Thread(task, "Thread-1");
        Thread thread2 = new Thread(task, "Thread-2");

        thread1.start(); // 输出: Thread-1 Value: 1
        thread2.start(); // 输出: Thread-2 Value: 1
    }
}
public class ThreadLocalExample {
    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() + " Value: " + threadLocal.get());
        };

        Thread thread1 = new Thread(task, "Thread-1");
        Thread thread2 = new Thread(task, "Thread-2");

        thread1.start(); // 输出: Thread-1 Value: 1
        thread2.start(); // 输出: Thread-2 Value: 1
    }
}

⭐️ ThreadLocal 原理了解吗?

⭐️ ThreadLocal 原理了解吗?

从 Thread 类源代码入手。

Thread
public class Thread implements Runnable {
    //......
    //与此线程有关的ThreadLocal值。由ThreadLocal类维护
    ThreadLocal.ThreadLocalMap threadLocals = null;

    //与此线程有关的InheritableThreadLocal值。由InheritableThreadLocal类维护
    ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
    //......
}
public class Thread implements Runnable {
    //......
    //与此线程有关的ThreadLocal值。由ThreadLocal类维护
    ThreadLocal.ThreadLocalMap threadLocals = null;

    //与此线程有关的InheritableThreadLocal值。由InheritableThreadLocal类维护
    ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
    //......
}

从上面 Thread 类 源代码可以看出 Thread 类中有一个 threadLocals 和 一个 inheritableThreadLocals 变量,它们都是 ThreadLocalMap 类型的变量,我们可以把 ThreadLocalMap 理解为 ThreadLocal 类实现的定制化的 HashMap。默认情况下这两个变量都是 null,只有当前线程调用 ThreadLocal 类的 set 或 get 方法时才创建它们,实际上调用这两个方法的时候,我们调用的是 ThreadLocalMap 类对应的 get()、set() 方法。

Thread
Thread
threadLocals
inheritableThreadLocals
ThreadLocalMap
ThreadLocalMap
ThreadLocal
HashMap
ThreadLocal
set
get
ThreadLocalMap
get()
set()

ThreadLocal 类的 set() 方法

ThreadLocal
set()
public void set(T value) {
    //获取当前请求的线程
    Thread t = Thread.currentThread();
    //取出 Thread 类内部的 threadLocals 变量(哈希表结构)
    ThreadLocalMap map = getMap(t);
    if (map != null)
        // 将需要存储的值放入到这个哈希表中
        map.set(this, value);
    else
        createMap(t, value);
}
ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}
public void set(T value) {
    //获取当前请求的线程
    Thread t = Thread.currentThread();
    //取出 Thread 类内部的 threadLocals 变量(哈希表结构)
    ThreadLocalMap map = getMap(t);
    if (map != null)
        // 将需要存储的值放入到这个哈希表中
        map.set(this, value);
    else
        createMap(t, value);
}
ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}

通过上面这些内容,我们足以通过猜测得出结论:最终的变量是放在了当前线程的 ThreadLocalMap 中,并不是存在 ThreadLocal 上,ThreadLocal 可以理解为只是 ThreadLocalMap 的封装,传递了变量值。 ThrealLocal 类中可以通过 Thread.currentThread() 获取到当前线程对象后,直接通过 getMap(Thread t) 可以访问到该线程的 ThreadLocalMap 对象。

ThreadLocalMap
ThreadLocal
ThreadLocal
ThreadLocalMap
ThrealLocal
Thread.currentThread()
getMap(Thread t)
ThreadLocalMap

每个 Thread 中都具备一个 ThreadLocalMap,而 ThreadLocalMap 可以存储以 ThreadLocal 为 key,Object 对象为 value 的键值对。

Thread
ThreadLocalMap
ThreadLocalMap
ThreadLocal
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
    //......
}
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
    //......
}

比如我们在同一个线程中声明了两个 ThreadLocal 对象的话, Thread 内部都是使用仅有的那个 ThreadLocalMap 存放数据的,ThreadLocalMap 的 key 就是 ThreadLocal 对象,value 就是 ThreadLocal 对象调用 set 方法设置的值。

ThreadLocal
Thread
ThreadLocalMap
ThreadLocalMap
ThreadLocal
ThreadLocal
set

ThreadLocal 数据结构如下图所示:

ThreadLocal

ThreadLocal 数据结构

ThreadLocalMap 是 ThreadLocal 的静态内部类。

ThreadLocalMap
ThreadLocal

ThreadLocal内部类

⭐️ ThreadLocal 内存泄露问题是怎么导致的?

⭐️ ThreadLocal 内存泄露问题是怎么导致的?

ThreadLocal 内存泄漏的根本原因在于其内部实现机制。

ThreadLocal

通过上面的内容我们已经知道:每个线程维护一个名为 ThreadLocalMap 的 map。 当你使用 ThreadLocal 存储值时,实际上是将值存储在当前线程的 ThreadLocalMap 中,其中 ThreadLocal 实例本身作为 key,而你要存储的值作为 value。

ThreadLocalMap
ThreadLocal
ThreadLocalMap
ThreadLocal

ThreadLocal 的 set() 方法源码如下:

ThreadLocal
set()
public void set(T value) {
    Thread t = Thread.currentThread(); // 获取当前线程
    ThreadLocalMap map = getMap(t);   // 获取当前线程的 ThreadLocalMap
    if (map != null) {
        map.set(this, value);         // 设置值
    } else {
        createMap(t, value);          // 创建新的 ThreadLocalMap
    }
}
public void set(T value) {
    Thread t = Thread.currentThread(); // 获取当前线程
    ThreadLocalMap map = getMap(t);   // 获取当前线程的 ThreadLocalMap
    if (map != null) {
        map.set(this, value);         // 设置值
    } else {
        createMap(t, value);          // 创建新的 ThreadLocalMap
    }
}

ThreadLocalMap 的 set() 和 createMap() 方法中,并没有直接存储 ThreadLocal 对象本身,而是使用 ThreadLocal 的哈希值计算数组索引,最终存储于类型为 static class Entry extends WeakReference<ThreadLocal<?>> 的数组中。

ThreadLocalMap
set()
createMap()
ThreadLocal
ThreadLocal
static class Entry extends WeakReference<ThreadLocal<?>>
int i = key.threadLocalHashCode & (len-1);
int i = key.threadLocalHashCode & (len-1);

ThreadLocalMap 的 Entry 定义如下:

ThreadLocalMap
Entry
static class Entry extends WeakReference<ThreadLocal<?>> {
    Object value;

    Entry(ThreadLocal<?> k, Object v) {
        super(k);
        value = v;
    }
}
static class Entry extends WeakReference<ThreadLocal<?>> {
    Object value;

    Entry(ThreadLocal<?> k, Object v) {
        super(k);
        value = v;
    }
}

ThreadLocalMap 的 key 和 value 引用机制:

ThreadLocalMap
key
value

key 是弱引用:ThreadLocalMap 中的 key 是 ThreadLocal 的弱引用 (WeakReference<ThreadLocal<?>>)。 这意味着,如果 ThreadLocal 实例不再被任何强引用指向,垃圾回收器会在下次 GC 时回收该实例,导致 ThreadLocalMap 中对应的 key 变为 null。

ThreadLocalMap
ThreadLocal
WeakReference<ThreadLocal<?>>
ThreadLocal
ThreadLocalMap
null

value 是强引用:即使 key 被 GC 回收,value 仍然被 ThreadLocalMap.Entry 强引用存在,无法被 GC 回收。

key
value
ThreadLocalMap.Entry

当 ThreadLocal 实例失去强引用后,其对应的 value 仍然存在于 ThreadLocalMap 中,因为 Entry 对象强引用了它。如果线程持续存活(例如线程池中的线程),ThreadLocalMap 也会一直存在,导致 key 为 null 的 entry 无法被垃圾回收,即会造成内存泄漏。

ThreadLocal
ThreadLocalMap
Entry
ThreadLocalMap
null

也就是说,内存泄漏的发生需要同时满足两个条件:

ThreadLocal 实例不再被强引用;

ThreadLocal

线程持续存活,导致 ThreadLocalMap 长期存在。

ThreadLocalMap

虽然 ThreadLocalMap 在 get(), set() 和 remove() 操作时会尝试清理 key 为 null 的 entry,但这种清理机制是被动的,并不完全可靠。

ThreadLocalMap
get()
set()
remove()

如何避免内存泄漏的发生?

在使用完 ThreadLocal 后,务必调用 remove() 方法。 这是最安全和最推荐的做法。 remove() 方法会从 ThreadLocalMap 中显式地移除对应的 entry,彻底解决内存泄漏的风险。 即使将 ThreadLocal 定义为 static final,也强烈建议在每次使用后调用 remove()。

ThreadLocal
remove()
remove()
ThreadLocalMap
ThreadLocal
static final
remove()

在线程池等线程复用的场景下,使用 try-finally 块可以确保即使发生异常,remove() 方法也一定会被执行。

try-finally
remove()

为什么 Entry 的 key 要设计为弱引用?

为什么 Entry 的 key 要设计为弱引用?

这是一个经典的面试追问。很多同学知道 ThreadLocalMap 的 key 是弱引用,但不清楚为什么要这样设计,以及如果换成强引用会怎样。

ThreadLocalMap

我们先来看完整的引用链路。当一个线程使用 ThreadLocal 时,涉及以下引用关系:

ThreadLocal
强引用(栈/静态变量)──→ ThreadLocal 实例
                              ↑
Thread ──→ ThreadLocalMap ──→ Entry ─── key(WeakReference)──┘
                              │
                              └─── value(强引用)──→ 实际存储的对象
强引用(栈/静态变量)──→ ThreadLocal 实例
                              ↑
Thread ──→ ThreadLocalMap ──→ Entry ─── key(WeakReference)──┘
                              │
                              └─── value(强引用)──→ 实际存储的对象

理解了这条引用链路,我们来对比两种设计方案:

假设 key 使用强引用(实际没有采用):

当业务代码中的 ThreadLocal 引用被置为 null(例如方法执行结束、对象被回收),此时虽然业务代码已经不再需要这个 ThreadLocal,但由于 ThreadLocalMap 的 Entry 对 key 持有强引用,ThreadLocal 实例仍然无法被 GC 回收。只要线程不终止,这个 ThreadLocal 和它对应的 value 都会一直存在于内存中,造成 key 和 value 都无法回收的内存泄漏。

ThreadLocal
null
ThreadLocal
ThreadLocalMap
ThreadLocal
ThreadLocal

key 使用弱引用(实际采用的方案):

当业务代码中的 ThreadLocal 引用被置为 null 后,由于 Entry 的 key 是弱引用,ThreadLocal 实例在下次 GC 时会被回收,key 变为 null。此时虽然 value 仍然存在(强引用),但 ThreadLocalMap 在执行 get()、set()、remove() 等操作时,会主动探测并清理这些 key 为 null 的 "stale entry"(过期条目),从而释放 value 对象。

ThreadLocal
null
ThreadLocal
null
ThreadLocalMap
get()
set()
remove()
null

也就是说,弱引用的设计是一种“兜底”防御机制——即便开发者忘记调用 remove(),JVM 的 GC 配合 ThreadLocalMap 的自清理逻辑,仍然有机会回收泄漏的数据。而如果使用强引用,一旦忘记 remove(),就完全没有任何补救机会了。

remove()
ThreadLocalMap
remove()

需要注意的是,这种自清理机制是被动触发的(只在 get/set/remove 操作时顺便清理),并不能保证所有过期条目都被及时清理。因此,弱引用只是降低了内存泄漏的风险,并没有彻底消除它,手动调用 remove() 仍然是必须的。

需要注意的是,这种自清理机制是被动触发的(只在 get/set/remove 操作时顺便清理),并不能保证所有过期条目都被及时清理。因此,弱引用只是降低了内存泄漏的风险,并没有彻底消除它,手动调用 remove() 仍然是必须的。

get
set
remove
remove()

线程池场景下的特殊风险

线程池场景下的特殊风险

上面提到内存泄漏的条件之一是“线程持续存活”。在使用 new Thread() 创建线程的场景下,线程执行完毕后会被销毁,其持有的 ThreadLocalMap 也会随之被 GC 回收,泄漏的影响相对有限。

new Thread()
ThreadLocalMap

但在线程池场景下,问题会被严重放大。线程池中的核心线程默认不会被销毁,它们会被反复复用来执行不同的任务。这意味着:

内存泄漏持续累积:每个任务如果使用了 ThreadLocal 却没有清理,其 value 就会一直残留在该线程的 ThreadLocalMap 中。随着任务不断提交和执行,泄漏的数据会越积越多,最终可能导致 OOM。

ThreadLocal
ThreadLocalMap

数据污染(脏数据):上一个任务设置的 ThreadLocal 值,如果没有被清理,下一个被分配到同一线程的任务就能读取到这个残留值。这可能导致严重的业务逻辑错误,比如用户 A 的请求读取到了用户 B 的身份信息。

ThreadLocal

美团技术团队的真实事故案例:

美团技术团队在《Java 线程池实现原理及其在美团业务中的实践》一文中就记录了一次因 ThreadLocal 使用不当引发的线上事故:在一个依赖 ThreadLocal 传递用户上下文的 Web 应用中,由于使用了线程池处理请求,且没有在请求结束后清理 ThreadLocal,导致后续请求复用了同一线程时,读取到了前一个请求遗留的用户信息,造成了用户数据串号的严重问题。

《Java 线程池实现原理及其在美团业务中的实践》

ThreadLocal
ThreadLocal
ThreadLocal

阿里巴巴 Java 开发手册的强制规约

阿里巴巴 Java 开发手册的强制规约

正因为线程池 + ThreadLocal 的组合如此容易踩坑,《阿里巴巴 Java 开发手册》在“并发处理”章节中对此做出了强制级别的要求:

ThreadLocal

【强制】 必须回收自定义的 ThreadLocal 变量记录的当前线程的值,尤其在线程池场景下,线程经常会被复用,如果不清理自定义的 ThreadLocal 变量,可能会影响后续业务逻辑和造成内存泄露等问题。尽量在代理中使用 try-finally 块进行回收。

【强制】 必须回收自定义的 ThreadLocal 变量记录的当前线程的值,尤其在线程池场景下,线程经常会被复用,如果不清理自定义的 ThreadLocal 变量,可能会影响后续业务逻辑和造成内存泄露等问题。尽量在代理中使用 try-finally 块进行回收。

ThreadLocal
ThreadLocal
try-finally

正确的使用模式如下:

// 定义为 static final,避免重复创建 ThreadLocal 实例
private static final ThreadLocal<UserContext> userContextHolder = new ThreadLocal<>();

public void processRequest(HttpServletRequest request) {
    try {
        // 在 try 块中设置值
        UserContext context = buildUserContext(request);
        userContextHolder.set(context);

        // 执行业务逻辑
        doBusinessLogic();
    } finally {
        // 在 finally 块中必须清理,确保无论是否发生异常都会执行
        userContextHolder.remove();
    }
}
// 定义为 static final,避免重复创建 ThreadLocal 实例
private static final ThreadLocal<UserContext> userContextHolder = new ThreadLocal<>();

public void processRequest(HttpServletRequest request) {
    try {
        // 在 try 块中设置值
        UserContext context = buildUserContext(request);
        userContextHolder.set(context);

        // 执行业务逻辑
        doBusinessLogic();
    } finally {
        // 在 finally 块中必须清理,确保无论是否发生异常都会执行
        userContextHolder.remove();
    }
}

这里有三个关键要点:

ThreadLocal 声明为 static final:确保整个应用只有一个 ThreadLocal 实例,避免因重复创建导致旧实例失去强引用后 key 被回收,加剧内存泄漏。

ThreadLocal
static final
ThreadLocal

try-finally 保证 remove() 一定被执行:即使业务逻辑抛出异常,finally 块也能确保 ThreadLocal 被清理。

try-finally
remove()
finally
ThreadLocal

在使用完毕后立即清理,而不是在下次使用前设置:在使用前 set() 虽然可以覆盖旧值解决脏数据问题,但无法解决上一次任务遗留 value 的内存占用问题。只有在用完后 remove(),才能同时避免内存泄漏和数据污染。

set()
remove()

⭐️ 如何跨线程传递 ThreadLocal 的值?

⭐️ 如何跨线程传递 ThreadLocal 的值?

为什么 ThreadLocal 在异步场景下会失效?

ThreadLocal 的值不在 ThreadLocal 对象中,而是存储在 Thread 里:

ThreadLocal
ThreadLocal
Thread
Thread → ThreadLocalMap → Entry(ThreadLocal, value)
Thread → ThreadLocalMap → Entry(ThreadLocal, value)

ThreadLocal 数据结构如下图所示:

ThreadLocal

ThreadLocal 数据结构

异步执行往往意味着任务会从当前线程切换到另一个线程(例如线程池中的工作线程)执行。由于不同线程各自维护独立的 ThreadLocalMap,默认情况下 ThreadLocal 的上下文无法在异步执行中自动传递。

ThreadLocalMap
ThreadLocal

如何跨线程传递 ThreadLocal 的值?

为了解决这个问题,业界有两套主流的解决方案,一套是 JDK 原生的,另一套是阿里巴巴开源的。

InheritableThreadLocal:JDK1.2 提供的一个类,继承自 ThreadLocal。使用 InheritableThreadLocal 时,会在创建子线程时,令子线程继承父线程中的 ThreadLocal 值,但是无法支持线程池场景下的 ThreadLocal 值传递。

InheritableThreadLocal
ThreadLocal
InheritableThreadLocal
ThreadLocal
ThreadLocal

TransmittableThreadLocal: TransmittableThreadLocal(简称 TTL) 是阿里巴巴开源的工具类,继承并加强了 InheritableThreadLocal 类,可以在线程池的场景下支持 ThreadLocal 值传递。项目地址:https://github.com/alibaba/transmittable-thread-local。

TransmittableThreadLocal
TransmittableThreadLocal
InheritableThreadLocal
ThreadLocal

https://github.com/alibaba/transmittable-thread-local

InheritableThreadLocal 原理

InheritableThreadLocal 原理

InheritableThreadLocal 实现了创建异步线程时,继承父线程 ThreadLocal 值的功能。该类是 JDK 团队提供的,通过改造 JDK 源码包中的 Thread 类来实现创建线程时,ThreadLocal 值的传递。

InheritableThreadLocal
ThreadLocal
Thread
ThreadLocal

InheritableThreadLocal 的值存储在哪里?

InheritableThreadLocal

在 Thread 类中添加了一个新的 ThreadLocalMap,命名为 inheritableThreadLocals,该变量用于存储需要跨线程传递的 ThreadLocal 值。如下:

Thread
ThreadLocalMap
inheritableThreadLocals
ThreadLocal
class Thread implements Runnable {
    ThreadLocal.ThreadLocalMap threadLocals = null;
    ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
}
class Thread implements Runnable {
    ThreadLocal.ThreadLocalMap threadLocals = null;
    ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
}

如何完成 ThreadLocal 值的传递?

ThreadLocal

通过改造 Thread 类的构造方法来实现,在创建 Thread 线程时,拿到父线程的 inheritableThreadLocals 变量赋值给子线程即可。相关代码如下:

Thread
Thread
inheritableThreadLocals
// Thread 的构造方法会调用 init() 方法
private void init(/* ... */) {
	// 1、获取父线程
    Thread parent = currentThread();
    // 2、将父线程的 inheritableThreadLocals 赋值给子线程
    if (inheritThreadLocals && parent.inheritableThreadLocals != null)
        this.inheritableThreadLocals =
        	ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
}
// Thread 的构造方法会调用 init() 方法
private void init(/* ... */) {
	// 1、获取父线程
    Thread parent = currentThread();
    // 2、将父线程的 inheritableThreadLocals 赋值给子线程
    if (inheritThreadLocals && parent.inheritableThreadLocals != null)
        this.inheritableThreadLocals =
        	ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
}

InheritableThreadLocal 的方案有什么问题?

InheritableThreadLocal

这个方案的缺陷在于它的一次性,也就是它只在线程创建时发生一次复制。然而,现在的开发中,我们会大量使用线程池,但线程池里的线程是被复用的。

想象一下,任务 A 在线程 1 中执行,把它的 ThreadLocal 值传给了线程池里的子线程 2。任务 A 结束后,线程 1 去休息了。接着,任务 B 来了,它在线程 3 中执行,线程池又复用了刚才那个子线程 2 来执行任务 B 的一部分。此时,子线程 2 的 ThreadLocal 里还残留着任务 A 传给它的脏数据,而任务 B(在线程 3 里)的上下文却完全没有传递过来。这就导致了数据污染和上下文丢失。

ThreadLocal
ThreadLocal

TransmittableThreadLocal 原理

TransmittableThreadLocal 原理

JDK 默认没有支持线程池场景下 ThreadLocal 值传递的功能,因此阿里巴巴开源了一套工具 TransmittableThreadLocal 来实现该功能。

ThreadLocal
TransmittableThreadLocal

由于阿里巴巴无法改动 JDK 源码,TTL 巧妙地利用了装饰器模式对任务(Runnable/Callable)或线程池(Executor)进行增强,将上下文的传递时机从“线程创建时”延迟到了“任务提交与执行时”。

Runnable
Callable
Executor

TTL 的核心逻辑可以概括为三个阶段(CRR):

Capture(捕获):在提交任务(如调用 execute)的一瞬间,TtlRunnable 会调用 TransmittableThreadLocal.Transmitter.capture()。它通过内部维护的 holder 集合,抓取当前父线程中所有活跃的 TTL 变量并存入快照。

execute
TtlRunnable
TransmittableThreadLocal.Transmitter.capture()
holder

Replay(回放):在线程池的工作线程执行 run() 方法前,调用 replay()。它将快照中的值 set 到当前工作线程中,并备份该线程原有的旧值。

run()
replay()
set

Restore(恢复):任务执行结束后,调用 restore()。它根据备份将工作线程恢复到执行前的状态,防止上下文污染或内存泄漏。

restore()

这张图是 TTL 官方提供的 CRR 整个过程的时序图:

TTL 官方提供的 CRR 整个过程的时序图

不太好理解吧?可以看下我绘制的这张 CRR 时序图,更清晰直观一些:

也就是说,TTL 的本质是在任务提交时 Capture 上下文,在任务执行前 Replay 上下文,在任务结束后 Restore 线程状态,从而安全地支持线程池中的 ThreadLocal 传递。

ThreadLocal

TTL 提供了两种主要的接入方式,可根据侵入性要求和改造成本进行选择。

  1. 显式包装(手动接入)

使用 TtlRunnable.get(Runnable) 或 TtlCallable.get(Callable) 对任务进行包装,使用 TtlExecutors.getTtlExecutor(Executor)、getTtlExecutorService(...) 对线程池进行包装。这种接入方式清晰可控,但需要业务代码配合,存在一定侵入性。

TtlRunnable.get(Runnable)
TtlCallable.get(Callable)
TtlExecutors.getTtlExecutor(Executor)
getTtlExecutorService(...)

下面这段代码展示了 TTL 通过 CRR,在支持线程池复用和拒绝策略的前提下,安全地传递并隔离 ThreadLocal 上下文。

ThreadLocal
public class TtlContextHolder {
    private static final Logger log = LoggerFactory.getLogger(TtlContextHolder.class);

    // 1. 使用 static final 确保 TTL 实例不被重复创建,防止内存泄漏
    // 重写 copy 方法(可选):如果是引用类型,建议实现深拷贝
    private static final TransmittableThreadLocal<String> CONTEXT = new TransmittableThreadLocal<String>() {
        @Override
        public String copy(String parentValue) {
            // 默认是直接返回引用,如果是可变对象(如 Map),请在这里 new 新对象
            return parentValue;
        }
    };

    // 2. 线程池初始化:确保只被 TtlExecutors 包装一次
    private static final ExecutorService TTL_EXECUTOR_SERVICE;

    static {
        ExecutorService rawExecutor = new ThreadPoolExecutor(
                2, 4, 60L, TimeUnit.SECONDS,
                new LinkedBlockingQueue<>(1000), (Runnable r) -> new Thread(r, "ttl-worker-" + r.hashCode()),
                new ThreadPoolExecutor.CallerRunsPolicy() // 关键:TTL 完美支持此拒绝策略
        );
        // 包装原始线程池
        TTL_EXECUTOR_SERVICE = TtlExecutors.getTtlExecutorService(rawExecutor);
    }

    public static void main(String[] args) throws Exception {
        try {
            // 3. 在父线程中设置上下文
            CONTEXT.set("value-set-in-parent");
            log.info("父线程上下文: {}", CONTEXT.get());

            // 4. 使用 Lambda 简化任务提交
            TTL_EXECUTOR_SERVICE.submit(() -> {
                log.info("异步任务(Runnable)读取上下文: {}", CONTEXT.get());
                // 模拟业务逻辑
                // 注意:子线程修改是否影响父线程,取决于 copy() 是否做了深拷贝
                CONTEXT.set("value-modified-in-child");
            });

            Future<String> future = TTL_EXECUTOR_SERVICE.submit(() -> {
                log.info("异步任务(Callable)读取上下文: {}", CONTEXT.get());
                return "Success";
            });

            future.get();

            // 5. 验证父线程上下文是否被污染
            log.info("父线程最终上下文: {}", CONTEXT.get());

        } finally {
            // 6. 清理当前线程(父线程)的上下文,子线程的上下文由 TTL 的 Restore 机制自动恢复
            CONTEXT.remove();
        }
    }
}
public class TtlContextHolder {
    private static final Logger log = LoggerFactory.getLogger(TtlContextHolder.class);

    // 1. 使用 static final 确保 TTL 实例不被重复创建,防止内存泄漏
    // 重写 copy 方法(可选):如果是引用类型,建议实现深拷贝
    private static final TransmittableThreadLocal<String> CONTEXT = new TransmittableThreadLocal<String>() {
        @Override
        public String copy(String parentValue) {
            // 默认是直接返回引用,如果是可变对象(如 Map),请在这里 new 新对象
            return parentValue;
        }
    };

    // 2. 线程池初始化:确保只被 TtlExecutors 包装一次
    private static final ExecutorService TTL_EXECUTOR_SERVICE;

    static {
        ExecutorService rawExecutor = new ThreadPoolExecutor(
                2, 4, 60L, TimeUnit.SECONDS,
                new LinkedBlockingQueue<>(1000), (Runnable r) -> new Thread(r, "ttl-worker-" + r.hashCode()),
                new ThreadPoolExecutor.CallerRunsPolicy() // 关键:TTL 完美支持此拒绝策略
        );
        // 包装原始线程池
        TTL_EXECUTOR_SERVICE = TtlExecutors.getTtlExecutorService(rawExecutor);
    }

    public static void main(String[] args) throws Exception {
        try {
            // 3. 在父线程中设置上下文
            CONTEXT.set("value-set-in-parent");
            log.info("父线程上下文: {}", CONTEXT.get());

            // 4. 使用 Lambda 简化任务提交
            TTL_EXECUTOR_SERVICE.submit(() -> {
                log.info("异步任务(Runnable)读取上下文: {}", CONTEXT.get());
                // 模拟业务逻辑
                // 注意:子线程修改是否影响父线程,取决于 copy() 是否做了深拷贝
                CONTEXT.set("value-modified-in-child");
            });

            Future<String> future = TTL_EXECUTOR_SERVICE.submit(() -> {
                log.info("异步任务(Callable)读取上下文: {}", CONTEXT.get());
                return "Success";
            });

            future.get();

            // 5. 验证父线程上下文是否被污染
            log.info("父线程最终上下文: {}", CONTEXT.get());

        } finally {
            // 6. 清理当前线程(父线程)的上下文,子线程的上下文由 TTL 的 Restore 机制自动恢复
            CONTEXT.remove();
        }
    }
}

输出:

09:06:31.438 INFO  [main] TtlContextHolder - 父线程上下文: value-set-in-parent
09:06:31.452 INFO  [ttl-worker-1663166483] TtlContextHolder - 异步任务(Runnable)读取上下文: value-set-in-parent
09:06:31.453 INFO  [ttl-worker-841283083] TtlContextHolder - 异步任务(Callable)读取上下文: value-set-in-parent
09:06:31.453 INFO  [main] TtlContextHolder - 父线程最终上下文: value-set-in-parent
09:06:31.438 INFO  [main] TtlContextHolder - 父线程上下文: value-set-in-parent
09:06:31.452 INFO  [ttl-worker-1663166483] TtlContextHolder - 异步任务(Runnable)读取上下文: value-set-in-parent
09:06:31.453 INFO  [ttl-worker-841283083] TtlContextHolder - 异步任务(Callable)读取上下文: value-set-in-parent
09:06:31.453 INFO  [main] TtlContextHolder - 父线程最终上下文: value-set-in-parent

如果你想要测试这段代码,记得引入 TTL 的 Maven 依赖;

<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>transmittable-thread-local</artifactId>
    <version>2.14.4</version>
</dependency>
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>transmittable-thread-local</artifactId>
    <version>2.14.4</version>
</dependency>
  1. 无侵入接入(Java Agent)

通过 Java Agent 在类加载阶段对线程池相关类进行 字节码增强,自动织入 TTL 的上下文传递逻辑,实现业务代码零改造的上下文透传。这种方式业务代码无需感知 TTL 的存在,但实现复杂度相对较高。

TTL Agent 默认修饰了以下 JDK 执行器组件:

标准线程池:java.util.concurrent.ThreadPoolExecutor 和 java.util.concurrent.ScheduledThreadPoolExecutor。

java.util.concurrent.ThreadPoolExecutor
java.util.concurrent.ScheduledThreadPoolExecutor

ForkJoin 体系:java.util.concurrent.ForkJoinTask(从而透明支持了 CompletableFuture 和 Java 8 并行流 Stream)。

java.util.concurrent.ForkJoinTask
CompletableFuture
Stream

遗留组件:java.util.TimerTask(自 v2.7.0 起支持,v2.11.2 起默认开启)。

java.util.TimerTask

在 Java 启动参数中加入 -javaagent 配置:

-javaagent
# 基础配置
java -javaagent:path/to/transmittable-thread-local-2.x.y.jar \
     -cp classes \
     com.your.app.Main
# 基础配置
java -javaagent:path/to/transmittable-thread-local-2.x.y.jar \
     -cp classes \
     com.your.app.Main

应用场景

应用场景

压测流量标记: 在压测场景中,使用 ThreadLocal 存储压测标记,用于区分压测流量和真实流量。如果标记丢失,可能导致压测流量被错误地当成线上流量处理。

ThreadLocal

上下文传递:在分布式系统中,传递链路追踪信息(如 Trace ID)或用户上下文信息。

总结

总结

ThreadLocal 的值默认是无法跨线程传递的,因为它的值是存在每个 Thread 对象自己的 ThreadLocalMap 里的,父子线程是两个不同的对象。

ThreadLocal
Thread
ThreadLocalMap

为了解决这个问题,主要有两种方案:

JDK 的 InheritableThreadLocal:它会在创建子线程的时候,把父线程的值复制一份给子线程。但它的问题是,在线程池场景下会失效。因为线程池会复用线程,这会导致线程拿到的可能是上一个任务传下来的脏数据。

阿里的 TransmittableThreadLocal (TTL):这是我们项目里用的方案,它专门解决线程池的问题。它的原理是,在提交任务到线程池时,它会把父线程的 ThreadLocal 值捕获下来,和任务绑定在一起。等线程池里的某个线程要执行这个任务时,它再把捕获的值设置到这个线程上,任务执行完再清理掉。

ThreadLocal

简单说,InheritableThreadLocal 是跟线程绑定的,只在创建时有效;而 TTL 是跟任务绑定的,完美支持线程池。

线程池

线程池

什么是线程池?

什么是线程池?

顾名思义,线程池就是管理一系列线程的资源池。当有任务要处理时,直接从线程池中获取线程来处理,处理完之后线程并不会立即被销毁,而是等待下一个任务。

⭐️ 为什么要用线程池?

⭐️ 为什么要用线程池?

池化技术想必大家已经屡见不鲜了,线程池、数据库连接池、HTTP 连接池等等都是对这个思想的应用。池化技术的思想主要是为了减少每次获取资源的消耗,提高对资源的利用率。

线程池提供了一种限制和管理资源(包括执行一个任务)的方式。 每个线程池还维护一些基本统计信息,例如已完成任务的数量。使用线程池主要带来以下几个好处:

降低资源消耗:线程池里的线程是可以重复利用的。一旦线程完成了某个任务,它不会立即销毁,而是回到池子里等待下一个任务。这就避免了频繁创建和销毁线程带来的开销。

提高响应速度:因为线程池里通常会维护一定数量的核心线程(或者说“常驻工人”),任务来了之后,可以直接交给这些已经存在的、空闲的线程去执行,省去了创建线程的时间,任务能够更快地得到处理。

提高线程的可管理性:线程池允许我们统一管理池中的线程。我们可以配置线程池的大小(核心线程数、最大线程数)、任务队列的类型和大小、拒绝策略等。这样就能控制并发线程的总量,防止资源耗尽,保证系统的稳定性。同时,线程池通常也提供了监控接口,方便我们了解线程池的运行状态(比如有多少活跃线程、多少任务在排队等),便于调优。

如何创建线程池?

如何创建线程池?

在 Java 中,创建线程池主要有两种方式:

方式一:通过 ThreadPoolExecutor 构造函数直接创建(推荐)

ThreadPoolExecutor

图中的“默认线程工厂”和“默认拒绝策略”,指的是当前构造函数没有显式传入对应参数时,ThreadPoolExecutor 会使用默认实现,并不是方法和说明错位。

ThreadPoolExecutor

这是最推荐的方式,因为它允许开发者明确指定线程池的核心参数,对线程池的运行行为有更精细的控制,从而避免资源耗尽的风险。

方式二:通过 Executors 工具类创建(不推荐用于生产环境)

Executors

Executors 工具类提供的创建线程池的方法如下图所示:

Executors

可以看出,通过 Executors 工具类可以创建多种类型的线程池,包括:

Executors

FixedThreadPool:固定线程数量的线程池。该线程池中的线程数量始终不变。当有一个新的任务提交时,线程池中若有空闲线程,则立即执行。若没有,则新的任务会被暂存在一个任务队列中,待有线程空闲时,便处理在任务队列中的任务。

FixedThreadPool

SingleThreadExecutor: 只有一个线程的线程池。若多余一个任务被提交到该线程池,任务会被保存在一个任务队列中,待线程空闲,按先入先出的顺序执行队列中的任务。

SingleThreadExecutor

CachedThreadPool: 可根据实际情况调整线程数量的线程池。线程池的线程数量不确定,但若有空闲线程可以复用,则会优先使用可复用的线程。若所有线程均在工作,又有新的任务提交,则会创建新的线程处理任务。所有线程在当前任务执行完毕后,将返回线程池进行复用。

CachedThreadPool

ScheduledThreadPool:给定的延迟后运行任务或者定期执行任务的线程池。

ScheduledThreadPool

⭐️ 为什么不推荐使用内置线程池?

⭐️ 为什么不推荐使用内置线程池?

在《阿里巴巴 Java 开发手册》“并发处理”这一章节,明确指出线程资源必须通过线程池提供,不允许在应用中自行显式创建线程。

为什么呢?

使用线程池的好处是减少在创建和销毁线程上所消耗的时间以及系统资源开销,解决资源不足的问题。如果不使用线程池,有可能会造成系统创建大量同类线程而导致消耗完内存或者“过度切换”的问题。

使用线程池的好处是减少在创建和销毁线程上所消耗的时间以及系统资源开销,解决资源不足的问题。如果不使用线程池,有可能会造成系统创建大量同类线程而导致消耗完内存或者“过度切换”的问题。

另外,《阿里巴巴 Java 开发手册》中强制线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 构造函数的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险

Executors
ThreadPoolExecutor

Executors 返回线程池对象的弊端如下(后文会详细介绍到):

Executors

FixedThreadPool 和 SingleThreadExecutor:使用的是阻塞队列 LinkedBlockingQueue,任务队列最大长度为 Integer.MAX_VALUE,可以看作是无界的,可能堆积大量的请求,从而导致 OOM。

FixedThreadPool
SingleThreadExecutor
LinkedBlockingQueue
Integer.MAX_VALUE

CachedThreadPool:使用的是同步队列 SynchronousQueue, 允许创建的线程数量为 Integer.MAX_VALUE,如果任务数量过多且执行速度较慢,可能会创建大量的线程,从而导致 OOM。

CachedThreadPool
SynchronousQueue
Integer.MAX_VALUE

ScheduledThreadPool 和 SingleThreadScheduledExecutor:使用的无界的延迟阻塞队列 DelayedWorkQueue,任务队列最大长度为 Integer.MAX_VALUE,可能堆积大量的请求,从而导致 OOM。

ScheduledThreadPool
SingleThreadScheduledExecutor
DelayedWorkQueue
Integer.MAX_VALUE
public static ExecutorService newFixedThreadPool(int nThreads) {
    // LinkedBlockingQueue 的默认长度为 Integer.MAX_VALUE,可以看作是无界的
    return new ThreadPoolExecutor(nThreads, nThreads,0L, TimeUnit.MILLISECONDS,new LinkedBlockingQueue<Runnable>());

}

public static ExecutorService newSingleThreadExecutor() {
    // LinkedBlockingQueue 的默认长度为 Integer.MAX_VALUE,可以看作是无界的
    return new FinalizableDelegatedExecutorService (new ThreadPoolExecutor(1, 1,0L, TimeUnit.MILLISECONDS,new LinkedBlockingQueue<Runnable>()));

}

// 同步队列 SynchronousQueue,没有容量,最大线程数是 Integer.MAX_VALUE`
public static ExecutorService newCachedThreadPool() {

    return new ThreadPoolExecutor(0, Integer.MAX_VALUE,60L, TimeUnit.SECONDS,new SynchronousQueue<Runnable>());

}

// DelayedWorkQueue(延迟阻塞队列)
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
    return new ScheduledThreadPoolExecutor(corePoolSize);
}
public ScheduledThreadPoolExecutor(int corePoolSize) {
    super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
          new DelayedWorkQueue());
}
public static ExecutorService newFixedThreadPool(int nThreads) {
    // LinkedBlockingQueue 的默认长度为 Integer.MAX_VALUE,可以看作是无界的
    return new ThreadPoolExecutor(nThreads, nThreads,0L, TimeUnit.MILLISECONDS,new LinkedBlockingQueue<Runnable>());

}

public static ExecutorService newSingleThreadExecutor() {
    // LinkedBlockingQueue 的默认长度为 Integer.MAX_VALUE,可以看作是无界的
    return new FinalizableDelegatedExecutorService (new ThreadPoolExecutor(1, 1,0L, TimeUnit.MILLISECONDS,new LinkedBlockingQueue<Runnable>()));

}

// 同步队列 SynchronousQueue,没有容量,最大线程数是 Integer.MAX_VALUE`
public static ExecutorService newCachedThreadPool() {

    return new ThreadPoolExecutor(0, Integer.MAX_VALUE,60L, TimeUnit.SECONDS,new SynchronousQueue<Runnable>());

}

// DelayedWorkQueue(延迟阻塞队列)
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
    return new ScheduledThreadPoolExecutor(corePoolSize);
}
public ScheduledThreadPoolExecutor(int corePoolSize) {
    super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
          new DelayedWorkQueue());
}

⭐️ 线程池常见参数有哪些?如何解释?

⭐️ 线程池常见参数有哪些?如何解释?

/**
     * 用给定的初始参数创建一个新的ThreadPoolExecutor。
     */
    public ThreadPoolExecutor(int corePoolSize,//线程池的核心线程数量
                              int maximumPoolSize,//线程池的最大线程数
                              long keepAliveTime,//当线程数大于核心线程数时,多余的空闲线程存活的最长时间
                              TimeUnit unit,//时间单位
                              BlockingQueue<Runnable> workQueue,//任务队列,用来储存等待执行任务的队列
                              ThreadFactory threadFactory,//线程工厂,用来创建线程,一般默认即可
                              RejectedExecutionHandler handler//拒绝策略,当提交的任务过多而不能及时处理时,我们可以定制策略来处理任务
                               ) {
        if (corePoolSize < 0 ||
            maximumPoolSize <= 0 ||
            maximumPoolSize < corePoolSize ||
            keepAliveTime < 0)
            throw new IllegalArgumentException();
        if (workQueue == null || threadFactory == null || handler == null)
            throw new NullPointerException();
        this.corePoolSize = corePoolSize;
        this.maximumPoolSize = maximumPoolSize;
        this.workQueue = workQueue;
        this.keepAliveTime = unit.toNanos(keepAliveTime);
        this.threadFactory = threadFactory;
        this.handler = handler;
    }
/**
     * 用给定的初始参数创建一个新的ThreadPoolExecutor。
     */
    public ThreadPoolExecutor(int corePoolSize,//线程池的核心线程数量
                              int maximumPoolSize,//线程池的最大线程数
                              long keepAliveTime,//当线程数大于核心线程数时,多余的空闲线程存活的最长时间
                              TimeUnit unit,//时间单位
                              BlockingQueue<Runnable> workQueue,//任务队列,用来储存等待执行任务的队列
                              ThreadFactory threadFactory,//线程工厂,用来创建线程,一般默认即可
                              RejectedExecutionHandler handler//拒绝策略,当提交的任务过多而不能及时处理时,我们可以定制策略来处理任务
                               ) {
        if (corePoolSize < 0 ||
            maximumPoolSize <= 0 ||
            maximumPoolSize < corePoolSize ||
            keepAliveTime < 0)
            throw new IllegalArgumentException();
        if (workQueue == null || threadFactory == null || handler == null)
            throw new NullPointerException();
        this.corePoolSize = corePoolSize;
        this.maximumPoolSize = maximumPoolSize;
        this.workQueue = workQueue;
        this.keepAliveTime = unit.toNanos(keepAliveTime);
        this.threadFactory = threadFactory;
        this.handler = handler;
    }

ThreadPoolExecutor 3 个最重要的参数:

ThreadPoolExecutor

corePoolSize : 任务队列未达到队列容量时,最大可以同时运行的线程数量。

corePoolSize

maximumPoolSize : 任务队列中存放的任务达到队列容量的时候,当前可以同时运行的线程数量变为最大线程数。

maximumPoolSize

workQueue: 新任务来的时候会先判断当前运行的线程数量是否达到核心线程数,如果达到的话,新任务就会被存放在队列中。

workQueue

ThreadPoolExecutor 其他常见参数 :

ThreadPoolExecutor

keepAliveTime:当线程池中的线程数量大于 corePoolSize,即有非核心线程(线程池中核心线程以外的线程)时,这些非核心线程空闲后不会立即销毁,而是会等待,直到等待的时间超过了 keepAliveTime 才会被回收销毁。

keepAliveTime
corePoolSize
keepAliveTime

unit : keepAliveTime 参数的时间单位。

unit
keepAliveTime

threadFactory :executor 创建新线程的时候会用到。

threadFactory

handler :拒绝策略(后面会单独详细介绍一下)。

handler

下面这张图可以加深你对线程池中各个参数的相互关系的理解(图片来源:《Java 性能调优实战》):

线程池各个参数的关系

线程池的核心线程会被回收吗?

线程池的核心线程会被回收吗?

ThreadPoolExecutor 默认不会回收核心线程,即使它们已经空闲了。这是为了减少创建线程的开销,因为核心线程通常是要长期保持活跃的。但是,如果线程池是被用于周期性使用的场景,且频率不高(周期之间有明显的空闲时间),可以考虑将 allowCoreThreadTimeOut(boolean value) 方法的参数设置为 true,这样就会回收空闲(时间间隔由 keepAliveTime 指定)的核心线程了。

ThreadPoolExecutor
allowCoreThreadTimeOut(boolean value)
true
keepAliveTime
public void allowCoreThreadTimeOut(boolean value) {
    // 核心线程的 keepAliveTime 必须大于 0 才能启用超时机制
    if (value && keepAliveTime <= 0) {
        throw new IllegalArgumentException("Core threads must have nonzero keep alive times");
    }
    // 设置 allowCoreThreadTimeOut 的值
    if (value != allowCoreThreadTimeOut) {
        allowCoreThreadTimeOut = value;
        // 如果启用了超时机制,清理所有空闲的线程,包括核心线程
        if (value) {
            interruptIdleWorkers();
        }
    }
}
public void allowCoreThreadTimeOut(boolean value) {
    // 核心线程的 keepAliveTime 必须大于 0 才能启用超时机制
    if (value && keepAliveTime <= 0) {
        throw new IllegalArgumentException("Core threads must have nonzero keep alive times");
    }
    // 设置 allowCoreThreadTimeOut 的值
    if (value != allowCoreThreadTimeOut) {
        allowCoreThreadTimeOut = value;
        // 如果启用了超时机制,清理所有空闲的线程,包括核心线程
        if (value) {
            interruptIdleWorkers();
        }
    }
}

核心线程空闲时处于什么状态?

核心线程空闲时处于什么状态?

核心线程空闲时,其状态分为以下两种情况:

设置了核心线程的存活时间:核心线程在空闲时,会处于 WAITING 状态,等待获取任务。如果阻塞等待的时间超过了核心线程存活时间,则该线程会退出工作,将该线程从线程池的工作线程集合中移除,线程状态变为 TERMINATED 状态。

WAITING
TERMINATED

没有设置核心线程的存活时间:核心线程在空闲时,会一直处于 WAITING 状态,等待获取任务,核心线程会一直存活在线程池中。

WAITING

当队列中有可用任务时,会唤醒被阻塞的线程,线程的状态会由 WAITING 状态变为 RUNNABLE 状态,之后去执行对应任务。

WAITING
RUNNABLE

接下来通过相关源码,了解一下线程池内部是如何做的。

线程在线程池内部被抽象为了 Worker,当 Worker 被启动之后,会不断去任务队列中获取任务。

Worker
Worker

在获取任务的时候,会根据 timed 值来决定从任务队列(BlockingQueue)获取任务的行为。

timed
BlockingQueue

如果「设置了核心线程的存活时间」或者「线程数量超过了核心线程数量」,则将 timed 标记为 true,表明获取任务时需要使用 poll() 指定超时时间。

timed
true
poll()

timed == true:使用 poll(timeout, unit) 来获取任务。使用 poll(timeout, unit) 方法获取任务超时的话,则当前线程会退出执行(TERMINATED),该线程从线程池中被移除。

timed == true
poll(timeout, unit)
poll(timeout, unit)
TERMINATED

timed == false:使用 take() 来获取任务。使用 take() 方法获取任务会让当前线程一直阻塞等待(WAITING)。

timed == false
take()
take()
WAITING

源码如下:

// ThreadPoolExecutor
private Runnable getTask() {
    boolean timedOut = false;
    for (;;) {
        // ...

        // 1、如果「设置了核心线程的存活时间」或者是「线程数量超过了核心线程数量」,则 timed 为 true。
        boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;
        // 2、扣减线程数量。
        // wc > maximuimPoolSize:线程池中的线程数量超过最大线程数量。其中 wc 为线程池中的线程数量。
        // timed && timeOut:timeOut 表示获取任务超时。
        // 分为两种情况:核心线程设置了存活时间 && 获取任务超时,则扣减线程数量;线程数量超过了核心线程数量 && 获取任务超时,则扣减线程数量。
        if ((wc > maximumPoolSize || (timed && timedOut))
            && (wc > 1 || workQueue.isEmpty())) {
            if (compareAndDecrementWorkerCount(c))
                return null;
            continue;
        }
        try {
            // 3、如果 timed 为 true,则使用 poll() 获取任务;否则,使用 take() 获取任务。
            Runnable r = timed ?
                workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
                workQueue.take();
            // 4、获取任务之后返回。
            if (r != null)
                return r;
            timedOut = true;
        } catch (InterruptedException retry) {
            timedOut = false;
        }
    }
}
// ThreadPoolExecutor
private Runnable getTask() {
    boolean timedOut = false;
    for (;;) {
        // ...

        // 1、如果「设置了核心线程的存活时间」或者是「线程数量超过了核心线程数量」,则 timed 为 true。
        boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;
        // 2、扣减线程数量。
        // wc > maximuimPoolSize:线程池中的线程数量超过最大线程数量。其中 wc 为线程池中的线程数量。
        // timed && timeOut:timeOut 表示获取任务超时。
        // 分为两种情况:核心线程设置了存活时间 && 获取任务超时,则扣减线程数量;线程数量超过了核心线程数量 && 获取任务超时,则扣减线程数量。
        if ((wc > maximumPoolSize || (timed && timedOut))
            && (wc > 1 || workQueue.isEmpty())) {
            if (compareAndDecrementWorkerCount(c))
                return null;
            continue;
        }
        try {
            // 3、如果 timed 为 true,则使用 poll() 获取任务;否则,使用 take() 获取任务。
            Runnable r = timed ?
                workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
                workQueue.take();
            // 4、获取任务之后返回。
            if (r != null)
                return r;
            timedOut = true;
        } catch (InterruptedException retry) {
            timedOut = false;
        }
    }
}

⭐️ 线程池的拒绝策略有哪些?

⭐️ 线程池的拒绝策略有哪些?

如果当前同时运行的线程数量达到最大线程数量并且队列也已经被放满了任务时,ThreadPoolExecutor 定义一些策略:

ThreadPoolExecutor

ThreadPoolExecutor.AbortPolicy:抛出 RejectedExecutionException 来拒绝新任务的处理。

ThreadPoolExecutor.AbortPolicy
RejectedExecutionException

ThreadPoolExecutor.CallerRunsPolicy:调用执行者自己的线程运行任务,也就是直接在调用 execute 方法的线程中运行(run)被拒绝的任务,如果执行程序已关闭,则会丢弃该任务。因此这种策略会降低对于新任务提交速度,影响程序的整体性能。如果你的应用程序可以承受此延迟并且你要求任何一个任务请求都要被执行的话,你可以选择这个策略。

ThreadPoolExecutor.CallerRunsPolicy
execute
run

ThreadPoolExecutor.DiscardPolicy:不处理新任务,直接丢弃掉。

ThreadPoolExecutor.DiscardPolicy

ThreadPoolExecutor.DiscardOldestPolicy:此策略将丢弃最早的未处理的任务请求。

ThreadPoolExecutor.DiscardOldestPolicy

举个例子:Spring 通过 ThreadPoolTaskExecutor 或者我们直接通过 ThreadPoolExecutor 的构造函数创建线程池的时候,当我们不指定 RejectedExecutionHandler 拒绝策略来配置线程池的时候,默认使用的是 AbortPolicy。在这种拒绝策略下,如果队列满了,ThreadPoolExecutor 将抛出 RejectedExecutionException 异常来拒绝新来的任务,这代表你将丢失对这个任务的处理。如果不想丢弃任务的话,可以使用 CallerRunsPolicy。CallerRunsPolicy 和其他的几个策略不同,它既不会抛弃任务,也不会抛出异常,而是将任务回退给调用者,使用调用者的线程来执行任务。

ThreadPoolTaskExecutor
ThreadPoolExecutor
RejectedExecutionHandler
AbortPolicy
ThreadPoolExecutor
RejectedExecutionException
CallerRunsPolicy
CallerRunsPolicy
public static class CallerRunsPolicy implements RejectedExecutionHandler {

        public CallerRunsPolicy() { }

        public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
            if (!e.isShutdown()) {
                // 直接主线程执行,而不是线程池中的线程执行
                r.run();
            }
        }
    }
public static class CallerRunsPolicy implements RejectedExecutionHandler {

        public CallerRunsPolicy() { }

        public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
            if (!e.isShutdown()) {
                // 直接主线程执行,而不是线程池中的线程执行
                r.run();
            }
        }
    }

如果不允许丢弃任务,应该选择哪个拒绝策略?

如果不允许丢弃任务,应该选择哪个拒绝策略?

根据上面对线程池拒绝策略的介绍,相信大家很容易能够得出答案是:CallerRunsPolicy。

CallerRunsPolicy

这里我们再来结合 CallerRunsPolicy 的源码来看看:

CallerRunsPolicy
public static class CallerRunsPolicy implements RejectedExecutionHandler {

        public CallerRunsPolicy() { }

        public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
            //只要当前程序没有关闭,就用执行execute方法的线程执行该任务
            if (!e.isShutdown()) {

                r.run();
            }
        }
    }
public static class CallerRunsPolicy implements RejectedExecutionHandler {

        public CallerRunsPolicy() { }

        public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
            //只要当前程序没有关闭,就用执行execute方法的线程执行该任务
            if (!e.isShutdown()) {

                r.run();
            }
        }
    }

从源码可以看出,只要当前程序不关闭就会使用执行 execute 方法的线程执行该任务。

execute

CallerRunsPolicy 拒绝策略有什么风险?如何解决?

CallerRunsPolicy 拒绝策略有什么风险?如何解决?

我们上面也提到了:如果想要保证任何一个任务请求都要被执行的话,那选择 CallerRunsPolicy 拒绝策略更合适一些。

CallerRunsPolicy

不过,如果走到 CallerRunsPolicy 的任务是个非常耗时的任务,且处理提交任务的线程是主线程,可能会导致主线程阻塞,影响程序的正常运行。

CallerRunsPolicy

这里简单举一个例子,该线程池限定了最大线程数为 2,阻塞队列大小为 1(这意味着第 4 个任务就会走到拒绝策略),ThreadUtil 为 Hutool 提供的工具类:

ThreadUtil
public class ThreadPoolTest {

    private static final Logger log = LoggerFactory.getLogger(ThreadPoolTest.class);

    public static void main(String[] args) {
        // 创建一个线程池,核心线程数为1,最大线程数为2
        // 当线程数大于核心线程数时,多余的空闲线程存活的最长时间为60秒,
        // 任务队列为容量为1的ArrayBlockingQueue,饱和策略为CallerRunsPolicy。
        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(1,
                2,
                60,
                TimeUnit.SECONDS,
                new ArrayBlockingQueue<>(1),
                new ThreadPoolExecutor.CallerRunsPolicy());

        // 提交第一个任务,由核心线程执行
        threadPoolExecutor.execute(() -> {
            log.info("核心线程执行第一个任务");
            ThreadUtil.sleep(1, TimeUnit.MINUTES);
        });

        // 提交第二个任务,由于核心线程被占用,任务将进入队列等待
        threadPoolExecutor.execute(() -> {
            log.info("非核心线程处理入队的第二个任务");
            ThreadUtil.sleep(1, TimeUnit.MINUTES);
        });

        // 提交第三个任务,由于核心线程被占用且队列已满,创建非核心线程处理
        threadPoolExecutor.execute(() -> {
            log.info("非核心线程处理第三个任务");
            ThreadUtil.sleep(1, TimeUnit.MINUTES);
        });

        // 提交第四个任务,由于核心线程和非核心线程都被占用,队列也满了,根据CallerRunsPolicy策略,任务将由提交任务的线程(即主线程)来执行
        threadPoolExecutor.execute(() -> {
            log.info("主线程处理第四个任务");
            ThreadUtil.sleep(2, TimeUnit.MINUTES);
        });

        // 提交第五个任务,主线程被第四个任务卡住,该任务必须等到主线程执行完才能提交
        threadPoolExecutor.execute(() -> {
            log.info("核心线程执行第五个任务");
        });

        // 关闭线程池
        threadPoolExecutor.shutdown();
    }
}
public class ThreadPoolTest {

    private static final Logger log = LoggerFactory.getLogger(ThreadPoolTest.class);

    public static void main(String[] args) {
        // 创建一个线程池,核心线程数为1,最大线程数为2
        // 当线程数大于核心线程数时,多余的空闲线程存活的最长时间为60秒,
        // 任务队列为容量为1的ArrayBlockingQueue,饱和策略为CallerRunsPolicy。
        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(1,
                2,
                60,
                TimeUnit.SECONDS,
                new ArrayBlockingQueue<>(1),
                new ThreadPoolExecutor.CallerRunsPolicy());

        // 提交第一个任务,由核心线程执行
        threadPoolExecutor.execute(() -> {
            log.info("核心线程执行第一个任务");
            ThreadUtil.sleep(1, TimeUnit.MINUTES);
        });

        // 提交第二个任务,由于核心线程被占用,任务将进入队列等待
        threadPoolExecutor.execute(() -> {
            log.info("非核心线程处理入队的第二个任务");
            ThreadUtil.sleep(1, TimeUnit.MINUTES);
        });

        // 提交第三个任务,由于核心线程被占用且队列已满,创建非核心线程处理
        threadPoolExecutor.execute(() -> {
            log.info("非核心线程处理第三个任务");
            ThreadUtil.sleep(1, TimeUnit.MINUTES);
        });

        // 提交第四个任务,由于核心线程和非核心线程都被占用,队列也满了,根据CallerRunsPolicy策略,任务将由提交任务的线程(即主线程)来执行
        threadPoolExecutor.execute(() -> {
            log.info("主线程处理第四个任务");
            ThreadUtil.sleep(2, TimeUnit.MINUTES);
        });

        // 提交第五个任务,主线程被第四个任务卡住,该任务必须等到主线程执行完才能提交
        threadPoolExecutor.execute(() -> {
            log.info("核心线程执行第五个任务");
        });

        // 关闭线程池
        threadPoolExecutor.shutdown();
    }
}

输出:

18:19:48.203 INFO  [pool-1-thread-1] c.j.concurrent.ThreadPoolTest - 核心线程执行第一个任务
18:19:48.203 INFO  [pool-1-thread-2] c.j.concurrent.ThreadPoolTest - 非核心线程处理第三个任务
18:19:48.203 INFO  [main] c.j.concurrent.ThreadPoolTest - 主线程处理第四个任务
18:20:48.212 INFO  [pool-1-thread-2] c.j.concurrent.ThreadPoolTest - 非核心线程处理入队的第二个任务
18:21:48.219 INFO  [pool-1-thread-2] c.j.concurrent.ThreadPoolTest - 核心线程执行第五个任务
18:19:48.203 INFO  [pool-1-thread-1] c.j.concurrent.ThreadPoolTest - 核心线程执行第一个任务
18:19:48.203 INFO  [pool-1-thread-2] c.j.concurrent.ThreadPoolTest - 非核心线程处理第三个任务
18:19:48.203 INFO  [main] c.j.concurrent.ThreadPoolTest - 主线程处理第四个任务
18:20:48.212 INFO  [pool-1-thread-2] c.j.concurrent.ThreadPoolTest - 非核心线程处理入队的第二个任务
18:21:48.219 INFO  [pool-1-thread-2] c.j.concurrent.ThreadPoolTest - 核心线程执行第五个任务

从输出结果可以看出,因为 CallerRunsPolicy 这个拒绝策略,导致耗时的任务用了主线程执行,导致线程池阻塞,进而导致后续任务无法及时执行,严重的情况下很可能导致 OOM。

CallerRunsPolicy

我们从问题的本质入手,调用者采用 CallerRunsPolicy 是希望所有的任务都能够被执行,暂时无法处理的任务又被保存在阻塞队列 BlockingQueue 中。这样的话,在内存允许的情况下,我们可以增加阻塞队列 BlockingQueue 的大小并调整堆内存以容纳更多的任务,确保任务能够被准确执行。

CallerRunsPolicy
BlockingQueue
BlockingQueue

为了充分利用 CPU,我们还可以调整线程池的 maximumPoolSize(最大线程数)参数,这样可以提高任务处理速度,避免累计在 BlockingQueue 的任务过多导致内存用完。

maximumPoolSize
BlockingQueue

调整阻塞队列大小和最大线程数

如果服务器资源已达到可利用的极限,这就意味我们要在设计策略上改变线程池的调度了,我们都知道,导致主线程卡死的本质就是因为我们不希望任何一个任务被丢弃。换个思路,有没有办法既能保证任务不被丢弃且在服务器有余力时及时处理呢?

这里提供的一种任务持久化的思路,这里所谓的任务持久化,包括但不限于:

设计一张任务表将任务存储到 MySQL 数据库中。

Redis 缓存任务。

将任务提交到消息队列中。

这里以方案一为例,简单介绍一下实现逻辑:

实现 RejectedExecutionHandler 接口自定义拒绝策略,自定义拒绝策略负责将线程池暂时无法处理(此时阻塞队列已满)的任务入库(保存到 MySQL 中)。注意:线程池暂时无法处理的任务会先被放在阻塞队列中,阻塞队列满了才会触发拒绝策略。

RejectedExecutionHandler

继承 BlockingQueue 实现一个混合式阻塞队列,该队列包含 JDK 自带的 ArrayBlockingQueue。另外,该混合式阻塞队列需要修改取任务处理的逻辑,也就是重写 take() 方法,取任务时优先从数据库中读取最早的任务,数据库中无任务时再从 ArrayBlockingQueue 中去取任务。

BlockingQueue
ArrayBlockingQueue
take()
ArrayBlockingQueue

将一部分任务保存到MySQL中

整个实现逻辑还是比较简单的,核心在于自定义拒绝策略和阻塞队列。如此一来,一旦我们的线程池中线程达到满载时,我们就可以通过拒绝策略将最新任务持久化到 MySQL 数据库中,等到线程池有了有余力处理所有任务时,让其优先处理数据库中的任务以避免“饥饿”问题。

当然,对于这个问题,我们也可以参考其他主流框架的做法,以 Netty 为例,它的拒绝策略则是直接创建一个线程池以外的线程处理这些任务,为了保证任务的实时处理,这种做法可能需要良好的硬件设备且临时创建的线程无法做到准确的监控:

private static final class NewThreadRunsPolicy implements RejectedExecutionHandler {
    NewThreadRunsPolicy() {
        super();
    }
    public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
        try {
            //创建一个临时线程处理任务
            final Thread t = new Thread(r, "Temporary task executor");
            t.start();
        } catch (Throwable e) {
            throw new RejectedExecutionException(
                    "Failed to start a new thread", e);
        }
    }
}
private static final class NewThreadRunsPolicy implements RejectedExecutionHandler {
    NewThreadRunsPolicy() {
        super();
    }
    public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
        try {
            //创建一个临时线程处理任务
            final Thread t = new Thread(r, "Temporary task executor");
            t.start();
        } catch (Throwable e) {
            throw new RejectedExecutionException(
                    "Failed to start a new thread", e);
        }
    }
}

ActiveMQ 则是尝试在指定的时效内尽可能的争取将任务入队,以保证最大交付:

new RejectedExecutionHandler() {
                @Override
                public void rejectedExecution(final Runnable r, final ThreadPoolExecutor executor) {
                    try {
                        //限时阻塞等待,实现尽可能交付
                        executor.getQueue().offer(r, 60, TimeUnit.SECONDS);
                    } catch (InterruptedException e) {
                        throw new RejectedExecutionException("Interrupted waiting for BrokerService.worker");
                    }
                    throw new RejectedExecutionException("Timed Out while attempting to enqueue Task.");
                }
            });
new RejectedExecutionHandler() {
                @Override
                public void rejectedExecution(final Runnable r, final ThreadPoolExecutor executor) {
                    try {
                        //限时阻塞等待,实现尽可能交付
                        executor.getQueue().offer(r, 60, TimeUnit.SECONDS);
                    } catch (InterruptedException e) {
                        throw new RejectedExecutionException("Interrupted waiting for BrokerService.worker");
                    }
                    throw new RejectedExecutionException("Timed Out while attempting to enqueue Task.");
                }
            });

线程池常用的阻塞队列有哪些?

线程池常用的阻塞队列有哪些?

新任务来的时候会先判断当前运行的线程数量是否达到核心线程数,如果达到的话,新任务就会被存放在队列中。

不同的线程池会选用不同的阻塞队列,我们可以结合内置线程池来分析。

容量为 Integer.MAX_VALUE 的 LinkedBlockingQueue(无界阻塞队列):FixedThreadPool 和 SingleThreadExecutor。FixedThreadPool 最多只能创建核心线程数的线程(核心线程数和最大线程数相等),SingleThreadExecutor 只能创建一个线程(核心线程数和最大线程数都是 1),二者的任务队列永远不会被放满。

Integer.MAX_VALUE
LinkedBlockingQueue
FixedThreadPool