StampedLock 是 Java 8 新增的并发锁工具类,位于 java.util.concurrent.locks 包下,是对读写锁(ReentrantReadWriteLock)的优化升级,旨在解决读写锁 “读 - 读不互斥、读 - 写互斥” 场景下的性能瓶颈。
其核心特性如下:
基于 “戳记(Stamp)” 实现锁控制:每次获取锁时会返回一个 long 类型的 “戳记”,释放锁或转换锁类型时需传入该戳记,确保操作的正确性(戳记无效则操作失败)。
支持三种锁模式:
写锁(Write Lock):排他锁,同一时间仅允许一个线程获取,获取后禁止其他线程获取读锁或写锁;
悲观读锁(Read Lock):共享锁,多个线程可同时获取,获取后禁止线程获取写锁;
乐观读锁(Optimistic Read):非阻塞模式,获取时不阻塞线程,仅通过戳记验证数据是否被修改(若未修改则直接使用数据,若已修改则升级为悲观读锁或重试)。
非重入性:StampedLock 不支持重入(同一线程多次获取同一锁会导致死锁),而 ReentrantReadWriteLock 是可重入的。
适用场景:读操作远多于写操作、且读操作无需重入的场景(如缓存查询、数据统计),能显著提升并发读性能。
CompletableFuture 是 Java 8 新增的异步编程工具类,位于 java.util.concurrent 包下,实现了 Future 和 CompletionStage 接口,解决了传统 Future 无法链式调用、无法优雅处理异常、无法组合多个异步任务的痛点。
其核心特性如下:
支持链式异步任务:通过 thenApply ()、thenAccept ()、thenRun () 等方法,可将多个异步任务按顺序串联,避免 “回调地狱”;
支持多任务组合:提供 thenCombine ()(两个任务完成后合并结果)、allOf ()(所有任务完成后触发)、anyOf ()(任一任务完成后触发)等方法,轻松处理多任务协作;
内置异常处理:通过 exceptionally ()、handle () 方法可优雅捕获异步任务中的异常,无需手动 try-catch;
支持手动完成任务:通过 complete ()、completeExceptionally () 方法,可手动设置任务结果或异常,灵活控制任务状态;
适用场景:异步调用(如 HTTP 请求、数据库查询)、多任务并行处理(如批量数据处理)、异步结果链式依赖(如 “查询用户→根据用户 ID 查订单→根据订单 ID 查物流”)。
ForkJoinPool 是 Java 7 新增的线程池实现,位于 java.util.concurrent 包下,基于 “分治思想(Divide and Conquer)” 设计,专门用于处理可拆分的计算密集型任务(即大任务拆分为多个小任务,并行计算后合并结果)。
其核心特性如下:
工作窃取(Work-Stealing)算法:每个线程维护一个独立的任务队列,当线程完成自身队列任务后,会 “窃取” 其他线程队列中的任务执行,减少线程空闲时间,提升 CPU 利用率;
任务拆分与合并:需配合 ForkJoinTask 子类(RecursiveTask 有返回结果、RecursiveAction 无返回结果)使用,通过 fork () 方法拆分任务、join () 方法合并结果;
默认并行度:若未指定并行度,默认等于当前 CPU 核心数(Runtime.getRuntime ().availableProcessors ()),避免线程过多导致上下文切换开销;
适用场景:大规模数组排序(如 Arrays.parallelSort () 底层依赖 ForkJoinPool)、大文件分片处理、复杂计算(如矩阵运算、数据统计)等计算密集型任务。
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 () 等方法,让异步任务按顺序执行(本质是线程池调度的顺序控制)。
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 接口(指定延迟时间);
核心特性:仅当元素延迟时间到期后,才能被获取;
适用场景:定时任务调度(如定时清理过期数据)。
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 类似。
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 的对比
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 包装多个变量)。
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 队列头部唤醒一个线程,被唤醒的线程再次尝试获取锁(循环)。
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,终止线程的等待过程。
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 阻塞,由操作系统负责线程的调度(上下文切换开销较大,但稳定性高)。
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 中。
会。synchronized 的轻量级锁阶段会通过 “自旋” 机制减少线程阻塞的开销,这是 JDK 6 后锁优化的重要部分,具体逻辑如下:
(1)自旋的目的
轻量级锁适用于 “锁竞争不剧烈、线程持有锁时间短” 的场景。当线程尝试获取轻量级锁时,若 CAS 操作失败(说明有其他线程正在持有锁),此时不立即升级为重量级锁(避免操作系统上下文切换的高开销),而是让线程 “自旋”(循环执行 CAS 操作),等待持有锁的线程快速释放锁。
(2)自旋的实现细节
自旋次数限制:JVM 默认自旋次数为 10 次(可通过 - XX:PreBlockSpin 参数调整),若自旋 10 次后仍未获取到锁,则轻量级锁升级为重量级锁,当前线程进入 Monitor 的 Entry Set 阻塞。
与自适应自旋的区别:
轻量级锁的自旋是 “固定次数自旋”,无论历史竞争情况如何,都按固定次数循环尝试 CAS;
JDK 6 后引入的 “自适应自旋” 是更智能的优化(适用于轻量级锁向重量级锁过渡的场景),自旋次数会根据 “历史竞争情况” 动态调整:若当前线程之前通过自旋成功获取过该锁,说明持有锁的线程释放锁快,JVM 会增加自旋次数;若多次自旋失败,说明持有锁的线程释放锁慢或竞争剧烈,JVM 会减少自旋次数,甚至直接跳过自旋升级为重量级锁。
能。synchronized 不仅能保证线程的原子性和可见性,还能通过 “内存屏障(Memory Barrier)” 禁止指令重排序,确保同步块内的代码按顺序执行。
(1)指令重排序的背景
CPU 和编译器为提升性能,会对代码指令进行 “重排序”(前提是不影响单线程的执行结果),但在多线程场景下,重排序可能导致线程安全问题。例如 “双重检查单例模式” 中,未加 volatile 的 instance 变量可能因重排序出现 “半初始化状态”,导致其他线程获取到未完全初始化的对象。
(2)synchronized 禁止重排序的实现
JVM 在处理 synchronized 时,会在同步块的 “进入点” 和 “退出点” 插入内存屏障,限制指令重排序的范围:
进入同步块(获取锁时):插入 “LoadLoad 屏障” 和 “LoadStore 屏障”,禁止同步块内的指令与同步块外的读指令重排序,确保同步块内读取的数据是最新的;
退出同步块(释放锁时):插入 “StoreStore 屏障” 和 “StoreLoad 屏障”,禁止同步块内的写指令与同步块外的写指令重排序,同时确保同步块内的写操作结果能立即刷新到主内存,让其他线程可见(兼顾可见性保障)。
(3)与 volatile 的区别
volatile 仅通过内存屏障禁止 “被修饰变量的读写指令” 与其他指令重排序,作用范围局限于单个变量;而 synchronized 通过内存屏障禁止 “整个同步块内的所有指令” 与块外指令重排序,作用范围是整个代码块,禁止重排序的力度更强。
是的。synchronized 的锁升级是 “单向不可逆” 的,一旦从轻量级锁升级为重量级锁,即使所有线程都释放锁(锁处于空闲状态),锁状态也不会回退为轻量级锁或偏向锁,仍保持为重量级锁。
(1)单向升级的原因
JVM 设计锁升级为单向的核心目的是 “减少锁状态切换的开销”:
锁升级的触发条件是 “锁竞争加剧”(如轻量级锁自旋失败、多线程竞争偏向锁),这说明该锁所在的代码块可能长期存在高竞争,后续再次出现竞争的概率较高;
若允许锁状态回退(如重量级锁空闲后回退为轻量级锁),则后续再次出现竞争时,需重新从低级别锁升级为重量级锁,频繁的状态切换会增加 JVM 的处理开销;
单向升级可确保锁状态稳定,避免反复切换,虽然空闲的重量级锁仍会占用 Monitor 资源,但对性能的影响远小于频繁切换锁状态的开销。
(2)例外情况:偏向锁的 “撤销”
需注意:偏向锁的 “撤销” 是特殊情况(并非升级)—— 当偏向锁存在竞争时,JVM 会撤销偏向锁(将 Mark Word 恢复为无锁状态或直接升级为轻量级锁),但这是 “偏向锁→无锁 / 轻量级锁” 的单向变化,而非 “重量级锁→轻量级锁” 的回退,与重量级锁的单向性不冲突。
锁自适应自旋是 JDK 6 后引入的锁优化机制,是对 “轻量级锁固定次数自旋” 的升级,核心逻辑是 “根据历史竞争情况动态调整自旋次数”,让自旋更智能,避免固定次数自旋的局限性。
(1)与固定次数自旋的区别
固定次数自旋的局限:
轻量级锁阶段的固定次数自旋(默认 10 次),无论历史竞争情况如何,都按固定次数循环尝试 CAS;
若持有锁的线程释放锁快(如自旋 2 次就释放),则剩余 8 次自旋属于无效开销,浪费 CPU 资源;
若持有锁的线程释放锁慢(如自旋 20 次才释放),则 10 次自旋后仍需升级为重量级锁,自旋失去意义,无法避免线程阻塞。
自适应自旋的动态调整逻辑:
自旋次数不固定,JVM 会根据 “当前线程获取该锁的历史记录” 动态调整:
若当前线程之前通过自旋成功获取过该锁,说明持有锁的线程释放锁速度快,JVM 会增加自旋次数(如从 10 次增加到 20 次),提高获取锁的概率;
若当前线程之前多次自旋失败,说明持有锁的线程释放锁速度慢或锁竞争剧烈,JVM 会减少自旋次数(如从 10 次减少到 5 次),甚至直接跳过自旋,升级为重量级锁,避免无效的 CPU 消耗。
(2)核心优势
自适应自旋通过 “学习历史经验” 优化自旋策略,在 “减少线程阻塞” 和 “避免无效自旋开销” 之间找到平衡,尤其适合锁竞争不稳定的场景(如有时竞争少、有时竞争多),进一步提升 synchronized 的性能,减少不必要的资源浪费。
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 自动管理,使用简单;
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++、共享集合的添加 / 删除操作,需确保多个操作的原子性、可见性和有序性。
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)复用线程,减少线程创建与销毁的开销,同时控制并发线程数,避免过多线程竞争同一把锁。
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 自动管理锁,灵活性低于读写锁。