Java技术八股学习Day13

1. ThreadLocal 相关

ThreadLocal 有什么用?

ThreadLocal 用于让每个线程拥有专属本地变量,避免多线程数据竞争和线程安全问题。每个访问该变量的线程会获得独立副本,可通过 get () 方法获取、set () 方法修改自身副本,不同线程间数据互不干扰,适用于压测流量标记、分布式系统上下文传递等场景。

ThreadLocal 原理

Thread 类中维护 threadLocals 和 inheritableThreadLocals 两个 ThreadLocalMap 类型变量(默认 null),ThreadLocalMap 可看作定制化 HashMap。ThreadLocal 仅作为封装,调用其 set ()/get () 方法时,会先获取当前线程,再通过 getMap () 拿到线程的 ThreadLocalMap,最终将键值对(ThreadLocal 为 key,存储值为 value)存入该 map 中,变量实际存储在当前线程的 ThreadLocalMap 里。

ThreadLocal 内存泄露原因及避免方法

内存泄露的核心原因是 ThreadLocalMap 的 Entry 中,key 是 ThreadLocal 的弱引用(无强引用时会被 GC 回收),而 value 是强引用。当 ThreadLocal 实例失去强引用且线程持续存活(如线程池线程)时,key 变为 null 但 value 仍被引用,无法被 GC 回收,导致内存泄露。避免方式:使用完 ThreadLocal 后务必调用 remove () 方法显式移除 entry,线程池场景下可通过 try-finally 块确保 remove () 必执行。

如何跨线程传递 ThreadLocal 的值?

异步场景下父子线程的 ThreadLocal 值无法直接传递,解决方案有二:一是 InheritableThreadLocal(JDK1.2 提供,继承 ThreadLocal,创建子线程时会继承父线程的 ThreadLocal 值,但不支持线程池场景);二是 TransmittableThreadLocal(阿里开源,简称 TTL,继承并加强 InheritableThreadLocal,支持线程池场景下的值传递,通过装饰器模式改造线程和线程池实现)。

InheritableThreadLocal 原理

Thread 类中新增 inheritableThreadLocals 变量(ThreadLocalMap 类型)存储需跨线程传递的值。Thread 构造方法调用 init () 时,会获取父线程的 inheritableThreadLocals,若不为 null 则赋值给子线程的同名变量,实现父线程 ThreadLocal 值向子线程的传递。

TransmittableThreadLocal 原理

阿里无法修改 JDK 源码,通过装饰器模式增强功能:一是实现自定义 Thread,在 run () 方法内完成 ThreadLocal 变量赋值;二是装饰线程池,在 execute () 方法中提交自定义 Thread 而非 JDK 原生 Thread,从而支持线程池场景下的 ThreadLocal 值传递。


2. 线程池相关

什么是线程池?

线程池是管理线程的资源池,任务提交时直接从池中获取线程处理,任务完成后线程不销毁,而是返回池中等待下一个任务,避免频繁创建和销毁线程的开销。

为什么要用线程池?

一是降低资源消耗,线程可复用,减少创建 / 销毁线程的开销;二是提高响应速度,核心线程常驻,任务无需等待线程创建即可执行;三是提高线程可管理性,可配置核心参数(核心线程数、最大线程数等)控制并发量,避免资源耗尽,且支持监控线程池运行状态,便于调优。

如何创建线程池?

主要有两种方式:一是通过 ThreadPoolExecutor 构造函数直接创建(推荐),可明确指定核心参数,精细控制线程池行为;二是通过 Executors 工具类创建(不推荐生产环境),可创建 FixedThreadPool、SingleThreadExecutor、CachedThreadPool、ScheduledThreadPool 等,但存在 OOM 风险。

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

《阿里巴巴 Java 开发手册》强制不推荐,原因:FixedThreadPool 和 SingleThreadExecutor 使用无界的 LinkedBlockingQueue(最大长度 Integer.MAX_VALUE),可能堆积大量请求导致 OOM;CachedThreadPool 使用 SynchronousQueue,允许创建的线程数为 Integer.MAX_VALUE,任务过多时可能创建大量线程导致 OOM;ScheduledThreadPool 和 SingleThreadScheduledExecutor 使用无界的 DelayedWorkQueue,可能堆积大量请求导致 OOM。

线程池常见参数及解释

