ThreadLocal
底层数据结构:ThreadLocal的数据存在Thread对象里,每个Thread都有一个threadLocals字段,它的类型是ThreadLocalMap,这个map的key是ThreadLocal对象本身,value是你存的值![[Pasted image 20260430103005.png]]
为什么需要ThreadLocal
使用ThreadLocal的主要目的是解决多线程环境下的线程安全问题。每个线程都有独立的变量副本,从而避免了多个线程对共享变量的竞争,使得不需要加锁也能保证线程安全。同时它简化了深层调用中的上下文信息传递,用ThreadLocal存一下,哪层需要哪层直接get,实现代码解耦
ThreadLocal是如何实现线程隔离的?
ThreadLocal通过在每个线程内部维护一个ThreadLocalMap来实现线程隔离。该Map以ThreadLocal实例为键,以线程本地变量副本为值。当调用ThreadLocal的get()或set()方法时,实际上是当前线程从中取出或存入自己的那份数据,因此各线程之间互不干扰。
ThreadLocal可能引发内存泄露吗,怎么避免?
可能,因为ThreadLocal的key是弱引用,但value是强引用。很可能ThreadLocal被回收后,而value未及时回收,则会导致内存泄漏,应该调用remove()方法主动清理数据。
你刚才提到 ThreadLocal 会有内存泄漏,那为什么 Entry 的 key 要设计成弱引用,直接用强引用不行吗?
key不能用强引用,会导致ThreadLocal对象永久无法回收,严重内存泄漏,弱引用key保证ThreadLocal对象能及时被GC,缩小泄露范围,真正杜绝必须手动调用
死锁
- 互斥条件:每个资源要么分配给一个线程,要么可用
- 占用且等待:一个线程已经占有至少一个资源,而又在等另一个被其他线程占有的资源
- 不可剥夺:线程占用的资源不能被剥夺,资源只能在使用由线程自己释放
- 环路等待:线程A等待线程B占有的资源,而C在等待B...从而形成等待环路
解决方案:
- 把资源独享改为共享,可以资源冗余,多搞几份相同资源
- 一次性请求所需的所有资源
- 如果一个进程得不到所需的资源就应该释放所持有的资源,或者使用优先级剥夺
- 对系统中的资源进行排序,线程按序请求资源
排查死锁:jps(查看进程PID)+jstack(打印线程栈),线上用arthas的thread -b一键查出阻塞死锁线程,打印栈信息,锁对象,等待关系
CompletableFuture
老版的Future是用来拿到另一个线程里的任务的执行结果,相当于是一个凭证,
等到需要结果的时候再调用get(),这时候主线程才开始阻塞等待拿结果。
痛点:一是get()方法会阻塞线程,二是没法把异步任务串起来
因此java8引入了CompletableFuture ,支持链式调用,可以用thenApply,thenCompose这些方法把多个异步操作串起来,下一个任务完成自动触发下一个,不用手动阻塞等待结果

常见的坑:
- 忘了处理异常:如果不加 exceptionally(只接异常) 或 handle(成功和异常全接,成功再加工,失败来兜底),异常会被吞掉,二者选其一就行。
- get 超时:虽然 CompletableFuture 主打不阻塞,但有时候还是得等结果。用 get(timeout, unit) 而不是 get(),避免无限等待
- 回调里阻塞:在 thenApply 这些回调里搞阻塞操作,会拖慢整个线程池。如果必须阻塞,用 thenApplyAsync 把后续操作扔到独立线程池
- allOf 拿不到结果:allOf 返回的是
CompletableFuture<Void>,拿结果得用 join 逐个取
如何组合多个 CompletableFuture?
可以使用 thenCombine 合并两个独立的 CompletableFuture 的结果;
使用 thenCompose 实现异步任务的串联,将一个异步任务的结果作为下一个异步任务的输入。
thenApply 和 thenApplyAsync 有什么区别?
thenApply 用的是上个任务的执行线程,如果任务已完成,就用当前调用 thenApply 的线程。thenApplyAsync 重新从线程池拿一个新线程异步干,默认是 commonPool,也可以传自定义线程池。如果回调里有耗时操作,用 Async 版本避免阻塞。
怎么实现多个异步任务,任意一个成功就返回?
用 anyOf,但它有个问题,异常也算完成。如果想要第一个成功的结果,得自己写逻辑,用一个 CompletableFuture 作为结果容器,每个任务完成时调 complete,因为 complete 只有第一次调用生效,后面的会被忽略。
生产环境用 CompletableFuture 有什么注意事项?
回答:几个关键点:一是必须用自定义线程池,不要用 commonPool;二是必须处理异常,不然异常会被吞;三是设置合理的超时,避免无限等待;四是监控线程池状态,队列堆积要报警。另外日志里要打印任务执行线程名,方便排查问题。