Java并发面试题(二)

发布于 2025-09-23 16:32:42 浏览 86 次

1.什么是 Java 的 StampedLock?​

StampedLock 是 Java 8 新增的并发锁工具类,位于 java.util.concurrent.locks 包下,是对读写锁(ReentrantReadWriteLock)的优化升级,旨在解决读写锁 “读 - 读不互斥、读 - 写互斥” 场景下的性能瓶颈。​
其核心特性如下:​
基于 “戳记(Stamp)” 实现锁控制:每次获取锁时会返回一个 long 类型的 “戳记”,释放锁或转换锁类型时需传入该戳记,确保操作的正确性(戳记无效则操作失败)。​
支持三种锁模式:​
写锁(Write Lock):排他锁,同一时间仅允许一个线程获取,获取后禁止其他线程获取读锁或写锁;​
悲观读锁(Read Lock):共享锁,多个线程可同时获取,获取后禁止线程获取写锁;​
乐观读锁(Optimistic Read):非阻塞模式,获取时不阻塞线程,仅通过戳记验证数据是否被修改(若未修改则直接使用数据,若已修改则升级为悲观读锁或重试)。​
非重入性:StampedLock 不支持重入(同一线程多次获取同一锁会导致死锁),而 ReentrantReadWriteLock 是可重入的。​
适用场景:读操作远多于写操作、且读操作无需重入的场景(如缓存查询、数据统计),能显著提升并发读性能。

2.什么是 Java 的 CompletableFuture?​

CompletableFuture 是 Java 8 新增的异步编程工具类,位于 java.util.concurrent 包下,实现了 Future 和 CompletionStage 接口,解决了传统 Future 无法链式调用、无法优雅处理异常、无法组合多个异步任务的痛点。​
其核心特性如下:​
支持链式异步任务:通过 thenApply ()、thenAccept ()、thenRun () 等方法,可将多个异步任务按顺序串联,避免 “回调地狱”;​
支持多任务组合:提供 thenCombine ()(两个任务完成后合并结果)、allOf ()(所有任务完成后触发)、anyOf ()(任一任务完成后触发)等方法,轻松处理多任务协作;​
内置异常处理:通过 exceptionally ()、handle () 方法可优雅捕获异步任务中的异常,无需手动 try-catch;​
支持手动完成任务:通过 complete ()、completeExceptionally () 方法,可手动设置任务结果或异常,灵活控制任务状态;​
适用场景:异步调用(如 HTTP 请求、数据库查询)、多任务并行处理(如批量数据处理)、异步结果链式依赖(如 “查询用户→根据用户 ID 查订单→根据订单 ID 查物流”)。​

3.什么是 Java 的 ForkJoinPool?


ForkJoinPool 是 Java 7 新增的线程池实现,位于 java.util.concurrent 包下,基于 “分治思想(Divide and Conquer)” 设计,专门用于处理可拆分的计算密集型任务(即大任务拆分为多个小任务,并行计算后合并结果)。​
其核心特性如下:​
工作窃取(Work-Stealing)算法:每个线程维护一个独立的任务队列,当线程完成自身队列任务后,会 “窃取” 其他线程队列中的任务执行,减少线程空闲时间,提升 CPU 利用率;​
任务拆分与合并:需配合 ForkJoinTask 子类(RecursiveTask 有返回结果、RecursiveAction 无返回结果)使用,通过 fork () 方法拆分任务、join () 方法合并结果;​
默认并行度:若未指定并行度,默认等于当前 CPU 核心数(Runtime.getRuntime ().availableProcessors ()),避免线程过多导致上下文切换开销;​
适用场景:大规模数组排序(如 Arrays.parallelSort () 底层依赖 ForkJoinPool)、大文件分片处理、复杂计算(如矩阵运算、数据统计)等计算密集型任务。

4.如何在 Java 中控制多个线程的执行顺序?


Java 中控制多线程执行顺序的核心思路是 “通过同步机制让线程按预设顺序等待或唤醒”,常见实现方式如下:​
(1)使用 join () 方法​
Thread.join () 表示 “当前线程等待目标线程执行完毕后再继续”,通过链式调用 join () 可强制线程按顺序执行。​
示例:​

Thread t1 = new Thread (() -> System.out.println ("线程 1 执行"));​
Thread t2 = new Thread (() -> System.out.println ("线程 2 执行"));​
t1.start ();​
t1.join (); // 主线程等待 t1 执行完,再启动 t2​
t2.start ();​

(2)使用 CountDownLatch​
CountDownLatch 通过 “计数器” 控制线程顺序:先执行的线程完成后递减计数器,后执行的线程等待计数器归零后再启动。​
示例:​

CountDownLatch latch = new CountDownLatch (1);​
Thread t1 = new Thread (() -> {​
System.out.println ("线程 1 执行");​
latch.countDown (); // 计数器减 1(从 1→0)​
});​
Thread t2 = new Thread (() -> {​
try {​
latch.await (); // 等待计数器归零​
} catch (InterruptedException e) {​
Thread.currentThread ().interrupt ();​
}​
System.out.println ("线程 2 执行");​
});​
t1.start ();​
t2.start ();​

(3)使用 CyclicBarrier​
CyclicBarrier 适用于 “多线程按阶段执行,需等待所有线程完成当前阶段后再进入下一阶段” 的场景,间接控制执行顺序。​
(4)使用锁与条件变量(ReentrantLock + Condition)​
通过 Condition.await () 和 Condition.signal () 实现线程间的精准唤醒,控制执行顺序。例如:让线程 A→线程 B→线程 C 依次执行,可给每个线程绑定一个 Condition,完成后唤醒下一个线程。​
(5)使用线程池的 submit () 链式调用​
通过 CompletableFuture 的 thenRunAsync () 等方法,让异步任务按顺序执行(本质是线程池调度的顺序控制)。​

