少女祈祷中...
NEKO
知识库

java ThreadLocal 面试要点

一、ThreadLocal

Q1:什么是 ThreadLocal?

答: ThreadLocal 提供线程本地变量,每个线程都有自己独立的副本,互不干扰。

核心思想:以空间换时间,用线程隔离替代同步。

ThreadLocal<String> tl = new ThreadLocal<>();

// 线程 A
new Thread(() -> {
    tl.set("A的值");
    System.out.println(tl.get()); // A的值
}).start();

// 线程 B
new Thread(() -> {
    tl.set("B的值");
    System.out.println(tl.get()); // B的值
}).start();

Q2:ThreadLocal 的底层原理是什么?

答: 每个 Thread 内部维护一个 ThreadLocalMap:

public class Thread {
    ThreadLocal.ThreadLocalMap threadLocals = null;
}

public class ThreadLocal<T> {
    public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = t.threadLocals;
        if (map != null)
            map.set(this, value);  // key 是 ThreadLocal 对象
        else
            t.threadLocals = new ThreadLocalMap(this, value);
    }

    public T get() {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = t.threadLocals;
        if (map != null) {
            Entry e = map.getEntry(this);
            if (e != null) return (T) e.value;
        }
        return setInitialValue();
    }
}

结构:

Thread
  └── ThreadLocalMap
        └── Entry[]
              ├── Entry(key=ThreadLocal1, value=值1)
              └── Entry(key=ThreadLocal2, value=值2)

关键:

  • 数据存在线程里,不是 ThreadLocal 里

  • ThreadLocal 作为 key

  • 每个线程独立,天然线程安全


Q3:ThreadLocalMap 的 key 为什么是弱引用?

答: Entry 继承 WeakReference<ThreadLocal<?>>:

static class Entry extends WeakReference<ThreadLocal<?>> {
    Object value;
    Entry(ThreadLocal<?> k, Object v) {
        super(k);  // key 是弱引用
        value = v; // value 是强引用
    }
}

为什么用弱引用:

  • 若 key 是强引用,ThreadLocal 对象即使外部不再使用,也无法被 GC

  • 因为 Thread → ThreadLocalMap → Entry → key(ThreadLocal) 强引用链

  • 弱引用让 ThreadLocal 在无外部引用时可被回收

但引入新问题:key 被回收后,value 仍被强引用 → 内存泄漏。


Q4:ThreadLocal 为什么会内存泄漏?

答: 关键在于 Entry 的 key 是弱引用,value 是强引用:

Thread → ThreadLocalMap → Entry → value(强引用)
                              ↓
                          key(弱引用,可能被回收)

泄漏过程:

  1. ThreadLocal 外部引用置 null

  2. GC 回收 key(弱引用)

  3. Entry 的 key 变成 null

  4. 但 value 仍被 Entry 强引用

  5. Thread 长期存活(如线程池),value 无法回收

  6. 内存泄漏

ThreadLocal<byte[]> tl = new ThreadLocal<>();
tl.set(new byte[1024 * 1024]); // 1MB
tl = null; // key 可被回收,但 value 还在

Q5:为什么 value 不用弱引用?

答: 因为 value 需要被正常使用:

  • 若 value 也是弱引用,可能在使用中被 GC 回收

  • 导致 get() 返回 null,逻辑错误

  • 所以 value 必须强引用

根本矛盾:

  • key 弱引用:避免 ThreadLocal 对象泄漏

  • value 强引用:保证数据可用

  • 结果:key 回收后 value 泄漏


Q6:如何避免 ThreadLocal 内存泄漏?

答: 核心是用完必须 remove():

ThreadLocal<User> userHolder = new ThreadLocal<>();

try {
    userHolder.set(user);
    doWork();
} finally {
    userHolder.remove(); // 必须清理
}

其他措施:

  1. try-finally 保证 remove

  2. 线程池场景尤其重要(线程复用)

  3. 用 static final 修饰 ThreadLocal(避免频繁创建)

private static final ThreadLocal<User> USER_HOLDER = 
    new ThreadLocal<>();

Q7:ThreadLocalMap 如何避免泄漏?

答: 通过探测式清理和启发式清理:

get() 时的清理:

