Skip to content

ThreadLocal 方法详解

get()set()remove()……ThreadLocal 的方法不多,但每个都有门道。

用错轻则拿不到值,重则内存泄漏。本文逐个拆解。

五个核心方法

方法说明返回值
get()获取当前线程的值T(无则返回初始值或 null)
set(T value)设置当前线程的值void
remove()移除当前线程的值void
initialValue()提供初始值(受保护方法)T
withInitial(Supplier)JDK 8+,创建带初始值的实例ThreadLocal<T>

get() 方法

返回当前线程存储的值。如果从未设置过,返回初始值(由 initialValue() 提供)。

java
public class GetMethodDemo {

    private static final ThreadLocal&lt;String&gt; context = new ThreadLocal&lt;&gt;();

    public static void main(String[] args) {
        // 未设置 → 返回 null(无初始值)
        System.out.println(context.get()); // null

        // 设置后 → 返回设置的值
        context.set("Hello");
        System.out.println(context.get()); // Hello

        // 有初始值时
        ThreadLocal&lt;Integer&gt; counter = ThreadLocal.withInitial(() -&gt; 0);
        System.out.println(counter.get()); // 0
    }
}

get() 的内部流程:

get() 被调用

    ├─► 获取当前线程 t

    ├─► 获取 t.threadLocals(ThreadLocalMap)

    ├─► Map 非空 → 根据当前 ThreadLocal 查找 Entry
    │          └─► 找到 → 返回 value
    │          └─► 找不到 → 调用 setInitialValue()

    └─► setInitialValue()
         ├─► 调用 initialValue() 获取初始值
         ├─► 创建 ThreadLocalMap(如不存在)
         └─► 存入初始值并返回

set(T value) 方法

设置当前线程的值。如果之前有值,会被覆盖。

java
public class SetMethodDemo {

    private static final ThreadLocal&lt;Integer&gt; counter = new ThreadLocal&lt;&gt;();

    public static void main(String[] args) throws InterruptedException {
        // 每个线程独立计数
        Thread t1 = new Thread(() -&gt; {
            counter.set(10);
            System.out.println("Thread-1 第一次: " + counter.get()); // 10
            counter.set(counter.get() + 1);
            System.out.println("Thread-1 第二次: " + counter.get()); // 11
        });

        Thread t2 = new Thread(() -&gt; {
            // t2 从来没 set 过 → 返回初始值 null
            System.out.println("Thread-2: " + counter.get()); // null
        });

        t1.start();
        t2.start();
        t1.join();
        t2.join();
    }
}

remove() 方法

删除当前线程存储的值。删除后,再次调用 get() 会触发 initialValue() 或返回 null。

这是最重要但最容易被遗忘的方法。

java
public class RemoveMethodDemo {

    private static final ThreadLocal&lt;String&gt; data = new ThreadLocal&lt;&gt;();

    public static void main(String[] args) throws InterruptedException {
        data.set("Value");
        System.out.println("设置后: " + data.get()); // Value

        data.remove();
        System.out.println("删除后: " + data.get()); // null

        // 线程池场景:必须清理
        ExecutorService executor = Executors.newFixedThreadPool(2);

        for (int i = 0; i &lt; 3; i++) {
            final int taskId = i;
            executor.submit(() -&gt; {
                try {
                    data.set("Task-" + taskId);
                    System.out.println(Thread.currentThread().getName() +
                        " 设置: " + data.get());

                    Thread.sleep(100);

                    // ⚠️ 不清理的话,同一个线程的下次任务会看到上次残留的值
                    System.out.println(Thread.currentThread().getName() +
                        " 处理后: " + data.get());
                } finally {
                    data.remove(); // ✅ 必须在 finally 中清理
                }
            });
        }

        executor.shutdown();
        executor.awaitTermination(3, TimeUnit.SECONDS);
    }
}

输出示例(注意线程复用问题):

pool-1-thread-1 设置: Task-0
pool-1-thread-2 设置: Task-1
pool-1-thread-1 处理后: Task-0
pool-1-thread-2 处理后: Task-1
pool-1-thread-1 设置: Task-2
pool-1-thread-1 处理后: Task-2

如果去掉 remove(),第三次 pool-1-thread-1 会看到 Task-0 的残留值。

initialValue() 方法

protected 方法,子类重写以提供初始值。每次 get() 时,如果从未 set() 过,就会调用此方法。

java
public class InitialValueDemo {

    // 匿名内部类方式
    private static final ThreadLocal&lt;List&lt;String&gt;&gt; listLocal =
        new ThreadLocal&lt;List&lt;String&gt;&gt;() {
            @Override
            protected List&lt;String&gt; initialValue() {
                return new ArrayList&lt;&gt;();
            }
        };