5.你使用过 Java 中的哪些阻塞队列?​

Java 中的阻塞队列均实现 BlockingQueue 接口,核心特性是 “当队列空时,获取元素的线程会阻塞;当队列满时,添加元素的线程会阻塞”,常用于线程池、生产者 - 消费者模型。常见实现类如下:​
(1)ArrayBlockingQueue​
底层结构:基于数组实现的有界阻塞队列(初始化时需指定容量,不可动态扩容);​
锁机制:使用一把独占锁(ReentrantLock)控制读写操作,读写互斥;​
适用场景:对队列容量有明确限制的场景(如固定大小的任务缓冲池)。​
(2)LinkedBlockingQueue​
底层结构:基于链表实现的阻塞队列,默认是无界队列(容量为 Integer.MAX_VALUE,也可手动指定容量);​
锁机制:使用两把独立的锁(takeLock 控制读、putLock 控制写),支持读写并行,性能优于 ArrayBlockingQueue;​
适用场景:任务数量不确定、需高效读写的场景(如 Executors.newFixedThreadPool () 底层默认使用此队列)。​
(3)SynchronousQueue​
底层结构:无容量的阻塞队列(不存储元素,添加元素后需等待另一个线程获取,否则阻塞);​
核心特性:“直接传递” 元素,避免队列存储开销;​
适用场景:线程间直接通信、任务需立即被处理的场景(如 Executors.newCachedThreadPool () 底层使用此队列)。​
(4)PriorityBlockingQueue​
底层结构:基于优先级堆实现的无界阻塞队列(元素按优先级排序,默认升序,可自定义 Comparator);​
注意点:仅保证 “获取元素时返回优先级最高的元素”,不保证元素插入顺序;​
适用场景:需按优先级处理任务的场景(如任务调度器)。​
(5)DelayQueue​
底层结构:基于 PriorityBlockingQueue 实现的延迟阻塞队列,元素需实现 Delayed 接口(指定延迟时间);​
核心特性:仅当元素延迟时间到期后,才能被获取;​
适用场景:定时任务调度(如定时清理过期数据)。​

6.你使用过 Java 中的哪些原子类?​

Java 中的原子类位于 java.util.concurrent.atomic 包下,基于 CAS(Compare-And-Swap)操作实现 “无锁化的线程安全”,避免了 synchronized 锁的上下文切换开销,适用于简单的变量原子操作。常见原子类分类及示例如下:​
(1)基本类型原子类​
AtomicInteger:原子操作 int 类型变量,提供 getAndIncrement ()(原子自增)、getAndSet ()(原子赋值)等方法;​
AtomicLong:原子操作 long 类型变量,功能与 AtomicInteger 类似,额外支持 longValue () 等方法;​
AtomicBoolean:原子操作 boolean 类型变量,常用于 “原子性的状态切换”(如标记任务是否完成)。​
(2)引用类型原子类​
AtomicReference:原子操作对象引用,支持原子性地更新对象引用(如原子替换某个对象);​
AtomicStampedReference:解决 AtomicReference 的 “ABA 问题”(通过 “版本号戳记” 记录引用的修改次数,避免误判);​
AtomicMarkableReference:通过 “布尔标记” 记录引用是否被修改,适用于只需判断 “是否修改过” 而无需版本号的场景。​
(3)数组类型原子类​
AtomicIntegerArray:原子操作 int 数组的元素;​
AtomicLongArray:原子操作 long 数组的元素;​
AtomicReferenceArray:原子操作对象引用数组的元素。​
(4)字段更新器原子类​
AtomicIntegerFieldUpdater:原子更新对象的 int 类型字段(字段需为 volatile 修饰的非静态字段);​
AtomicLongFieldUpdater:原子更新对象的 long 类型字段;​
AtomicReferenceFieldUpdater:原子更新对象的引用类型字段;​
适用场景:需原子更新对象私有字段,但不想修改类结构的场景(如第三方类的字段)。​
(5)累加器原子类​
LongAdder:优化版的 AtomicLong,高并发下通过 “分段累加” 减少 CAS 竞争,适合频繁自增的场景(如计数器、统计指标);​
DoubleAdder:原子操作 double 类型变量,原理与 LongAdder 类似。

7.你使用过 Java 的累加器吗?​

Java 中的累加器是 java.util.concurrent.atomic 包下的 LongAdder 和 DoubleAdder,是 JDK 8 为解决高并发下 AtomicLong/AtomicDouble 的性能瓶颈而设计的优化类,核心思路是 “分段累加”。​
(1)核心原理​
AtomicLong 的问题:高并发下所有线程竞争同一个变量的 CAS 操作,导致大量 CAS 失败重试,性能下降;​
LongAdder 的优化:将累加值拆分为多个 “分段变量(Cell 数组)”,每个线程优先操作自己的分段变量(减少竞争),最终通过 sum () 方法汇总所有分段变量的值。​
(2)常用方法​
add (long x):原子性地累加 x(x 可正可负);​
increment ():原子性地自增 1(等价于 add (1));​
decrement ():原子性地自减 1(等价于 add (-1));​
sum ():汇总所有分段变量的值,返回当前总累加值(非原子操作,高并发下可能存在短暂误差,适合最终统计场景);​
reset ():重置所有分段变量的值为 0(仅在单线程或无并发时使用)。​
(3)适用场景​
高并发下的计数器(如接口调用次数统计、用户访问量统计);​
无需精确实时获取累加值,仅需最终统计结果的场景(若需实时精确值,仍需使用 AtomicLong)。​
(4)与 AtomicLong 的对比​
310620461f5c7e0c2cc540a31a2bbbef.png