private Entry getEntry(ThreadLocal<?> key) {
    int i = key.threadLocalHashCode & (table.length - 1);
    Entry e = table[i];
    if (e != null && e.get() == key)
        return e;
    else
        return getEntryAfterMiss(key, i, e); // 清理 key=null 的 Entry
}

set() 时的清理:

  • cleanSomeSlots():启发式扫描,清理部分

  • expungeStaleEntry():探测式清理,遇到 null key 就清除

注意:这些清理不保证彻底,仍需手动 remove。


Q8:ThreadLocal 的 hash 冲突如何解决?

答: 用开放地址法(线性探测):

// 计算索引
int i = key.threadLocalHashCode & (len - 1);

// 冲突则往后找
for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) {
    // ...
}

对比 HashMap:

  • HashMap:数组 + 链表/红黑树

  • ThreadLocalMap:纯数组 + 开放地址法

原因:ThreadLocal 数量通常少,开放地址法更简单高效。


Q9:ThreadLocal 的初始值如何设置?

答: 三种方式:

1. setInitialValue():

ThreadLocal<String> tl = new ThreadLocal<>() {
    @Override
    protected String initialValue() {
        return "默认值";
    }
};

2. withInitial()(推荐):

ThreadLocal<String> tl = ThreadLocal.withInitial(() -> "默认值");

3. 手动 set:

tl.set("值");

Q10:ThreadLocal 的典型应用场景?

答:

  1. 用户上下文:

private static final ThreadLocal<User> USER = new ThreadLocal<>();
// 请求入口 set,出口 remove
  1. 数据库连接:

private static final ThreadLocal<Connection> CONN = new ThreadLocal<>();
  1. 事务管理:

private static final ThreadLocal<Transaction> TX = new ThreadLocal<>();
  1. SimpleDateFormat(非线程安全):

private static final ThreadLocal<SimpleDateFormat> SDF = 
    ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd"));
  1. MDC 日志追踪:traceId 传递


Q11:ThreadLocal 和 synchronized 有什么区别?

答:

对比项

ThreadLocal

synchronized

思想

空间换时间

时间换空间

隔离

线程独立副本

共享变量加锁

阻塞

不阻塞

阻塞

适用

线程独享数据

共享数据

性能

高

有锁开销

// ThreadLocal:各用各的
ThreadLocal<SimpleDateFormat> sdf = ...;

// synchronized:共享一把锁
synchronized (sdf) { sdf.format(date); }

Q12:线程池中使用 ThreadLocal 有什么坑?

答: 线程复用导致数据串扰:

// 线程池
executor.execute(() -> {
    userHolder.set(userA);
    // 任务结束未 remove
});

// 后续任务复用同一线程
executor.execute(() -> {
    User u = userHolder.get(); // 可能拿到 userA!
});

解决:

try {
    userHolder.set(user);
    doWork();
} finally {
    userHolder.remove(); // 必须清理
}

最佳实践:用 AOP 或拦截器统一清理。


Q13:InheritableThreadLocal 是什么?

答: 支持父子线程传递的 ThreadLocal:

InheritableThreadLocal<String> tl = new InheritableThreadLocal<>();
tl.set("父线程的值");

new Thread(() -> {
    System.out.println(tl.get()); // 父线程的值
}).start();

原理:创建子线程时,复制父线程的 inheritableThreadLocals。

注意:

  • 线程池不适用(线程复用,不重新创建)

  • 需用 TransmittableThreadLocal(阿里 TTL)


Q14:TransmittableThreadLocal 是什么?

答: 阿里开源的 TTL,解决线程池中上下文传递:

TransmittableThreadLocal<String> tl = new TransmittableThreadLocal<>();
tl.set("值");

// 包装线程池
ExecutorService pool = TtlExecutors.getTtlExecutorService(
    Executors.newFixedThreadPool(3));

pool.execute(() -> {
    System.out.println(tl.get()); // 正确传递
});

原理:任务提交时捕获上下文,执行前设置,执行后恢复。


二、Fork/Join

Q15:什么是 Fork/Join 框架?

答: Fork/Join 是 JDK 7 引入的并行计算框架,用于分治任务:

  • Fork:把大任务拆成小任务

  • Join:合并小任务结果

ForkJoinPool pool = new ForkJoinPool();
Long result = pool.invoke(new SumTask(1, 1000000));

