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,底层原理基于线程调度优化,虽目前面试中提问较少,但建议简单了解其核心概念(如与平台线程的区别、适用场景),应对未来面试趋势。

相关推荐
木井巳8 分钟前
【记忆化搜索】最长递增子序列
java·算法·leetcode·深度优先·剪枝·推荐算法
杨杨杨大侠15 分钟前
知识库已经有了,Java 程序员还要做什么?Spring AI RAG 实战
java·openai·ai编程
小贺儿开发16 分钟前
Unity 结合百度AI开放平台 手写文字识别
人工智能·科技·学习·unity·云服务·文字识别·手写文字
传奇开心果编程20 分钟前
【SwiftUI入门练中学】第1课 从零开始
学习·ui·ios·swiftui·swift
伊信20 分钟前
Spring AI 基础学习与应用
java·后端
是隼人25 分钟前
buuctf-pwn ciscn_2019_sw_1(32位只有一次格式化机会)题解(学习过程持续更新)
c语言·学习·安全·pwn入门·ctf入门
夜郎king27 分钟前
基于 Java + Playwright 实现网站自动访问与数据采集(以腾讯云开发者社区为例)
java·playwright·网页数据获取
Anastasiozzzz32 分钟前
重新定义 Agent 基建:Redis 在现代 AI 与智能体系统中的工程实践
java·人工智能·redis·ai
careathers34 分钟前
【数据结构】队列
java·数据结构
步行cgn38 分钟前
Spring 通过 factory-method 实例化 Bean 详解与常见错误排查
java·后端