8.什么是 Java 的 CAS(Compare-And-Swap)操作?​

CAS(Compare-And-Swap,比较并交换)是 Java 并发编程中实现 “无锁化线程安全” 的核心机制,是一种硬件级别的原子操作(由 CPU 指令 cmpxchg 实现),确保多线程环境下变量修改的原子性。​
(1)核心逻辑​
CAS 操作涉及三个参数:​
内存地址 V:存储目标变量的内存地址;​
预期值 A:线程认为当前变量应该有的值;​
新值 B:线程希望将变量修改为的值;​
操作流程:​
线程读取内存地址 V 中的当前值,与预期值 A 比较;​
若相等(说明变量未被其他线程修改),则将内存地址 V 中的值更新为 B;​
若不相等(说明变量已被其他线程修改),则不做任何操作;​
无论是否修改成功,都返回内存地址 V 中的当前值(线程可根据返回值判断是否重试)。​
(2)Java 中的 CAS 实现​
Java 中 CAS 操作通过 sun.misc.Unsafe 类提供的 native 方法实现(如 compareAndSwapInt ()、compareAndSwapLong ()),原子类(如 AtomicInteger)的底层就是调用这些方法。​
示例:

(AtomicInteger 的 getAndIncrement () 核心逻辑):​
public final int getAndIncrement () {​
// 参数:当前对象、value 字段的偏移量、预期值(当前值)、新值(当前值 + 1)​
return unsafe.getAndAddInt (this, valueOffset, 1);​
}​
// Unsafe 类的底层实现(native 方法)​
public final int getAndAddInt (Object o, long offset, int delta) {​
int v;​
do {​
v = getIntVolatile (o, offset); // 读取当前值(volatile 保证可见性)​
} while (!compareAndSwapInt (o, offset, v, v + delta)); // CAS 重试​
return v;​
}​

(3)核心优势​
无锁化:无需使用 synchronized 锁,避免了线程阻塞和上下文切换的开销;​
原子性:由硬件指令保证操作的原子性,比锁机制更高效。​
(4)存在的问题​
ABA 问题:线程 1 读取变量值为 A,线程 2 将变量改为 B 后又改回 A,线程 1 再次 CAS 时会误认为变量未修改,导致错误。解决方案:使用 AtomicStampedReference(添加版本号)或 AtomicMarkableReference(添加布尔标记);​
自旋开销:CAS 失败后会重试(如 AtomicInteger 中的 do-while 循环),高并发下重试次数过多会占用 CPU 资源,导致性能下降。解决方案:限制重试次数、使用 LongAdder 分段累加、或改用锁机制;​
只能操作单个变量:CAS 仅支持对单个变量的原子操作,无法实现多个变量的原子性组合(需使用 synchronized 或 AtomicReference 包装多个变量)。​

9.说说 AQS 吧?​

AQS(AbstractQueuedSynchronizer,抽象队列同步器)是 Java 并发编程中 “锁和同步工具的基础框架”,位于 java.util.concurrent.locks 包下,是 ReentrantLock、CountDownLatch、Semaphore 等类的底层实现。​
(1)核心设计思想​
AQS 基于 “模板方法模式” 设计:​
父类(AQS)定义核心流程(如锁的获取、释放、线程排队);​
子类(如 ReentrantLock)通过重写 “tryAcquire ()”“tryRelease ()” 等抽象方法,实现具体的同步逻辑(独占锁 / 共享锁)。​
(2)核心组成部分​
状态变量(state):volatile 修饰的 int 变量,用于存储同步状态(如 ReentrantLock 中,state=0 表示无锁,state>0 表示重入次数;CountDownLatch 中,state 表示剩余计数);​
双向同步队列(CLH 队列):用于存储等待锁的线程,是一个 FIFO 队列,每个节点对应一个等待线程,节点状态包括 “取消(CANCELLED)”“等待信号(SIGNAL)” 等;​
条件变量(ConditionObject):AQS 的内部类,用于实现 “线程等待 / 唤醒”(如 ReentrantLock 的 newCondition () 方法,支持多条件等待)。​
(3)核心流程(以独占锁为例)​
线程调用 lock () 方法,AQS 先调用子类的 tryAcquire () 尝试获取锁;​
若 tryAcquire () 返回 true(获取成功),则当前线程持有锁,执行后续逻辑;​
若 tryAcquire () 返回 false(获取失败),则将当前线程封装为节点,加入 CLH 队列尾部,并通过 LockSupport.park () 阻塞线程;​
持有锁的线程调用 unlock () 方法,AQS 调用子类的 tryRelease () 尝试释放锁;​
若 tryRelease () 返回 true(释放成功),则从 CLH 队列头部唤醒一个线程,被唤醒的线程再次尝试获取锁(循环​)。

10.Java 中 ReentrantLock 的实现原理是什么?​