核心思想:分而治之 + 工作窃取。


Q16:Fork/Join 的核心组件是什么?

答:

组件

作用

ForkJoinPool

线程池

ForkJoinTask

任务抽象类

RecursiveAction

无返回值任务

RecursiveTask

有返回值任务

WorkQueue

每线程双端队列

class SumTask extends RecursiveTask<Long> {
    private final int start, end;

    protected Long compute() {
        if (end - start <= 1000) {
            // 小任务直接计算
            long sum = 0;
            for (int i = start; i <= end; i++) sum += i;
            return sum;
        }
        int mid = (start + end) / 2;
        SumTask left = new SumTask(start, mid);
        SumTask right = new SumTask(mid + 1, end);
        left.fork();          // 异步执行左任务
        long rightResult = right.compute(); // 当前线程算右任务
        long leftResult = left.join();      // 等待左任务
        return leftResult + rightResult;
    }
}

Q17:什么是工作窃取(Work Stealing)?

答: 空闲线程从其他线程队列尾部窃取任务:

线程A: [任务1, 任务2, 任务3, 任务4]  ← 从头部取
线程B: [任务5]                       ← 从A尾部偷任务4

规则:

  • 自己从头部取任务(LIFO)

  • 窃取从尾部拿任务(FIFO)

  • 减少竞争

优点:

  • 充分利用 CPU

  • 减少线程空闲

  • 降低队列竞争


Q18:Fork/Join 和 ThreadPoolExecutor 有什么区别?

答:

对比项

ThreadPoolExecutor

ForkJoinPool

队列

共享队列

每线程独立队列

调度

FIFO

工作窃取

任务

独立任务

可分治任务

适用

通用

递归/并行

代表

Executors

parallelStream

// ThreadPoolExecutor:共享队列
ExecutorService pool = Executors.newFixedThreadPool(4);

// ForkJoinPool:工作窃取
ForkJoinPool fjp = new ForkJoinPool(4);

Q19:Fork/Join 的 fork() 和 join() 有什么区别?

答:

  • fork():把任务提交到当前线程的队列,异步执行

  • join():等待任务完成,返回结果

left.fork();                    // 提交左任务
long rightResult = right.compute(); // 当前线程算右
long leftResult = left.join();  // 等左任务

优化技巧:一个 fork,一个 compute,减少线程阻塞。


Q20:Fork/Join 的并行度如何设置?

答:

// 默认:CPU 核数
ForkJoinPool pool = new ForkJoinPool();

// 手动指定
ForkJoinPool pool = new ForkJoinPool(8);

推荐:默认即可,除非有特殊需求。

注意:

  • 并行度 ≠ 线程数,可能动态调整

  • 阻塞任务会降低并行度


Q21:Fork/Join 的典型应用场景?

答:

  1. 并行流:

list.parallelStream()
    .filter(...)
    .map(...)
    .collect(...);
  1. 归并排序:

class MergeSortTask extends RecursiveAction { ... }
  1. 数组求和:

class SumTask extends RecursiveTask<Long> { ... }
  1. 树遍历:

class TreeTask extends RecursiveTask<Result> { ... }

Q22:Fork/Join 的阈值如何设置?

答: 阈值决定任务拆分粒度:

private static final int THRESHOLD = 1000;

protected Long compute() {
    if (end - start <= THRESHOLD) {
        // 直接计算
    } else {
        // 拆分
    }
}

原则:

  • 太小:拆分开销大

  • 太大:并行度不够

  • 经验值:1000 ~ 10000


Q23:Fork/Join 有什么缺点?

答:

  1. 编程复杂:需要拆分逻辑

  2. 调试困难:多线程栈

  3. 不适合 IO:阻塞会拖累

  4. 任务粒度敏感:阈值难调

  5. 异常处理复杂:需特殊处理

建议:CPU 密集型、可递归拆分任务才用。


三、CompletableFuture

Q24:什么是 CompletableFuture?

答: CompletableFuture 是 JDK 8 引入的异步编程工具:

  • 实现 Future 和 CompletionStage

  • 支持链式调用

  • 支持组合多个异步任务

  • 支持回调,无需阻塞