ThreadPoolExecutor 的核心参数有 7 个:corePoolSize(任务队列未满时可同时运行的最大线程数,核心线程数)、maximumPoolSize(任务队列满时可同时运行的最大线程数,最大线程数)、keepAliveTime(非核心线程空闲后的最长存活时间)、unit(keepAliveTime 的时间单位)、workQueue(存储等待执行任务的阻塞队列)、threadFactory(创建线程的工厂,默认即可)、handler(任务无法处理时的拒绝策略)。其中 corePoolSize、maximumPoolSize、workQueue 是最关键的三个参数,决定线程池任务处理策略。

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

默认不会,核心线程空闲时也会常驻以减少创建开销。若线程池用于低频周期性场景,可调用 allowCoreThreadTimeOut (true) 开启核心线程超时回收,此时空闲核心线程超过 keepAliveTime 会被回收,需确保 keepAliveTime 大于 0。

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

分两种情况:未设置核心线程存活时间时,核心线程空闲时处于 WAITING 状态,持续等待任务;设置存活时间后,核心线程空闲时处于 WAITING 状态,若阻塞等待时间超过存活时间,会退出并从线程池移除,状态变为 TERMINATED。队列有任务时,阻塞线程会被唤醒,状态变为 RUNNABLE 并执行任务。

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

共四种:一是 AbortPolicy(默认),抛出 RejectedExecutionException 拒绝新任务;二是 CallerRunsPolicy,由调用 execute () 方法的线程执行被拒绝的任务,不丢弃任务但可能降低提交速度;三是 DiscardPolicy,直接丢弃新任务不处理;四是 DiscardOldestPolicy,丢弃队列中最早未处理的任务。若不允许丢弃任务,推荐 CallerRunsPolicy,但需注意耗时任务可能导致主线程阻塞,可通过增大队列容量、调整堆内存或任务持久化(存入 MySQL/Redis/ 消息队列)解决风险。

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

不同线程池适配不同阻塞队列:LinkedBlockingQueue(无界队列,容量 Integer.MAX_VALUE)适配 FixedThreadPool 和 SingleThreadExecutor;SynchronousQueue(同步队列,无容量,不存储元素)适配 CachedThreadPool;DelayedWorkQueue(延迟无界队列,按延迟时间排序,底层数组自动扩容)适配 ScheduledThreadPool 和 SingleThreadScheduledExecutor;ArrayBlockingQueue(有界队列,数组实现,容量创建后不可修改)可自定义配置。

线程池处理任务的流程

提交任务后,流程如下:1. 若当前运行线程数 <核心线程数,新建核心线程执行任务;2. 若当前运行线程数 ≥ 核心线程数,将任务加入阻塞队列;3. 若队列已满且当前运行线程数 < 最大线程数,新建非核心线程执行任务;4. 若队列已满且当前运行线程数 ≥ 最大线程数,触发拒绝策略处理任务。此外,可通过 prestartCoreThread () 启动单个核心线程预热,或 prestartAllCoreThreads () 启动所有核心线程预热。

线程池中线程异常后,销毁还是复用?

分提交方式:一是 execute () 提交,任务未捕获异常时,线程会终止,线程池会创建新线程替换,保持配置线程数不变,异常会打印到控制台 / 日志;二是 submit () 提交,任务异常会被封装在 Future 对象中,线程不会终止,可通过 Future.get () 捕获 ExecutionException,线程继续复用。

如何给线程池命名?

默认线程名无业务含义(如 pool-1-thread-n),命名方式有二:一是利用 Guava 的 ThreadFactoryBuilder,通过 setNameFormat () 设置名称前缀;二是自定义 ThreadFactory 实现,重写 newThread () 方法,为线程设置包含业务含义的名称,便于问题定位。

如何设定线程池的大小?

线程池大小需适配任务类型,避免过大或过小:CPU 密集型任务(如大量计算),线程数设为 N(CPU 核心数)+1,多余线程应对任务偶发暂停,充分利用 CPU 空闲时间;I/O 密集型任务(如读写文件、网络请求),线程数设为 2N,利用线程等待 I/O 的时间让其他线程执行任务。更严谨的公式:最佳线程数 = N × (1 + WT/ST),其中 WT 为线程等待时间,ST 为线程计算时间,可通过 VisualVM 查看 WT/ST 比例调整。

如何动态修改线程池的参数?

核心思路是修改 ThreadPoolExecutor 的核心参数,美团采用的方案:一是利用 ThreadPoolExecutor 提供的 setCorePoolSize ()、setMaximumPoolSize ()、setKeepAliveTime () 等方法,动态调整核心线程数、最大线程数、非核心线程存活时间等;二是自定义 ResizableCapacityLinkedBlockingQueue 队列,移除 capacity 字段的 final 修饰,实现队列长度动态修改。开源方案可选用 Hippo4j 或 Dynamic TP,支持参数动态变更、监控和告警。