ReentrantLock(可重入锁)是 Java 并发包中基于 AQS(AbstractQueuedSynchronizer)实现的独占锁,支持重入、可中断、公平 / 非公平锁模式,核心实现依赖 AQS 的同步机制,具体原理如下:​
(1)基于 AQS 实现核心逻辑​
ReentrantLock 内部维护了一个继承 AQS 的同步器(Sync),并通过 Sync 的子类(FairSync 公平锁、NonfairSync 非公平锁)实现不同锁策略,核心依赖 AQS 的两个核心组件:​
状态变量 state:用于记录锁的重入次数。初始值为 0(无锁状态),线程首次获取锁时,通过 CAS 将 state 从 0 改为 1;若同一线程再次获取锁(重入),则直接将 state 加 1;释放锁时,state 减 1,直至 state 为 0 时完全释放锁。​
CLH 双向队列:当线程获取锁失败时,会被封装为 AQS 节点加入队列尾部,并通过 LockSupport.park () 阻塞;当持有锁的线程释放锁后,会唤醒队列头部的线程,使其再次尝试获取锁。​
(2)公平锁与非公平锁的实现差异​
非公平锁(默认):线程获取锁时,会先通过 CAS 尝试直接抢占锁(无论队列是否有等待线程),抢占失败后才会加入队列。优点是吞吐量高,缺点是可能导致线程饥饿(部分线程长期无法获取锁)。​
公平锁:线程获取锁时,会先检查 CLH 队列是否有等待线程,若有则直接加入队列,按 FIFO 顺序等待;若队列空,才通过 CAS 尝试获取锁。优点是保证线程获取锁的公平性,缺点是吞吐量较低(频繁切换线程)。​
(3)重入性的实现​
通过 AQS 的 “当前持有锁的线程(exclusiveOwnerThread)” 字段实现:线程获取锁时,先判断 exclusiveOwnerThread 是否为当前线程,若是则直接将 state 加 1(无需 CAS);释放锁时,同样先判断当前线程是否为持有线程,若是则 state 减 1,直至 state 为 0 时,将 exclusiveOwnerThread 置为 null,完成释放。​
(4)可中断特性的实现​
ReentrantLock 提供 lockInterruptibly () 方法,支持线程在等待锁时响应中断:当线程通过该方法获取锁失败并进入队列后,若其他线程调用该线程的 interrupt () 方法,AQS 会检测到线程的中断状态,抛出 InterruptedException,终止线程的等待过程。

11.Java 的 synchronized 是怎么实现的?​

synchronized 是 Java 的内置锁(隐式锁),其实现依赖 JVM 层面的 “对象头” 和 “监视器锁(Monitor)”,不同 JDK 版本(尤其是 JDK 6 后)通过 “锁升级” 机制优化性能,具体实现分为底层结构和锁升级两部分:​
(1)底层核心结构​
对象头(Object Header):每个 Java 对象的内存布局中,对象头占 8 字节(32 位 JVM)或 16 字节(64 位 JVM),其中 “Mark Word” 字段是 synchronized 实现的关键,存储对象的锁状态、持有锁的线程 ID、偏向锁 ID 等信息。例如:​
无锁状态:Mark Word 存储对象哈希码、分代年龄;​
偏向锁状态:存储偏向线程 ID、偏向时间戳;​
轻量级锁状态:存储轻量级锁的指针(指向线程栈中的锁记录);​
重量级锁状态:存储重量级锁的 Monitor 指针。​
监视器锁(Monitor):又称 “管程”,是 JVM 内部的同步机制,每个对象都关联一个 Monitor(通过对象头的 Monitor 指针关联)。Monitor 内部维护了 “Entry Set(等待锁的线程队列)”“Owner(当前持有锁的线程)”“Wait Set(等待条件的线程队列)”,当线程尝试获取 synchronized 锁时,本质是竞争 Monitor 的 Owner 权限:​
若 Monitor 的 Owner 为 null,当前线程直接成为 Owner,锁状态变为 “持有”;​
若 Owner 已被其他线程占用,当前线程进入 Entry Set 阻塞,直至 Owner 释放锁后被唤醒。​
(2)锁升级机制(JDK 6 + 优化)​
为解决传统重量级锁性能低下的问题,JVM 引入 “锁升级” 策略,按 “无锁→偏向锁→轻量级锁→重量级锁” 的顺序动态升级,避免频繁使用重量级锁:​
偏向锁:适用于 “单线程重复获取锁” 的场景。线程首次获取锁时,通过 CAS 将 Mark Word 的 “偏向线程 ID” 设为当前线程 ID,后续该线程再次获取锁时,无需 CAS,直接判断偏向线程 ID 即可(无锁竞争时性能最优)。​
轻量级锁:当有其他线程尝试获取偏向锁时,偏向锁升级为轻量级锁。此时线程会在自己的栈帧中创建 “锁记录(Lock Record)”,并通过 CAS 将对象头的 Mark Word 替换为锁记录的指针:​
若 CAS 成功,线程获取轻量级锁;​
若 CAS 失败(存在锁竞争),则自旋尝试 CAS(避免立即升级为重量级锁),自旋一定次数后仍失败,则升级为重量级锁。​
重量级锁:当锁竞争剧烈(自旋失败或线程数超过阈值)时,轻量级锁升级为重量级锁,此时线程通过 Monitor 的 Entry Set 阻塞,由操作系统负责线程的调度(上下文切换开销较大,但稳定性高)。​

12.Synchronized 修饰静态方法和修饰普通方法有什么区别?​

