ThreadLocal和CompletableFuture

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;二是必须处理异常,不然异常会被吞;三是设置合理的超时,避免无限等待;四是监控线程池状态,队列堆积要报警。另外日志里要打印任务执行线程名,方便排查问题。

相关推荐
码云数智-园园1 小时前
Spring循环依赖:三级缓存到底解决了什么,没解决什么?
java·后端·spring
上海云盾商务经理杨杨1 小时前
企业级DDoS防护核心:流量清洗技术全解析
网络·安全·ddos
龙亘川1 小时前
城市更新×智慧治理:老旧小区改造中的数字化创新实践
java·大数据·人工智能·机器学习·智慧城市
无所事事O_o1 小时前
OPENSSL生成非对称加密公私钥
java·加密
yaoxin5211231 小时前
401. Java 文件操作基础 - 使用 Buffered Stream I/O 写入文本文件
java·开发语言·python
wefg12 小时前
【计算机网络】DNS/ICMP协议/ping指令
网络·网络协议·计算机网络
花间相见2 小时前
【全栈开发03】—— curl 常用参数详解与 HTTP 请求实战
网络·网络协议·http
青山师2 小时前
线程池深度解析:从生产者-消费者模型到工业级调优实践
java·面试题·线程池·多线程·java面试
qq_589568102 小时前
封装工具类,JwtUtils令牌工具类
java