CompletableFuture<String> future = CompletableFuture
    .supplyAsync(() -> "Hello")
    .thenApply(s -> s + " World")
    .thenApply(String::toUpperCase);

System.out.println(future.get()); // HELLO WORLD

Q25:CompletableFuture 和 Future 有什么区别?

答:

对比项

Future

CompletableFuture

获取结果

get() 阻塞

回调 + get

链式调用

不支持

支持

组合任务

不支持

支持

异常处理

需 try-catch

链式处理

手动完成

不支持

complete()

函数式

不友好

友好

// Future:阻塞
Future<String> f = pool.submit(() -> "result");
String r = f.get(); // 阻塞

// CompletableFuture:回调
CompletableFuture.supplyAsync(() -> "result")
    .thenAccept(System.out::println);

Q26:CompletableFuture 的创建方式有哪些?

答:

// 1. 已完成
CompletableFuture<String> f1 = 
    CompletableFuture.completedFuture("done");

// 2. 无返回值,异步
CompletableFuture<Void> f2 = 
    CompletableFuture.runAsync(() -> doWork());

// 3. 有返回值,异步
CompletableFuture<String> f3 = 
    CompletableFuture.supplyAsync(() -> "result");

// 4. 指定线程池
CompletableFuture<String> f4 = 
    CompletableFuture.supplyAsync(() -> "result", executor);

// 5. 手动完成
CompletableFuture<String> f5 = new CompletableFuture<>();
f5.complete("manual");

默认线程池:ForkJoinPool.commonPool()(CPU 核数 - 1)。


Q27:CompletableFuture 的常用方法有哪些?

答:

方法

作用

thenApply

转换结果

thenAccept

消费结果

thenRun

执行动作

thenCompose

链式异步

thenCombine

合并两个结果

exceptionally

异常处理

handle

结果+异常处理

whenComplete

完成回调

allOf

等所有完成

anyOf

任一完成

CompletableFuture.supplyAsync(() -> "Hello")
    .thenApply(s -> s + " World")     // 转换
    .thenAccept(System.out::println)  // 消费
    .thenRun(() -> System.out.println("Done")); // 动作

Q28:thenApply 和 thenCompose 有什么区别?

答:

  • thenApply:转换结果(同步函数)

  • thenCompose:链式异步(返回 CompletableFuture)

// thenApply:Function<T, U>
CompletableFuture<String> f1 = 
    CompletableFuture.supplyAsync(() -> "Hello")
        .thenApply(s -> s + " World");

// thenCompose:Function<T, CompletableFuture<U>>
CompletableFuture<String> f2 = 
    CompletableFuture.supplyAsync(() -> "Hello")
        .thenCompose(s -> CompletableFuture.supplyAsync(
            () -> s + " World"));

区别:

  • thenApply 返回普通值

  • thenCompose 返回新的 CompletableFuture,避免嵌套


Q29:thenCombine 和 thenCompose 有什么区别?

答:

  • thenCombine:合并两个独立 CompletableFuture

  • thenCompose:依赖前一个结果

// thenCombine:两个独立任务
CompletableFuture<String> f1 = 
    CompletableFuture.supplyAsync(() -> "Hello");
CompletableFuture<String> f2 = 
    CompletableFuture.supplyAsync(() -> "World");

f1.thenCombine(f2, (a, b) -> a + " " + b)
  .thenAccept(System.out::println); // Hello World

// thenCompose:依赖前一个
CompletableFuture.supplyAsync(() -> "user1")
    .thenCompose(user -> queryOrder(user));

Q30:CompletableFuture 如何处理异常?

答: 三种方式:

1. exceptionally:

CompletableFuture.supplyAsync(() -> {
    throw new RuntimeException("error");
}).exceptionally(ex -> {
    System.out.println("异常: " + ex);
    return "默认值";
});

2. handle:

CompletableFuture.supplyAsync(() -> "result")
    .handle((result, ex) -> {
        if (ex != null) return "异常处理";
        return result;
    });

3. whenComplete:

CompletableFuture.supplyAsync(() -> "result")
    .whenComplete((result, ex) -> {
        if (ex != null) log.error("异常", ex);
        else log.info("结果: " + result);
    });

区别:

  • exceptionally:只处理异常,返回默认值

  • handle:结果和异常都处理,有返回值

  • whenComplete:只做回调,不改变结果