synchronized 修饰静态方法与普通方法的核心区别在于 “锁的对象不同”,进而导致锁的作用范围和竞争场景不同,具体差异如下:​
锁的对象不同:​
修饰普通方法时,锁的对象是当前实例对象(this);​
修饰静态方法时,锁的对象是方法所在类的 Class 对象(如 Xxx.class)。​
锁的作用范围不同:​
普通方法的锁仅对当前实例对象的该方法有同步作用,不同实例对象的该方法互不影响;​
静态方法的锁对整个类的所有实例对象的该静态方法都有同步作用,所有实例共享同一把锁。​
竞争场景不同:​
普通方法的锁:不同实例对象的线程之间不竞争锁。例如创建 A 类的 2 个实例 a1、a2,线程 1 调用 a1.method () 时,线程 2 可同时调用 a2.method (),二者无锁竞争;​
静态方法的锁:所有实例对象的线程之间共享同一把锁,会产生竞争。例如 A 类有静态方法 staticMethod (),线程 1 调用 a1.staticMethod () 时,线程 2 调用 a2.staticMethod () 会被阻塞,需等待线程 1 释放锁。​
底层关联的对象头不同:​
普通方法的锁状态存储在实例对象的 Mark Word 中;​
静态方法的锁状态存储在 Class 对象的 Mark Word 中。

13.Java 中的 synchronized 轻量级锁是否会进行自旋?​

会。synchronized 的轻量级锁阶段会通过 “自旋” 机制减少线程阻塞的开销,这是 JDK 6 后锁优化的重要部分,具体逻辑如下:​
(1)自旋的目的​
轻量级锁适用于 “锁竞争不剧烈、线程持有锁时间短” 的场景。当线程尝试获取轻量级锁时,若 CAS 操作失败(说明有其他线程正在持有锁),此时不立即升级为重量级锁(避免操作系统上下文切换的高开销),而是让线程 “自旋”(循环执行 CAS 操作),等待持有锁的线程快速释放锁。​
(2)自旋的实现细节​
自旋次数限制:JVM 默认自旋次数为 10 次(可通过 - XX:PreBlockSpin 参数调整),若自旋 10 次后仍未获取到锁,则轻量级锁升级为重量级锁,当前线程进入 Monitor 的 Entry Set 阻塞。​
与自适应自旋的区别:​
轻量级锁的自旋是 “固定次数自旋”,无论历史竞争情况如何,都按固定次数循环尝试 CAS;​
JDK 6 后引入的 “自适应自旋” 是更智能的优化(适用于轻量级锁向重量级锁过渡的场景),自旋次数会根据 “历史竞争情况” 动态调整:若当前线程之前通过自旋成功获取过该锁,说明持有锁的线程释放锁快,JVM 会增加自旋次数;若多次自旋失败,说明持有锁的线程释放锁慢或竞争剧烈,JVM 会减少自旋次数,甚至直接跳过自旋升级为重量级锁。

14.Synchronized 能不能禁止指令重排序?​

能。synchronized 不仅能保证线程的原子性和可见性,还能通过 “内存屏障(Memory Barrier)” 禁止指令重排序,确保同步块内的代码按顺序执行。​
(1)指令重排序的背景​
CPU 和编译器为提升性能,会对代码指令进行 “重排序”(前提是不影响单线程的执行结果),但在多线程场景下,重排序可能导致线程安全问题。例如 “双重检查单例模式” 中,未加 volatile 的 instance 变量可能因重排序出现 “半初始化状态”,导致其他线程获取到未完全初始化的对象。​
(2)synchronized 禁止重排序的实现​
JVM 在处理 synchronized 时,会在同步块的 “进入点” 和 “退出点” 插入内存屏障,限制指令重排序的范围:​
进入同步块(获取锁时):插入 “LoadLoad 屏障” 和 “LoadStore 屏障”,禁止同步块内的指令与同步块外的读指令重排序,确保同步块内读取的数据是最新的;​
退出同步块(释放锁时):插入 “StoreStore 屏障” 和 “StoreLoad 屏障”,禁止同步块内的写指令与同步块外的写指令重排序,同时确保同步块内的写操作结果能立即刷新到主内存,让其他线程可见(兼顾可见性保障)。​
(3)与 volatile 的区别​
volatile 仅通过内存屏障禁止 “被修饰变量的读写指令” 与其他指令重排序,作用范围局限于单个变量;而 synchronized 通过内存屏障禁止 “整个同步块内的所有指令” 与块外指令重排序,作用范围是整个代码块,禁止重排序的力度更强。

15.当 Java 的 synchronized 升级到重量级锁后,所有线程都释放锁了,此时它还是重量级锁吗?​

是的。synchronized 的锁升级是 “单向不可逆” 的,一旦从轻量级锁升级为重量级锁,即使所有线程都释放锁(锁处于空闲状态),锁状态也不会回退为轻量级锁或偏向锁,仍保持为重量级锁。​
(1)单向升级的原因​
JVM 设计锁升级为单向的核心目的是 “减少锁状态切换的开销”:​
锁升级的触发条件是 “锁竞争加剧”(如轻量级锁自旋失败、多线程竞争偏向锁),这说明该锁所在的代码块可能长期存在高竞争,后续再次出现竞争的概率较高;​
若允许锁状态回退(如重量级锁空闲后回退为轻量级锁),则后续再次出现竞争时,需重新从低级别锁升级为重量级锁,频繁的状态切换会增加 JVM 的处理开销;​
单向升级可确保锁状态稳定,避免反复切换,虽然空闲的重量级锁仍会占用 Monitor 资源,但对性能的影响远小于频繁切换锁状态的开销。​
(2)例外情况:偏向锁的 “撤销”​
需注意:偏向锁的 “撤销” 是特殊情况(并非升级)—— 当偏向锁存在竞争时,JVM 会撤销偏向锁(将 Mark Word 恢复为无锁状态或直接升级为轻量级锁),但这是 “偏向锁→无锁 / 轻量级锁” 的单向变化,而非 “重量级锁→轻量级锁” 的回退,与重量级锁的单向性不冲突。​

