Dev Guide / Java开发技术guide / java
content/dev-guide/a-Java开发技术guide/02-java/3-并发编程.md
- 进程与线程
- 进程是程序的一次执行过程,是系统运行程序的基本单位
- 线程是程序中最小的执行单元,一个进程可以包含多个线程,同类的多个线程共享进程的堆和方法区资源,但每个线程自己的程序计数器、虚拟机栈和本地方法栈。
- Java线程与系统线程的区别:现在的 Java 线程的本质其实就是操作系统的线程,JVM 直接使用操作系统原生的内核级线程(内核线程)来实现 Java 线程,由操作系统内核进行线程的调度和管理(一个用户线程对应一个内核线程)。
- 程序计数器为什么是私有的?程序计数器(用于记录当前线程执行的位置)私有主要是为了线程切换后能恢复到正确的执行位置
- 虚拟机栈:每个 Java 方法在执行之前会创建一个栈帧用于存储局部变量(代码块和方法中的变量)表、操作数栈、常量池引用等信息
- 本地方法栈:和虚拟机栈所发挥的作用非常相似,区别是:虚拟机栈为虚拟机执行 Java 方法 (也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native 方法服务
- 为了保证线程中的局部变量不被别的线程访问到,虚拟机栈和本地方法栈是线程私有的
- 线程的生命周期:
- new:就绪状态
- runnable:运行状态,线程被调用了 start(),还在等待运行的状态(操作系统层面:处于ready状态,获得CPU时间片后就处于运行状态running)
- blocked:阻塞状态,线程获取锁资源失败并进入休眠状态,需要等待锁释放(争夺锁失败,锁被其他线程持有,退出阻塞状态条件:持有锁的线程释放锁,且本线程抢到)
- waiting:等待状态,该线程调用无超时等待方法(如Object.wait()、Thread.join()、LockSupport.park()),主动交出执行权,并进入休眠状态(退出休眠状态:需要等待其他线程做出一些特定动作如通知或中断)。
- time_waiting:超时等待,该线程主动调用带超时等待方法(如Thread.sleep(ms)、Object.wait(ms)、Thread.join(ms)、lock.tryLock(time) 多了超时参数。时间一到自动恢复,不用等别人叫)
- terminated:终止状态,表示该线程已经运行完毕
- Object.wait(): 线程必须在synchronized代码块内调用该方法,交出执行权,线程被唤醒后,需要重新获取锁资源(JVM规定,调用wait方法的线程必须是该对象监视器(Monitor)的当前持有者)。
- Object#wait()和Thread#sleep()的区别
- 实例wait()是对象实例方法,sleep()是静态方法
- sleep()在任意地方调用,不释放锁;sleep()线程必须在synchronized代码块内调用,调用时瞬间释放当前对象监视器锁
- 唤醒方法:sleep()时间到自动醒/interrupt() 中断; wait()需要notify()/notifyAll() 显式唤醒 / 超时自动醒(wait(ms))
- 线程状态:调用sleep()后线程进入TIME_WAITING状态;wait()调用后进入WAITING或者TIME_WAITING
- wait() 通常被用于线程间交互/通信,sleep()通常被用于暂停执行
- 创建线程的方法:继承Thread类、实现Runnable接口、实现Callable接口、使用线程池、使用CompletableFuture类等等,严格来说,Java 就只有一种方式可以创建线程,就是通过new Thread().start()创建
- 多线程
- 线程更轻量级,是程序执行的最小单位,线程间的切换和调度的成本远远小于进程,开销较小
- 单核CPU支持多线程吗?操作系统通过时间片轮转的方式,将 CPU 的时间分配给不同的线程。Java 使用的线程调度是抢占式的,也就是说,JVM 本身不负责线程的调度,而是将线程的调度委托给操作系统。
- 并发编程的三个特性:原子性、可见性、有序性
- 线程死锁
- 线程死锁:多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放
- 死锁产生的四个条件:互斥条件、请求与保持条件、不剥夺条件、循环等待条件
- Java内存模型
- CPU缓存:解决 CPU 处理速度和内存处理速度不对等的问题。
- 指令重排序:系统在执行代码的时候并不一定是按照你写的代码的顺序依次执行
- Java 源代码会经历 编译器优化重排 —> 指令并行重排 —> 内存系统重排 的过程,最终才变成操作系统可执行的指令序列。
- JMM:Java 内存模型抽象了线程和主内存之间的关系,就比如说线程之间的共享变量必须存储在主内存中
- happens-before原则
- volatile
- 保证变量的可见性(禁用 CPU 缓存,如果我们将一个变量使用 volatile 修饰,这就指示编译器,这个变量是共享且不稳定的,每次使用它都到主存中进行读取)
- 防止 JVM 的指令重排序:如果volatile使用声明变量,在对这个变量进行读写操作的时候,会通过插入特定的内存屏障的方式来禁止指令重排序
- volatile 关键字能保证变量的可见性,但不能保证对变量的操作是原子性的。
- JMM 中的 happens-before 原则是判断数据是否存在竞争、线程是否安全的重要依据。volatile 变量的读写操作与 happens-before 原则有着密切的关系(如果线程 A 写入了一个 volatile 变量,线程 B 随后读取了同一个 volatile 变量,那么线程 A 在写入 volatile 变量之前所做的所有修改(包括对非 volatile 变量的修改),对线程 B 都是可见的)。
- 乐观锁和悲观锁
- 乐观锁实现
- 版本号机制:一般是在数据表中加上一个数据版本号 version 字段,表示数据被修改的次数。当数据被修改时,version 值会加一。当线程 A 要更新数据值时,在读取数据的同时也会读取 version 值,在提交更新时,若刚才读取到的 version 值为当前数据库中的 version 值相等时才更新,否则重试更新操作,直到更新成功
- CAS算法:Java原子类
- synchronized
- 重量级锁:JVM的监视器锁(monitor)是依赖于底层的操作系统的 Mutex Lock 来实现的,Java 的线程是映射到操作系统的原生线程之上的。如果要挂起或者唤醒一个线程,都需要操作系统帮忙完成,而操作系统实现线程之间的切换时需要从用户态转换到内核态,这个状态之间的转换需要相对比较长的时间,时间成本相对较高
- 锁优化:在 Java 6 之后, synchronized 引入了大量的优化如自旋锁、适应性自旋锁、锁消除、锁粗化、偏向锁、轻量级锁等技术来减少锁操作的开销
- 锁的升级:锁主要存在四种状态,依次是:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态
- synchronized 与 volatile
- volatile 关键字是线程同步的轻量级实现,但只能修饰变量,synchronized修饰代码块和方法
- volatile 关键字能保证数据的可见性,但不能保证数据的原子性。synchronized 关键字两者都能保证
- volatile关键字主要用于解决变量在多个线程之间的可见性,而 synchronized 关键字解决的是多个线程之间访问资源的同步性
- ReentrantLock
- ReentrantLock 实现了 Lock 接口,是一个可重入且独占式的锁,和 synchronized 关键字类似。不过,ReentrantLock 更灵活、更强大,增加了轮询、超时、中断、公平锁和非公平锁等高级功能
- synchronized 和 ReentrantLock:
- 都是可重入锁;
- synchronized 依赖于 JVM 而 ReentrantLock 依赖于 API;
- ReentrantLock 比 synchronized 增加了一些高级功能:等待可中断、可实现公平锁(synchronized只支持非公平锁)、通知机制更强大、支持超时
- StampedLock:读写锁(不可冲入),适合读多写少的场景,可作为ReentrantReadWriteLock的替代品
- Atomic原子类
- 基本类型:AtomicInteger、AtomicLong、AtomicBoolean
- 数组类型:AtomicIntegerArray、AtomicLongArray、AtomicReferenceArray
- 引用类型:AtomicReference
- 对象属性的修改类型:AtomicIntegerFieldUpdater、AtomicLongFieldUpdater、AtomicReferenceFieldUpdater
- ThreadLocal类
- ThreadLocal是线程局部变量,为每个线程提供独立的变量副本,实现线程间数据隔离,互不干扰
- 底层原理:每个Thread对象内部维护一个ThreadLocalMap,key为ThreadLocal实例,value为线程局部变量值
- 内存泄漏风险:ThreadLocalMap使用弱引用key(ThreadLocal),但value是强引用,线程长时间存活会导致value无法回收
- 最佳实践:使用完ThreadLocal后必须调用remove()方法清理,避免内存泄漏,不要存大对象(推荐轻量级 ID、枚举、状态码)
- 常见应用场景:
- 用户会话信息存储userId,日志链路追踪traceId等
- 数据库连接管理、事务上下文传递(Java 企业级开发中的行业标准实践,几乎所有主流框架Spring、Hibernate、MyBatis、Dubbo 等底层都重度依赖 ThreadLocal)
- 非线程安全对象的线程级缓存(SimpleDateFormat 或 DecimalFormat 不是线程安全的,多线程共用会报错,比new更省内存)
- 面试高频考点:
- ThreadLocal与synchronized的区别:ThreadLocal以空间换时间,synchronized以时间换空间
- 内存泄漏原因及解决方案:弱引用key + 强引用value的问题,必须手动remove()
- ThreadLocalMap的hash冲突解决:采用线性探测法而非链表
- initialValue()方法的作用:提供默认初始值,避免null值判断
- 父子线程数据传递:普通ThreadLocal无法跨线程传递,需使用InheritableThreadLocal
- InheritableThreadLocal:子线程可以继承父线程的ThreadLocal值,适用于线程池场景需配合TransmittableThreadLocal使用
- 线程池
- 线程池是一种管理和复用线程资源的机制,通过预先创建一定数量的线程,避免频繁创建销毁线程的开销,同时实现任务的高效调度和执行。
- 线程池的四大组件
- 线程控制数:corePoolSize/maximumPoolSize,决定池中“干活的线程”数量规模,必须按业务压测显式设置,禁用默认值
- 线程任务队列:workQueue,存放已提交但尚未被线程执行的任务,必须用有界队列,严禁无界防 OOM
- 线程工厂:threadFactory,负责创建新线程,可定制线程属性,必须带业务前缀,否则线上无法排查
- 拒绝策略:handler,队列满+线程达上限时,对新任务的处理方式,核心业务必须自定义或 CallerRuns,禁默认
- 创建线程池:通过 ThreadPoolExecutor 构造函数直接创建 (推荐)、通过 Executors 工具类创建 (内置线程池不推荐)
- 线程池的常见参数:
- corePoolSize:线程池的核心线程数量
- maximumPoolSize:线程池的最大线程数,当线程数大于该值,则创建非核心线程执行
- workQueue:任务队列,用来储存等待执行任务的队列
- keepAliveTime:当线程数大于核心线程数时,多余的空闲线程存活的最长时间
- unit:keepAliveTime 参数的时间单位
- threadFactory:线程工厂,用来创建线程,一般默认即可
- handler:拒绝策略,达到上线线程数时(线程池数量大于maximumPoolSize),执行拒绝策略
- 线程池的拒绝策略:
- AbortPolicy: 默认拒绝策略,直接抛出 RejectedExecutionException,非核心业务,适合允许快速失败
- CallerRunsPolicy:调用者线程自己执行该任务,适用于核心业务(自动降级同步,防丢数据+自带背压)
- DiscardPolicy:silently 丢弃新任务,适合监控埋点、日志等可丢失场景
- DiscardOldestPolicy:丢弃队列最老的未处理任务,然后重新尝试执行任务,适合实时性要求高、允许丢弃旧数据
- 线程池处理任务的流程(当向线程池提交一个任务时)
- 检查核心线程是否已满,如果当前运行的线程数 < corePoolSize,创建核心线程
- 尝试加入工作队列,如果当前线程数 >= corePoolSize且工作队列未满,将任务放入工作队列等待执行(此时不会创建新线程,而是等待空闲的核心线程从队列中取出任务)
- 创建非核心线程,工作队列已满且当前线程数 < maximumPoolSize,:创建新的非核心线程来立即执行任务(如果这些线程在空闲超过
keepAliveTime后会被回收)
- 工作队列已满且线程数已达到maximumPoolSize,根据配置的
RejectedExecutionHandler执行拒绝策略
- 常用应用:
- 本地异步执行:主流程不阻塞,后台静默处理(@Async,配置线程池)
- 并发加速:拆分大任务,多线程并行计算后聚合结果
- 资源隔离:慢任务/高危任务独立线程池,避免拖垮主线程(Tomcat 处理 HTTP 请求,但“导出报表”任务扔给独立线程池,防止占满 Web 线程)
- 流量削峰:通过队列容量+拒绝策略控制瞬时并发。如秒杀场景:请求进内存队列,线程池按固定速率消费,超出的直接拒绝或降级
- 定时/延迟任务:ScheduledThreadPoolExecutor 替代 Timer?
- Future模式
- Future类的异步思想运用,主要用在一些需要执行耗时任务的场景,避免程序一直原地等待耗时任务执行完成
- CompletableFuture 类:Future 在实际使用过程中存在一些局限性,比如不支持异步任务的编排组合、获取计算结果的 get() 方法为阻塞调用
- Future核心方法:
- get():阻塞获取结果,可设置超时时间防止无限等待
- isDone():判断任务是否完成
- cancel():尝试取消任务执行
- isCancelled():判断任务是否被取消
- CompletableFuture优势:
- 链式调用:thenApply/thenAccept/thenRun等方法支持函数式编程
- 组合操作:allOf/anyOf支持多个异步任务的组合
- 异常处理:exceptionally/whenComplete提供完善的异常处理机制
- 非阻塞:所有操作都是异步非阻塞的
- 面试高频考点:
- Future.get()阻塞原理:基于AQS的Condition.await()
- CompletableFuture线程池选择:默认使用ForkJoinPool.commonPool(),可自定义线程池
- 异步任务编排:如何优雅处理多个异步任务的依赖关系
- 超时控制:CompletableFuture没有内置超时,需结合delayedExecutor实现
- AQS
- 抽象队列同步器,位于java.util.concurrent.locks 包
- CLH队列:CLH队列(Craig, Landin, and Hagersten)
- AQS核心设计:
- state变量:int类型的同步状态,通过CAS操作保证原子性
- FIFO等待队列:双向链表结构,存储等待获取锁的线程节点
- 模板方法模式:子类通过重写tryAcquire/tryRelease等方法定义同步逻辑
- 独占模式 vs 共享模式:
- 独占模式:同一时刻只有一个线程能获取资源(如ReentrantLock)
- 共享模式:同一时刻多个线程可同时获取资源(如Semaphore/CountDownLatch)
- 常见实现类:
- ReentrantLock:独占模式,可重入
- ReentrantReadWriteLock:读写锁,读共享写独占
- Semaphore:信号量,控制并发数量
- CountDownLatch:倒计时门闩,等待多个线程完成
- CyclicBarrier:循环屏障,线程相互等待
- 面试高频考点:
- AQS如何避免死锁:通过CLH队列的FIFO特性保证公平性
- state变量的作用:表示锁的状态或资源数量
- Condition的实现原理:每个Condition对应一个等待队列
- 自旋优化:在阻塞前先自旋尝试获取锁,减少线程切换开销
- 虚拟线程:jdk21正式引入,略
- 虚拟线程(Virtual Threads)是JDK21的正式特性,属于Project Loom项目
- 核心特点:
- 轻量级:由JVM管理而非操作系统,创建成本极低(KB级别内存)
- 大量并发:可轻松创建百万级虚拟线程
- 与平台线程映射:M:N映射关系,多个虚拟线程映射到少量平台线程
- 无缝兼容:现有代码无需修改即可使用虚拟线程
- 使用方式:
- Thread.startVirtualThread(Runnable):直接启动虚拟线程
- Executors.newVirtualThreadPerTaskExecutor():虚拟线程执行器
- Structured Concurrency:结构化并发API(JDK21预览)
- 适用场景:
- IO密集型应用:Web服务器、数据库连接、文件操作等
- 高并发服务:微服务、API网关等需要处理大量并发请求的场景
- 不适用CPU密集型:虚拟线程无法提升CPU计算性能
- 面试高频考点:
- 虚拟线程vs平台线程:轻量级vs重量级,JVM管理vs操作系统管理
- 内存占用对比:虚拟线程KB级别,平台线程MB级别
- 与协程区别:Java虚拟线程是真正的线程语义,协程是协作式调度
- 性能提升原理:减少线程上下文切换开销,提高IO并发能力