Q31:allOf 和 anyOf 有什么区别?

答:

  • allOf:等所有完成

  • anyOf:任一完成

// allOf
CompletableFuture<String> f1 = ...;
CompletableFuture<String> f2 = ...;
CompletableFuture<Void> all = CompletableFuture.allOf(f1, f2);
all.thenRun(() -> {
    // f1、f2 都完成
    String r1 = f1.join();
    String r2 = f2.join();
});

// anyOf
CompletableFuture<Object> any = CompletableFuture.anyOf(f1, f2);
any.thenAccept(r -> System.out.println("最快: " + r));

注意:allOf 返回 CompletableFuture<Void>,需手动 get 结果。


Q32:CompletableFuture 的 get() 和 join() 有什么区别?

答:

对比项

get()

join()

异常

抛 ExecutionException

抛 CompletionException

检查异常

是

否

接口

Future

CompletionStage

// get:需 try-catch
try {
    String r = future.get();
} catch (Exception e) { }

// join:不需检查异常
String r = future.join();

推荐:join() 更简洁。


Q33:CompletableFuture 默认线程池有什么问题?

答: 默认用 ForkJoinPool.commonPool(),问题:

  1. 共享:所有 CompletableFuture 共用

  2. 大小固定:CPU 核数 - 1

  3. 阻塞任务拖累:IO 任务会占满

  4. 无法监控:不好排查

解决:自定义线程池:

ExecutorService executor = new ThreadPoolExecutor(
    10, 20, 60L, TimeUnit.SECONDS,
    new ArrayBlockingQueue<>(100),
    new ThreadFactoryBuilder().setNameFormat("cf-pool-%d").build(),
    new ThreadPoolExecutor.CallerRunsPolicy()
);

CompletableFuture.supplyAsync(() -> doWork(), executor);

Q34:CompletableFuture 的 thenApply 和 thenApplyAsync 有什么区别?

答:

  • thenApply:当前线程执行

  • thenApplyAsync:线程池执行

CompletableFuture.supplyAsync(() -> "Hello")
    .thenApply(s -> {  // 可能由完成 supplyAsync 的线程执行
        return s + " World";
    })
    .thenApplyAsync(s -> {  // 提交到线程池
        return s + "!";
    }, executor);

建议:

  • 简单转换用 thenApply

  • 耗时操作用 thenApplyAsync + 自定义线程池


Q35:如何用 CompletableFuture 实现多个任务并行?

答:

// 三个独立任务并行
CompletableFuture<String> f1 = 
    CompletableFuture.supplyAsync(() -> queryUser(), executor);
CompletableFuture<String> f2 = 
    CompletableFuture.supplyAsync(() -> queryOrder(), executor);
CompletableFuture<String> f3 = 
    CompletableFuture.supplyAsync(() -> queryCoupon(), executor);

// 等所有完成
CompletableFuture<Void> all = 
    CompletableFuture.allOf(f1, f2, f3);

// 合并结果
CompletableFuture<Result> result = all.thenApply(v -> {
    return new Result(f1.join(), f2.join(), f3.join());
});

result.thenAccept(r -> System.out.println(r));

优势:三个查询并行,总耗时 = max(三个),而非 sum。


Q36:CompletableFuture 能替代线程池吗?

答: 不能,它们是互补关系:

  • 线程池:管理线程,执行任务

  • CompletableFuture:编排异步任务,链式组合

// 线程池:执行
ExecutorService pool = Executors.newFixedThreadPool(10);

// CompletableFuture:编排
CompletableFuture.supplyAsync(() -> task1(), pool)
    .thenCompose(r -> CompletableFuture.supplyAsync(
        () -> task2(r), pool))
    .thenAccept(System.out::println);

最佳实践:CompletableFuture + 自定义线程池。


Q37:CompletableFuture 的常见坑有哪些?

答:

  1. 默认线程池阻塞:IO 任务用 commonPool

  2. 异常被吞:未处理时静默失败

  3. get() 阻塞:失去异步意义

  4. 嵌套过深:可读性差

  5. 线程池未关闭:资源泄漏

// 坑:异常被吞
CompletableFuture.supplyAsync(() -> {
    throw new RuntimeException("error");
}); // 无异常处理,静默失败