16.什么是 Java 中的锁自适应自旋?​

锁自适应自旋是 JDK 6 后引入的锁优化机制,是对 “轻量级锁固定次数自旋” 的升级,核心逻辑是 “根据历史竞争情况动态调整自旋次数”,让自旋更智能,避免固定次数自旋的局限性。​
(1)与固定次数自旋的区别​
固定次数自旋的局限:​
轻量级锁阶段的固定次数自旋(默认 10 次),无论历史竞争情况如何,都按固定次数循环尝试 CAS;​
若持有锁的线程释放锁快(如自旋 2 次就释放),则剩余 8 次自旋属于无效开销,浪费 CPU 资源;​
若持有锁的线程释放锁慢(如自旋 20 次才释放),则 10 次自旋后仍需升级为重量级锁,自旋失去意义,无法避免线程阻塞。​
自适应自旋的动态调整逻辑:​
自旋次数不固定,JVM 会根据 “当前线程获取该锁的历史记录” 动态调整:​
若当前线程之前通过自旋成功获取过该锁,说明持有锁的线程释放锁速度快,JVM 会增加自旋次数(如从 10 次增加到 20 次),提高获取锁的概率;​
若当前线程之前多次自旋失败,说明持有锁的线程释放锁速度慢或锁竞争剧烈,JVM 会减少自旋次数(如从 10 次减少到 5 次),甚至直接跳过自旋,升级为重量级锁,避免无效的 CPU 消耗。​
(2)核心优势​
自适应自旋通过 “学习历史经验” 优化自旋策略,在 “减少线程阻塞” 和 “避免无效自旋开销” 之间找到平衡,尤其适合锁竞争不稳定的场景(如有时竞争少、有时竞争多),进一步提升 synchronized 的性能,减少不必要的资源浪费。​

17.Synchronized 和 ReentrantLock 有什么区别?​

synchronized(内置锁)和 ReentrantLock(显式锁)都是 Java 中实现同步的核心工具,但在使用方式、功能特性、性能优化等方面有显著区别,具体如下:​
锁的类型与管理方式不同:​
synchronized 是隐式锁,由 JVM 自动管理锁的获取与释放(进入同步块 / 方法时自动获取,退出时自动释放,无需手动操作);​
ReentrantLock 是显式锁,需手动调用 lock () 方法获取锁,调用 unlock () 方法释放锁(为避免锁泄漏,通常在 finally 块中调用 unlock ())。​
重入性支持方式不同:​
synchronized 默认支持重入,无需手动处理,JVM 会自动记录线程的重入次数;​
ReentrantLock 也支持重入,通过 AQS 的 state 变量(记录重入次数)和 exclusiveOwnerThread 变量(记录当前持有锁的线程)实现,需确保同一线程多次获取锁后对应释放。​
公平性支持不同:​
synchronized 仅支持非公平锁(JDK 6 后,偏向锁、轻量级锁均为非公平模式,重量级锁默认也为非公平模式),无法配置为公平锁;​
ReentrantLock 同时支持公平锁和非公平锁,通过构造函数(new ReentrantLock (true) 创建公平锁,默认非公平锁)指定,可根据场景选择。​
可中断性不同:​
synchronized 不支持中断,线程在等待锁时会一直阻塞,无法响应 interrupt () 方法,只能等待其他线程释放锁;​
ReentrantLock 支持中断,通过 lockInterruptibly () 方法实现,线程在等待锁时若被中断,会抛出 InterruptedException,可终止等待过程,避免永久阻塞。​
超时获取锁支持不同:​
synchronized 不支持超时获取,线程等待锁时无时间限制,若持有锁的线程长期不释放,等待线程会永久阻塞;​
ReentrantLock 支持超时获取,通过 tryLock (long timeout, TimeUnit unit) 方法实现,若在指定时间内未获取到锁,会返回 false,可避免永久阻塞。​
条件变量支持不同:​
synchronized 仅支持一个条件变量,基于对象的 Monitor 实现,通过 wait ()、notify ()、notifyAll () 方法操作,notifyAll () 会唤醒所有等待线程,无法精准唤醒;​
ReentrantLock 支持多个条件变量,通过 newCondition () 方法创建多个 Condition 对象,每个条件变量对应独立的等待队列,可通过 signal ()/signalAll () 精准唤醒特定条件的等待线程,灵活性更高。​
锁状态查询支持不同:​
synchronized 不支持直接查询锁状态,无法获取当前是否有线程持有锁、等待锁的线程数等信息;​
ReentrantLock 提供 isLocked ()(判断是否有线程持有锁)、hasQueuedThreads ()(判断是否有线程等待锁)等方法,可查询锁的状态,便于调试和监控。​
底层实现不同:​
synchronized 依赖 JVM 层面的对象头、Monitor 机制和锁升级策略实现;​
ReentrantLock 基于 Java 并发包的 AQS(AbstractQueuedSynchronizer)框架实现,通过重写 AQS 的 tryAcquire ()、tryRelease () 等方法自定义同步逻辑。​
适用场景不同:​
synchronized 适合简单同步场景(如普通方法、代码块同步),无需复杂功能,依赖 JVM 自动管理,使用简单;​

18.Volatile 与 Synchronized 的区别是什么?​