    // 使用泛型方法(JDK 8+ 推荐)
    private static final ThreadLocal&lt;List&lt;String&gt;&gt; listLocal2 =
        ThreadLocal.withInitial(ArrayList::new);

    public static void main(String[] args) {
        // 直接 get 就有值,无需先 set
        System.out.println(listLocal.get()); // []

        listLocal.get().add("item");
        System.out.println(listLocal.get()); // [item]

        // 新线程同样是初始值
        new Thread(() -&gt;
            System.out.println("新线程: " + listLocal.get())  // []
        ).start();
    }
}

withInitial(Supplier) 方法

JDK 8 引入的工厂方法,更简洁地创建带初始值的 ThreadLocal:

java
public class WithInitialDemo {

    public static void main(String[] args) {
        // String
        ThreadLocal&lt;String&gt; stringLocal =
            ThreadLocal.withInitial(() -&gt; "default");

        // Integer
        ThreadLocal&lt;Integer&gt; intLocal =
            ThreadLocal.withInitial(() -&gt; 0);

        // 对象
        ThreadLocal&lt;SimpleDateFormat&gt; dateFormatLocal =
            ThreadLocal.withInitial(() -&gt;
                new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));

        // 集合
        ThreadLocal&lt;List&lt;String&gt;&gt; listLocal =
            ThreadLocal.withInitial(ArrayList::new);

        // Map
        ThreadLocal&lt;Map&lt;String, Object&gt;&gt; mapLocal =
            ThreadLocal.withInitial(LinkedHashMap::new);

        System.out.println(stringLocal.get());
        System.out.println(intLocal.get());
        System.out.println(dateFormatLocal.get().format(new Date()));
        System.out.println(listLocal.get());
    }
}

综合使用示例

完整的请求上下文管理:

java
public class RequestContext {

    private static final ThreadLocal&lt;UserContext&gt; userContext =
        new ThreadLocal&lt;&gt;();

    private static final ThreadLocal&lt;RequestId&gt; requestId =
        ThreadLocal.withInitial(RequestId::new);

    public static void main(String[] args) throws InterruptedException {
        processRequest("user1", "req-001");
        processRequest("user2", "req-002");
    }

    private static void processRequest(String username, String reqId) {
        try {
            userContext.set(new UserContext(username));
            requestId.get().setId(reqId);

            System.out.println("开始处理请求: " + requestId.get());

            callService();
            callDao();

        } finally {
            // ⚠️ 重要:每次请求结束必须清理
            userContext.remove();
            requestId.remove();
        }
    }

    private static void callService() {
        System.out.println("  Service: " + userContext.get() +
            ", " + requestId.get());
    }

    private static void callDao() {
        System.out.println("  DAO: " + userContext.get() +
            ", " + requestId.get());
    }

    static class UserContext {
        String username;
        UserContext(String username) { this.username = username; }
        @Override
        public String toString() { return "User[" + username + "]"; }
    }

    static class RequestId {
        String id;
        void setId(String id) { this.id = id; }
        @Override
        public String toString() { return "RequestId[" + id + "]"; }
    }
}

方法对比一览

方法调用时机线程安全注意事项
get()读取安全无值时调用 initialValue
set()写入安全覆盖已有值
remove()删除安全最重要,容易遗漏
initialValue()get 时触发安全仅在首次 get 时调用
withInitial()创建时安全JDK 8+ 推荐方式

常见错误

错误一:忘记清理

java
// ❌ 错误
void process() {
    context.set(value);
    doSomething();
    // 线程复用后,下一个任务会看到残留值
}

// ✅ 正确
void process() {
    try {
        context.set(value);
        doSomething();
    } finally {
        context.remove();
    }
}

错误二:以为子线程能看到父线程的值

java
// ❌ 错误认知
context.set("父线程");
new Thread(() -&gt; {
    context.get();  // 返回 null,不是 "父线程"
});

如果需要子线程继承,使用 InheritableThreadLocal

要点回顾

  • get():返回当前线程的值,无值则返回初始值
  • set():设置当前线程的值,会覆盖之前的值
  • remove()最重要,删除当前线程的值,用完必须调用
  • initialValue():提供初始值,仅在首次 get 时调用
  • withInitial():JDK 8+ 的工厂方法,更简洁

最佳实践:始终使用 withInitial() 或重写 initialValue() 提供初始值,减少 null 判断;在 try-finally 中确保 remove() 一定被调用。

相关链接

基于 VitePress 构建