如何设计一个支持任务优先级的线程池?

核心是使用 PriorityBlockingQueue(优先级阻塞队列)作为任务队列,该队列是线程安全的无界队列,底层基于小顶堆实现,值最小的元素优先出队。需确保提交的任务具备排序能力:一是任务实现 Comparable 接口并重写 compareTo () 方法;二是创建队列时传入 Comparator 对象指定排序规则(推荐)。注意风险:队列无界可能导致 OOM(可重写 offer () 方法限制元素数量)、低优先级任务可能饥饿(可优化设计提升长时间等待任务的优先级)、排序和并发控制会降低性能(大部分场景可接受)。


3. Future 相关

Future 类有什么用?

Future 是异步思想的体现,用于处理耗时任务:将耗时任务交给子线程异步执行,主线程可执行其他操作,后续通过 Future 类获取任务结果。Future 接口定义 5 个方法,支持取消任务、判断任务是否取消 / 完成、获取任务结果(get () 方法阻塞,支持超时获取),是多线程领域的经典 Future 模式。

Callable 和 Future 有什么关系?

通过 FutureTask 关联:FutureTask 实现 Future 和 Runnable 接口,常用来封装 Callable 和 Runnable(Runnable 会通过适配器转换为 Callable),是 ExecutorService.submit () 方法的返回类型。FutureTask 管理任务执行状态,存储 Callable 的 call () 方法执行结果,可作为任务直接被线程执行,实现了 Callable 任务与 Future 结果获取的绑定。

CompletableFuture 类有什么用?

CompletableFuture 是 Java8 引入,解决 Future 的局限性(不支持任务编排、get () 方法阻塞)。它同时实现 Future 和 CompletionStage 接口,不仅具备 Future 的核心功能,还支持函数式编程和异步任务编排组合(如串联、并行、依赖执行),可将多个异步任务组成链式调用,提升异步编程效率。

一个任务依赖另外两个任务执行完后再执行,怎么设计?

通过 CompletableFuture 的 allOf () 方法实现:先创建两个任务对应的 CompletableFuture 实例(如 futureT1、futureT2),调用 CompletableFuture.allOf (futureT1, futureT2) 合并两个任务,得到 bothCompleted 实例,再通过 bothCompleted.thenRunAsync () 注册回调任务(如 T3),即可实现 T3 在 T1 和 T2 都执行完后再执行。

使用 CompletableFuture 时,有一个任务失败如何处理异常?

需正确处理避免异常丢失或不可控:一是用 whenComplete 方法,任务完成时触发回调,同时处理正常结果和异常;二是用 exceptionally 方法,捕获异常并重新抛出,确保异常传播;三是用 handle 方法,处理正常结果和异常并返回新结果;四是用 CompletableFuture.allOf () 组合多个任务时,统一处理所有任务的异常。

为什么使用 CompletableFuture 要自定义线程池?

CompletableFuture 默认使用全局共享的 ForkJoinPool.commonPool (),所有未指定执行器的异步任务都会共享该线程池,若多个应用、库同时使用,可能导致资源竞争和线程饥饿,影响系统性能。自定义线程池可实现任务隔离(不同任务用独立线程池)、资源控制(按需调整线程池大小和队列类型)、优化异常处理(通过自定义 ThreadFactory 处理线程异常),提升系统稳定性。


4. AQS 相关

AQS 是什么?

AQS(AbstractQueuedSynchronizer,抽象队列同步器)是 JDK1.5 提供的 Java 并发核心组件,为同步器(如 ReentrantLock、Semaphore、CountDownLatch)提供通用执行框架。它封装底层线程同步机制,定义资源获取和释放的通用流程,具体同步逻辑由子类重写模板方法实现,是同步器的基础 "底座"。

AQS 的原理是什么?

核心思想:若共享资源空闲,将当前请求线程设为有效工作线程,标记资源为锁定状态;若资源被占用,通过基于 CLH 锁变体的双向队列管理阻塞线程,实现线程等待和唤醒时的锁分配。AQS 用 volatile 修饰的 int 变量 state 表示同步状态,通过 getState ()、setState ()、compareAndSetState () 操作状态;等待队列是 CLH 变体队列(自旋 + 阻塞,双向队列),线程被封装为 Node 节点(包含线程引用、等待状态、前后驱节点),通过队列实现线程排队和唤醒。


5. Semaphore 相关

Semaphore 有什么用?