volatile 和 synchronized 都是 Java 中解决多线程可见性、有序性问题的核心工具,但二者的作用范围、功能特性和适用场景差异显著,具体区别如下:​
作用对象不同:​
volatile 仅能修饰变量(包括成员变量和静态变量),无法修饰方法或代码块;​
synchronized 可修饰普通方法、静态方法,也可修饰代码块(锁定对象或类),作用范围更广泛。​
原子性保障不同:​
volatile 不保障原子性,仅能保证变量读写操作的可见性和有序性。例如 i++ 这类复合操作(读取 - 修改 - 写入),volatile 无法确保多线程下的原子性,仍可能出现线程安全问题;​
synchronized 保障原子性,同步块或同步方法内的所有操作会被视为一个原子单元,多线程下同一时间仅一个线程能执行,避免复合操作的线程安全问题。​
可见性保障机制不同:​
volatile 通过 “内存屏障” 实现可见性:变量修改后会立即刷新到主内存,其他线程读取时会从主内存加载最新值,禁止变量的读写操作被缓存到工作内存;​
synchronized 通过 “锁释放 - 获取” 机制实现可见性:线程释放锁时,会将同步块内的写操作结果刷新到主内存;其他线程获取锁时,会从主内存加载最新数据,确保读取到的是最新值。​
有序性保障范围不同:​
volatile 仅禁止 “被修饰变量的读写指令” 与其他指令重排序,作用范围局限于单个变量。例如 volatile 修饰的 instance 变量,仅能保证 instance 的赋值与其他指令不重排序,无法影响其他变量的指令顺序;​
synchronized 禁止 “整个同步块内的所有指令” 与块外指令重排序,作用范围是整个同步块。通过在同步块进入和退出时插入内存屏障,确保块内代码按顺序执行,有序性保障力度更强。​
锁特性不同:​
volatile 无锁特性,不会导致线程阻塞,仅通过内存屏障优化指令顺序,属于 “无锁同步”,性能开销极低;​
synchronized 是有锁机制,可能导致线程阻塞:轻量级锁阶段通过自旋避免阻塞,重量级锁阶段线程会进入 Monitor 队列阻塞,存在上下文切换开销,性能开销高于 volatile。​
适用场景不同:​
volatile 适合 “多线程共享变量的简单读写场景”,如状态标记变量(如 boolean isRunning)、单例模式中的实例变量(双重检查锁模式),需确保变量操作是单个读写,无复合逻辑;​
synchronized 适合 “多线程对共享资源的复合操作场景”,如计数器 i++、共享集合的添加 / 删除操作,需确保多个操作的原子性、可见性和有序性。

19.如何优化 Java 中的锁的使用?​

Java 中锁的优化核心目标是 “减少锁竞争、降低阻塞开销、提升并发性能”,常见优化手段如下:​
减少锁持有时间:​
仅在 “需要同步的核心逻辑” 上加锁,避免将无关代码(如 IO 操作、耗时计算)放入同步块,缩短线程持有锁的时间,减少其他线程的等待开销。​
示例:若同步块内包含 “读取配置文件(IO 操作)+ 更新共享变量”,可将读取配置文件的逻辑移到同步块外,仅对 “更新共享变量” 的核心逻辑加锁。​
降低锁粒度:​
将 “大锁” 拆分为 “小锁”,减少线程对同一把锁的竞争。典型案例是 ConcurrentHashMap(JDK 7)的 “分段锁”:将 HashMap 分为多个 Segment,每个 Segment 对应一把锁,线程仅竞争操作的 Segment 锁,而非整个 HashMap 的锁,大幅提升并发性能。​
注意:拆分锁需确保 “拆分后的锁能覆盖同步需求”,避免因锁粒度太小导致线程安全问题(如拆分后未覆盖所有共享资源的操作)。​
使用无锁同步机制替代锁:​
对于简单的变量原子操作(如计数器、状态标记),使用原子类(如 AtomicInteger、LongAdder)或 CAS 操作替代 synchronized/ReentrantLock,避免锁竞争带来的阻塞开销。​
例如:高并发计数器场景,LongAdder 通过分段累加减少 CAS 竞争,性能优于使用 synchronized 修饰的计数器。​
使用读写分离锁(ReentrantReadWriteLock):​
若场景中 “读操作远多于写操作”,使用读写锁替代独占锁(如 synchronized)。读写锁的核心特性是 “读 - 读不互斥、读 - 写互斥、写 - 写互斥”,多个线程可同时读取共享资源,仅在写操作时独占锁,提升读操作的并发性能。​
适用场景:缓存查询、数据统计等读多写少的场景,如商品详情页的库存查询(读)与库存扣减(写)。​
避免锁重入与锁嵌套:​
尽量避免锁的嵌套使用(如同步块内调用另一个同步方法),减少锁重入次数。锁重入会增加线程持有锁的时间,且可能导致死锁(如线程 A 持有锁 1 等待锁 2,线程 B 持有锁 2 等待锁 1)。​
若必须嵌套,需严格控制嵌套层级,确保锁获取顺序一致,避免死锁风险。​
使用乐观锁替代悲观锁:​
悲观锁(如 synchronized、ReentrantLock)默认 “假设会有锁竞争”,通过阻塞线程确保同步;乐观锁(如 CAS、版本号机制)默认 “假设无锁竞争”,仅在提交修改时验证数据是否被修改,无阻塞开销。​
适用场景:低锁竞争场景(如用户注册时的用户名唯一性校验),乐观锁性能优于悲观锁;高竞争场景下,乐观锁的重试开销可能高于悲观锁,需根据实际竞争情况选择。​
利用锁的优化机制(JVM 层面):​
依赖 JDK 6 后的锁升级机制(偏向锁→轻量级锁→重量级锁),避免直接使用重量级锁。例如单线程场景下,synchronized 会自动使用偏向锁,无需自旋或阻塞,性能接近无锁;​
避免禁用 JVM 的锁优化参数(如 -XX:-UseBiasedLocking 禁用偏向锁),确保 JVM 能自动优化锁状态。​
使用线程池减少线程创建开销:​
频繁创建线程会增加上下文切换开销,间接加剧锁竞争。通过线程池(如 ThreadPoolExecutor)复用线程,减少线程创建与销毁的开销,同时控制并发线程数,避免过多线程竞争同一把锁。