// 正确
.exceptionally(ex -> { log.error("", ex); return null; });

Q38:CompletableFuture 的 thenAcceptBoth 和 thenCombine 有什么区别?

答:

  • thenAcceptBoth:消费两个结果,无返回值

  • thenCombine:合并两个结果,有返回值

// thenAcceptBoth
f1.thenAcceptBoth(f2, (a, b) -> 
    System.out.println(a + b));

// thenCombine
CompletableFuture<String> combined = 
    f1.thenCombine(f2, (a, b) -> a + b);

Q39:CompletableFuture 如何实现超时控制?

答: JDK 9+ 用 orTimeout() / completeOnTimeout():

// 超时抛异常
CompletableFuture.supplyAsync(() -> slowTask())
    .orTimeout(3, TimeUnit.SECONDS)
    .exceptionally(ex -> "超时默认值");

// 超时返回默认值
CompletableFuture.supplyAsync(() -> slowTask())
    .completeOnTimeout("默认值", 3, TimeUnit.SECONDS);

JDK 8:用 ScheduledExecutorService 手动实现:

ScheduledExecutorService scheduler = ...;
scheduler.schedule(() -> future.complete("超时"), 
    3, TimeUnit.SECONDS);

Q40:CompletableFuture 的典型应用场景?

答:

  1. 并行调用多个服务:

CompletableFuture<User> user = 
    CompletableFuture.supplyAsync(() -> userService.get(id));
CompletableFuture<Order> order = 
    CompletableFuture.supplyAsync(() -> orderService.get(id));
CompletableFuture.allOf(user, order).join();
  1. 异步流水线:

CompletableFuture.supplyAsync(() -> fetch())
    .thenApply(data -> transform(data))
    .thenAccept(result -> save(result));
  1. 超时降级:

future.orTimeout(1, TimeUnit.SECONDS)
      .exceptionally(ex -> fallback());
  1. 组合多个结果:

f1.thenCombine(f2, (a, b) -> a + b)
  .thenCombine(f3, (ab, c) -> ab + c);

四、综合对比

Q41:ThreadLocal、Fork/Join、CompletableFuture 的定位?

答:

工具

定位

解决问题

ThreadLocal

线程隔离

共享变量竞争

Fork/Join

并行计算

分治任务

CompletableFuture

异步编排

异步任务组合


Q42:三者能配合使用吗?

答: 能:

// ThreadLocal 传递上下文
private static final ThreadLocal<String> TRACE = new ThreadLocal<>();

// CompletableFuture 异步编排
CompletableFuture.supplyAsync(() -> {
    TRACE.set("trace-1");
    return doWork();
}, executor)
.thenApplyAsync(r -> {
    // 注意:线程池中 ThreadLocal 不自动传递
    return r + TRACE.get();
}, executor)
.whenComplete((r, ex) -> TRACE.remove());

注意:CompletableFuture 切换线程时,ThreadLocal 不传递,需用 TTL。


Q43:如何选择异步编程工具?

答:

场景

推荐

简单异步

CompletableFuture

CPU 密集分治

Fork/Join

线程上下文

ThreadLocal

复杂流程编排

CompletableFuture

大数据并行

Fork/Join / parallelStream


Q44:ThreadLocal 在 CompletableFuture 中如何传递?

答: 用 TransmittableThreadLocal:

TransmittableThreadLocal<String> tl = new TransmittableThreadLocal<>();
tl.set("trace-1");

ExecutorService pool = TtlExecutors.getTtlExecutorService(
    Executors.newFixedThreadPool(3));

CompletableFuture.supplyAsync(() -> {
    return tl.get(); // 正确获取 trace-1
}, pool);

原因:普通 ThreadLocal 在线程池切换时丢失。


Q45:Fork/Join 和 parallelStream 有什么关系?

答: parallelStream 底层用 ForkJoinPool.commonPool():

list.parallelStream()
    .filter(...)
    .map(...)
    .collect(...);
// 底层:ForkJoinPool.commonPool()

注意:

  • 共享 commonPool,可能互相影响

  • 可用自定义 ForkJoinPool:

ForkJoinPool pool = new ForkJoinPool(8);
pool.submit(() -> list.parallelStream()...).get();

评论