Semaphore(信号量)用于控制同时访问特定资源的线程数量,区别于 synchronized 和 ReentrantLock(一次仅允许一个线程访问)。通过 acquire () 获取许可、release () 释放许可,初始许可数可配置,许可数为 1 时退化为排他锁。支持公平模式(FIFO 顺序获取许可)和非公平模式(抢占式,默认),常用于单机限流场景(分布式限流推荐 Redis+Lua)。

Semaphore 的原理是什么?

Semaphore 是共享锁实现,默认构造时将 AQS 的 state 设为许可数(permits)。调用 acquire () 时,线程尝试获取许可:state ≥ 0 则通过 CAS 操作 state-1 成功获取;state < 0 则创建 Node 节点加入阻塞队列并挂起。调用 release () 时,线程通过 CAS 操作 state+1 释放许可,同时唤醒队列中的一个线程,被唤醒线程再次尝试 state-1,成功则获取许可,失败则重新挂起。


6. CountDownLatch 相关

CountDownLatch 有什么用?

CountDownLatch 允许 count 个线程阻塞在某点,直至所有线程的任务执行完毕。它是一次性的,计数器仅能在构造方法中初始化一次,使用后无法重置,适用于需等待多个线程完成后再执行后续逻辑的场景(如多线程读取多个文件后统计结果)。

CountDownLatch 的原理是什么?

CountDownLatch 是共享锁实现,构造时将 AQS 的 state 设为 count。线程调用 countDown () 时,通过 CAS 操作 state-1,直至 state 为 0;线程调用 await () 时,若 state ≠ 0 则阻塞,直至 state 为 0 或线程被中断,此时阻塞线程被唤醒,继续执行后续逻辑。

CountDownLatch 的使用场景及改进方案?

使用场景:多线程执行无依赖任务,需等待所有任务完成后汇总结果(如多线程处理多个文件)。改进方案:可用 CompletableFuture 替代,通过 supplyAsync () 创建多个任务对应的 CompletableFuture 实例,再用 allOf () 合并,调用 join () 等待所有任务完成,无需手动管理计数器,且支持更灵活的任务编排,任务过多时可通过循环批量添加任务。


7. CyclicBarrier 相关

CyclicBarrier 有什么用?

CyclicBarrier(可循环屏障)与 CountDownLatch 类似,用于线程间等待,但支持循环使用。它让一组线程到达屏障(同步点)时阻塞,直至最后一个线程到达,屏障才打开,所有被拦截的线程继续执行,底层基于 ReentrantLock 和 Condition 实现,应用场景与 CountDownLatch 类似,但可重复利用。

CyclicBarrier 的原理是什么?

内部维护 parties(拦截的线程数)和 count(计数器,初始值 = parties)。线程调用 await () 方法时,计数器 count-1,若 count ≠ 0 则线程阻塞;若 count = 0 则执行构造方法中指定的 barrierAction(可选),随后重置 count 为 parties,唤醒所有阻塞线程,进入下一轮拦截,实现循环使用。


8. 虚拟线程相关

虚拟线程是 Java21 正式发布的重量级更新,是轻量级线程,与平台线程存在关联,具备自身的优缺点。创建方式有特定 API,底层原理基于线程调度优化,虽目前面试中提问较少,但建议简单了解其核心概念(如与平台线程的区别、适用场景),应对未来面试趋势。

相关推荐
忧郁的Mr.Li9 小时前
SpringBoot中实现多数据源配置
java·spring boot·后端
yq1982043011569 小时前
静思书屋:基于Java Web技术栈构建高性能图书信息平台实践
java·开发语言·前端
一个public的class9 小时前
你在浏览器输入一个网址,到底发生了什么?
java·开发语言·javascript
有位神秘人9 小时前
kotlin与Java中的单例模式总结
java·单例模式·kotlin
golang学习记9 小时前
IntelliJ IDEA 2025.3 重磅发布:K2 模式全面接管 Kotlin —— 告别 K1,性能飙升 40%!
java·kotlin·intellij-idea
爬山算法9 小时前
Hibernate(89)如何在压力测试中使用Hibernate?
java·压力测试·hibernate
消失的旧时光-194310 小时前
第十四课:Redis 在后端到底扮演什么角色?——缓存模型全景图
java·redis·缓存
BD_Marathon10 小时前
设计模式——依赖倒转原则
java·开发语言·设计模式
BD_Marathon10 小时前
设计模式——里氏替换原则
java·设计模式·里氏替换原则
Coder_Boy_10 小时前
Deeplearning4j+ Spring Boot 电商用户复购预测案例中相关概念
java·人工智能·spring boot·后端·spring