20.你了解 Java 中的读写锁吗?​

Java 中的读写锁是 java.util.concurrent.locks.ReentrantReadWriteLock,是一种 “读写分离” 的同步锁,基于 AQS 实现,核心特性是 “区分读操作和写操作的锁竞争策略”,解决了 “读多写少” 场景下独占锁(如 synchronized)的性能瓶颈。​
(1)核心特性​
读写分离:提供两把锁 —— 读锁(共享锁)和写锁(独占锁),分别对应读操作和写操作:​
读锁(SharedLock):多个线程可同时获取,支持 “读 - 读共存”,同一时间允许多个线程读取共享资源;​
写锁(ExclusiveLock):仅允许一个线程获取,禁止 “读 - 写共存” 和 “写 - 写共存”,确保写操作的原子性。​
可重入性:读锁和写锁均支持重入:​
读锁重入:持有读锁的线程可再次获取读锁(需确保未持有写锁);​
写锁重入:持有写锁的线程可再次获取写锁,也可获取读锁(写锁可降级为读锁),但持有读锁的线程无法直接获取写锁(避免写操作被插队)。​
锁降级:支持 “写锁→读锁” 的降级,不支持 “读锁→写锁” 的升级:​
锁降级:持有写锁的线程,可先获取读锁,再释放写锁,最终持有读锁,确保后续读操作能读取到自己修改的数据,避免其他线程在写锁释放后立即修改数据;​
禁止升级:持有读锁的线程若尝试获取写锁,会导致线程阻塞(需等待所有读锁释放),避免 “读 - 写竞争” 导致的数据不一致,同时防止死锁(如多个读线程同时尝试升级为写锁)。​
公平性选项:支持公平锁和非公平锁(通过构造函数 ReentrantReadWriteLock(boolean fair) 指定):​
公平锁:按 “线程请求锁的顺序” 分配读锁和写锁,避免写线程饥饿(长期无法获取写锁);​
非公平锁(默认):允许 “读锁插队”(已持有读锁的线程可再次获取读锁,无需排队),吞吐量更高,但可能导致写线程饥饿。​
(2)核心方法​
获取与释放锁:​
读锁操作:通过 readLock().lock() 获取读锁,readLock().unlock() 释放读锁;​
写锁操作:通过 writeLock().lock() 获取写锁,writeLock().unlock() 释放写锁;​
注意:释放锁需与获取锁一一对应(重入时需多次释放),避免锁泄漏。​
锁状态查询:​
getReadLockCount():获取当前持有读锁的线程数(包括重入次数);​
isWriteLocked():判断写锁是否被持有;​
getWriteHoldCount():获取当前持有写锁的线程的重入次数。​
条件变量:读锁不支持条件变量(readLock().newCondition() 会抛出 UnsupportedOperationException),写锁支持条件变量(与 ReentrantLock 的条件变量用法一致),用于实现写线程的等待/唤醒逻辑。​
(3)适用场景​
读写锁适用于 “读操作远多于写操作” 的场景,典型案例包括:​
缓存系统:如 Redis 客户端的本地缓存,读操作(查询缓存)频繁,写操作(更新缓存)稀少,读写锁可提升读操作的并发性能;​
数据统计:如系统的日活用户统计,读操作(查询统计结果)远多于写操作(更新统计数据),读写锁避免读操作阻塞写操作;​
配置中心:如分布式配置中心的配置读取,多个服务实例同时读取配置(读操作),配置更新仅偶尔发生(写操作),读写锁确保配置读取的并发性和更新的原子性。​
(4)注意事项​
写线程饥饿问题:非公平模式下,若读操作频繁,新的读线程会不断获取读锁,导致写线程长期无法获取写锁(饥饿)。解决方案:使用公平模式,或在写操作前通过 tryLock() 尝试获取写锁,超时则降级处理;​
锁重入的限制:持有读锁的线程无法直接获取写锁,需先释放所有读锁,再尝试获取写锁,避免因 “读锁未释放” 导致死锁;​
性能权衡:读写锁的实现复杂度高于独占锁,若写操作频率高(如写操作占比超过 30%),读写锁的性能可能低于独占锁(因锁切换开销),此时建议使用 ReentrantLock 或 synchronized;​
数据一致性保障:读锁仅确保 “读取时无写操作”,但无法确保 “读取到的数据是最新的”(如其他线程已修改数据但未释放写锁),需结合业务逻辑确保数据一致性(如定期刷新缓存)。​
(5)与 synchronized 的对比​
性能:读多写少场景下,读写锁的读操作支持并发,性能远高于 synchronized(synchronized 无论读写均为独占锁);写操作频繁场景下,读写锁的锁切换开销可能高于 synchronized;​
功能:读写锁支持锁降级、读锁计数查询等功能,synchronized 无此特性;​
灵活性:读写锁可手动控制读锁和写锁的获取与释放,synchronized 由 JVM 自动管理锁,灵活性低于读写锁。​

0 条评论

发布
问题