目录
2.1.6 ReentrantLock和synchronized的对比
2.2 ReentrantReadWriteLock(读写锁)
2.3 StampedLock(乐观读锁,不可重入,非公平)
一、锁
1、Synchronized关键字(隐式锁)
synchronized
用于实现线程同步,确保多个线程对共享资源的互斥访问
1.1 用法
// 1. 修饰实例方法(锁对象是当前实例this)
public synchronized void method() { ... }
// 2. 修饰静态方法(锁对象是当前类的Class对象)
public static synchronized void staticMethod() { ... }
// 3. 修饰代码块(需显式指定锁对象,通常是 private final 对象,避免暴露)
public void block() {
synchronized (lockObj) { ... }
}
1.2 锁的特性与优化
-
可重入性:同一线程可重复获取同一把锁(避免死锁)。
不可重入的场景,若线程已持有锁,再次尝试获取该锁时会被阻塞,导致线程“自己等待自己”,形成死锁
-
非公平锁:默认抢占式获取锁,不保证等待线程的获取顺序。wait释放锁,notify唤醒线程后重新竞争锁
-
自动释放:无论正常执行完毕还是抛出异常,锁都会被释放。
-
锁消除:JIT编译器通过逃逸分析,若发现锁对象不会逃逸出当前线程,则直接消除锁。
-
锁粗化:将相邻的多个细粒度锁合并为一个粗粒度锁,减少锁开销
-
自旋锁与适应性自旋:线程在获取锁失败后,短暂自旋(循环尝试)而非立即阻塞;自适应自旋根据历史成功率动态调整自旋次数
1.3 原理:基于 Monitor 机制,锁升级
synchronized
的底层实现通过 对象头标记、锁升级机制、Monitor 管程 三者结合,平衡了性能与线程安全。
字节码指令层面:
-
monitorenter
:进入同步代码块,尝试获取锁。 -
monitorexit
:退出同步代码块,释放锁。JVM 确保在代码异常退出时也会执行monitorexit。
在每个Java对象的对象头中,有Mark Word字段存储了锁的相关信息:
锁标志位(Lock Flag)
指向 Monitor 的指针(重量级锁时),Monitor核心字段:
_owner:当前持有锁的线程。
_EntryList:竞争锁的线程队列(未获取锁的线程阻塞在此)。
_WaitSet:调用 wait() 后进入等待状态的线程队列。notify唤醒线程后,需重新竞争锁
_recursions:锁的重入次数(支持可重入性)。
线程 ID(偏向锁时)
锁记录指针(轻量级锁时)
锁升级过程(锁膨胀):无锁 → 偏向锁 → 轻量级锁 → 重量级锁
1> 偏向锁:消除无竞争情况下的同步开销(如单线程环境)
实现:
-
线程首次进入同步代码时,通过 CAS 将 Mark Word 中的线程 ID 设为当前线程 ID。
-
后续进入同步代码时,直接检查线程 ID 是否匹配,无需 CAS。
触发条件:未发生多线程竞争
升级:当其他线程尝试获取锁时,偏向锁撤销并升级为轻量级锁
2> 轻量级锁:减少多线程轻微竞争时的开销(如交替执行)
实现:
-
线程在栈帧中创建 锁记录(Lock Record),拷贝对象头的 Mark Word。
-
通过 CAS 将对象头的 Mark Word 替换为指向锁记录的指针。
自旋期间若有新线程加入,新线程同样可以参与 CAS 竞争,导致 “后来者先得”(非公平)
-
成功则获取锁;失败则自旋重试(自适应自旋)。
触发条件:多线程竞争但未达到自旋阈值。
升级:自旋失败后升级为重量级锁。
3> 重量级锁:处理高竞争场景
重量级锁 依赖操作系统的 互斥量(Mutex Lock),涉及用户态到内核态的切换,性能开销较大
实现:
-
对象头中的 Mark Word 指向 Monitor 的指针。
-
未获取锁的线程进入
_EntryList
阻塞,依赖操作系统进行线程调度。
触发条件:自旋超过阈值或等待线程数过多
1.4 使用注意事项
1> 避免死锁
预防方法:按固定顺序获取锁;使用 tryLock
超时机制(需配合 Lock
接口);减少锁的持有时间
2> 不要锁不可控对象
错误示例:锁字符串常量、基础类型包装类(如 Integer
)
3> 锁的范围最小化
4> 避免锁静态方法:静态方法的锁是 Class
对象,可能导致全局性能瓶颈
2、显式锁
2.1 ReentrantLock
(可重入锁)
2.1.1 用法
2.1.1.1 公平锁与非公平锁
基本用法
import java.util.concurrent.locks.ReentrantLock;
public class ReentrantLockDemo {
private final ReentrantLock lock = new ReentrantLock();
public void performTask() {
lock.lock(); // 获取锁
try {
// 临界区代码(线程安全操作)
System.out.println(Thread.currentThread().getName() + " 持有锁");
} finally {
lock.unlock(); // 必须确保释放锁
System.out.println(Thread.currentThread().getName() + " 释放锁");
}
}
}
ReentrantLock fairLock = new ReentrantLock(true); // 公平锁
ReentrantLock unfairLock = new ReentrantLock(); // 非公平锁(默认)
-
公平锁:按线程请求锁的顺序分配锁(先到先得,先调用
lock()
的线程先获得锁),减少饥饿现象,但吞吐量较低。
原理:内部使用 AbstractQueuedSynchronizer(AQS)
的队列机制,每次锁释放时,队列中的第一个等待线程会被唤醒。线程必须检查队列中是否有其他等待线程,若队列非空,则自身加入队列尾部排队。
-
非公平锁:允许插队(新请求的线程可以直接尝试获取锁,无需立即进入等待队列),吞吐量高,但可能导致线程饥饿。
原理:锁释放时,新线程可以直接通过 CAS 尝试获取锁,失败后再进入队列等待
2.1.1.2 可中断与超时机制
可中断的锁获取:线程在等待锁的过程中可以响应中断,避免无限阻塞。
使用场景:需要支持任务取消或超时中断的线程协作。
public void performInterruptibleTask() throws InterruptedException {
lock.lockInterruptibly(); // 可中断的锁获取
try {
// 临界区代码
} finally {
lock.unlock();
}
}
超时尝试获取锁:尝试在指定时间内获取锁,失败后执行备选逻辑。
适用场景:避免死锁或高并发下的资源争用
public void tryLockWithTimeout() {
try {
if (lock.tryLock(1, TimeUnit.SECONDS)) { // 尝试1秒内获取锁
try {
// 临界区代码
} finally {
lock.unlock();
}
} else {
System.out.println("获取锁超时,执行其他操作");
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt(); // 恢复中断状态
}
}
2.1.1.3 多条件变量(Condition)
ReentrantLock
可以创建多个 Condition
对象,实现更精细的线程等待与唤醒机制(替代wait()
/notify()
的更灵活方式)
优势:通过多个 Condition
分离等待条件,避免无效唤醒
案例:生产者消费者模型
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
public class BoundedBuffer {
private final ReentrantLock lock = new ReentrantLock();
private final Condition notFull = lock.newCondition(); // 非满条件
private final Condition notEmpty = lock.newCondition(); // 非空条件
private final Object[] items = new Object[100];
private int putPtr, takePtr, count;
public void put(Object x) throws InterruptedException {
lock.lock();
try {
while (count == items.length) {
notFull.await(); // 等待队列非满
}
items[putPtr] = x;
if (++putPtr == items.length) putPtr = 0;
count++;
notEmpty.signal(); // 唤醒等待非空的消费者
} finally {
lock.unlock();
}
}
public Object take() throws InterruptedException {
lock.lock();
try {
while (count == 0) {
notEmpty.await(); // 等待队列非空
}
Object x = items[takePtr];
if (++takePtr == items.length) takePtr = 0;
count--;
notFull.signal(); // 唤醒等待非满的生产者
return x;
} finally {
lock.unlock();
}
}
}
2.1.1.4 分段锁
分段锁(Striped Lock)是一种通过将数据分片(Segment)并为每个分片分配独立锁来减少锁竞争的技术,常用于优化高并发场景下的数据结构(如 ConcurrentHashMap
)。
适用场景:
-
读多写少的高并发数据访问(如缓存、计数器)。
-
需要分区管理的共享资源(如哈希表、分布式存储分片)。
固定分段锁:先创建固定数量的锁,根据键的哈希值映射到对应锁
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class StripedLock<K> {
private final Lock[] locks;
private static final int DEFAULT_STRIPES = 16; // 默认分段数
public StripedLock(int stripes) {
locks = new ReentrantLock[stripes];
for (int i = 0; i < stripes; i++) {
locks[i] = new ReentrantLock();
}
}
public StripedLock() {
this(DEFAULT_STRIPES);
}
// 根据键的哈希值获取对应的锁
public Lock getLock(K key) {
int hash = key.hashCode();
int index = (hash & Integer.MAX_VALUE) % locks.length; // 避免负数
return locks[index];
}
// 执行受锁保护的操作
public void doWithLock(K key, Runnable task) {
Lock lock = getLock(key);
lock.lock();
try {
task.run();
} finally {
lock.unlock();
}
}
}
动态分段锁:根据数据动态调整锁的分段数(如按需创建锁)。实现思路,使用 ConcurrentHashMap
存储键到锁的映射,通过 computeIfAbsent
方法动态创建锁
应用案例:高性能计数器
public class StripedCounter {
private final StripedLock<String> stripedLock = new StripedLock<>();
private final Map<String, Integer> counters = new HashMap<>();
public void increment(String key) {
stripedLock.doWithLock(key, () -> {
int count = counters.getOrDefault(key, 0);
counters.put(key, count + 1);
});
}
public int getCount(String key) {
stripedLock.doWithLock(key, () -> {
// 若需要读取一致性,此处也需加锁
});
return counters.getOrDefault(key, 0);
}
}
分段数的选择:通常设置为并发线程数的 2~4 倍(如 16、32、64)。分段数越多,锁竞争越少,但是内存开销越大。分段数应足够大,确保键的哈希分布均匀。
避免死锁:若一个操作涉及多个分段,可能因锁顺序不一致导致死锁
解决方案:
-
固定锁顺序:按分段编号从小到大加锁。
-
超时机制:使用
tryLock
尝试获取锁。
2.1.1.5 锁状态查询
ReentrantLock
提供方法查询锁的状态,用于调试或监控
public void lockStatusCheck() {
System.out.println("锁是否被持有: " + lock.isLocked());
System.out.println("当前线程是否持有锁: " + lock.isHeldByCurrentThread());
System.out.println("等待锁的线程数: " + lock.getQueueLength());
}
2.1.2 锁的特性
-
显式加锁(需手动
lock()
和unlock()
)。 -
支持公平锁(通过构造函数设置
fair=true
)。 -
可中断等待(
lockInterruptibly()
)。 -
超时获取锁:支持尝试获取锁并设置超时时间(
tryLock(long timeout, TimeUnit unit)
)。 -
可重入性:同一线程可多次获取同一把锁(通过计数器实现),避免死锁
-
条件变量:可通过
newCondition()
创建多个条件变量,实现更精细的线程等待/唤醒机制
2.1.3 原理:AQS
AbstractQueuedSynchronizer
AQS的核心结构:
-
同步状态(
state
):state=0
表示锁未被占用,state>0
表示锁被占用且记录重入次数。 -
等待队列(CLH队列):双向链表实现的 FIFO 队列,存储等待获取锁的线程。
-
独占模式:
ReentrantLock
使用独占模式,同一时刻只有一个线程能持有锁。
核心:通过CLH队列管理等待线程,CAS操作更新状态
加锁流程(以非公平锁为例):
1> 尝试直接获取锁:通过 CAS 操作尝试将 state
从 0 改为 1,成功则设置当前线程为独占线程
2> 获取失败则入队:将线程包装为 Node
加入等待队列,通过自旋或阻塞(LockSupport.park()
)等待唤醒
3> 锁释放与唤醒:释放锁时,将 state
减 1,若 state=0
则唤醒队列中的下一个线程
公平锁:直接检查等待队列是否有前驱节点,若有则排队
-
优点:避免线程饥饿;缺点:吞吐量较低。
非公平锁:直接尝试插队获取锁,失败后再排队
-
优点:减少线程切换,提高吞吐量;缺点:可能导致饥饿。
2.1.4 使用注意事项
1> 必须手动释放锁:unlock()
必须放在 finally
块中,避免异常导致锁无法释放
2> 谨慎适用公平锁:公平锁会显著降低吞吐量,仅在严格要求顺序时使用
3> 条件变量与锁绑定:使用 Condition
前必须持有对应的锁
2.1.5 适用场景
1> 需要可中断或超时控制的锁(如防止死锁)。
2> 需要公平锁机制(如任务调度按顺序执行)。
3> 需要多个条件变量(如生产者-消费者模型中的多个等待队列)。
4> 替代 synchronized 的高竞争场景(如线程池任务队列)。
2.1.6 ReentrantLock和synchronized的对比
特性 | ReentrantLock | synchronized |
---|---|---|
锁获取方式 | 显式调用 lock() /unlock() | 隐式通过代码块或方法 |
公平性 | 支持公平和非公平锁 | 仅非公平锁 |
可中断性 | 支持(lockInterruptibly() ) | 不支持 |
超时获取锁 | 支持(tryLock() ) | 不支持 |
条件变量 | 支持多个 Condition | 单一条件(wait() /notify() ) |
性能 | 高竞争场景下更优 | 低竞争场景优化更好(偏向锁、轻量级锁) |
代码灵活性 | 高(可跨方法、控制粒度细) | 低(基于代码块) |
2.2 ReentrantReadWriteLock
(读写锁)
2.2.1 用法
1> 创建读写锁,进行加锁和解锁
// 构造方法默认非公平模式,传参true为公平模式
ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
ReentrantReadWriteLock.ReadLock readLock = rwLock.readLock(); // 获取读锁
ReentrantReadWriteLock.WriteLock writeLock = rwLock.writeLock(); // 获取写锁
// 读锁(共享):允许多个线程同时获取。
readLock.lock();
try {
// 读操作
} finally {
readLock.unlock();
}
//写锁(独占):仅允许单个线程获取。只允许无读锁和写锁被持有的情况下获取
writeLock.lock();
try {
// 写操作
} finally {
writeLock.unlock();
}
2> 锁降级(允许)
ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
rwLock.writeLock().lock(); // 1. 获取写锁
try {
// 2. 修改数据(如更新缓存)
updateData();
// 3. 获取读锁(锁降级关键)
rwLock.readLock().lock();
} finally {
rwLock.writeLock().unlock(); // 4. 释放写锁,在释放时,降级为读锁
}
try {
// 5. 读取数据(其他读线程可并发访问)
readData();
} finally {
rwLock.readLock().unlock(); // 6. 释放读锁
}
锁降级的意义:
数据一致性:在写锁降级为读锁的过程中,确保其他线程无法插入写操作,避免数据更新过程中出现中间状态。
高并发性:释放写锁后,允许多个读线程并发访问最新数据,减少写锁独占时间,提升系统吞吐量。
适用场景:缓存更新、配置热加载等需“更新后立即读取”的高并发场景。
3> 锁升级(禁止)
readLock.lock();
try {
// 读操作
writeLock.lock(); // 错误:直接尝试升级会导致死锁!
} finally {
readLock.unlock();
}
当前线程持有读锁,再去获取写锁,会导致阻塞(等待自身读锁释放),从而死锁
2.2.2 锁的特性
1> 读写分离
-
读锁共享:允许多个线程并发读取。
-
写锁独占:确保写操作互斥,防止数据不一致。
2> 可重入性:同一线程可重复获取读锁或写锁,释放次数需匹配获取次数。
3> 公平性选项
-
公平模式:线程按请求顺序获取锁(通过构造函数设置
true
)。 -
非公平模式:允许插队,提高吞吐(默认模式)。
4> 锁降级支持:允许从写锁降级为读锁,确保数据一致性
2.2.3 原理:AQS状态分割
1> 基于AQS同步器
-
状态分割:AQS 的
state
分为两部分:-
高 16 位:写锁的重入次数。
-
低 16 位:读锁的持有线程数。
-
-
写锁实现:使用独占模式,记录持有线程及重入次数。
-
读锁实现:使用共享模式,维护读线程计数。
2> 锁竞争逻辑
-
写锁获取条件:无读锁和写锁被持有。
-
读锁获取条件:无写锁被持有,或当前线程持有写锁(锁降级)。
3> 等待队列管理
-
公平模式下,线程按 FIFO 顺序获取锁。
-
非公平模式下,新线程可能直接插队尝试获取锁。
2.2.3 使用注意事项
1> 避免锁升级:持有读锁时,不可直接尝试获取写锁,否则导致死锁。需先释放读锁再获取写锁
2> 性能权衡:适用读多写少的情况(如缓存、配置管理)
3> 死锁防范:确保锁的获取和释放成对出现
4> 公平性选择:公平模式减少线程饥饿,但降低吞吐;非公平模式提高吞吐,可能导致读/写线程饥饿
5> 监控与调试
-
使用
getReadLockCount()
和isWriteLocked()
监控锁状态。 -
通过线程转储(
jstack
)分析锁竞争问题。
2.3 StampedLock
(乐观读锁,不可重入,非公平)
StampedLock 是 Java 8 引入的高性能锁,支持三种访问模式(读锁、写锁、乐观读),适用于读多写少的高并发场景。相比 ReentrantReadWriteLock
,它通过乐观读和无锁化设计显著提升性能,但牺牲了部分功能(如可重入性)。
2.3.1 用法
1> 锁的获取与释放:互斥写锁、悲观读锁、乐观读
StampedLock lock = new StampedLock();
// 写锁(独占锁)
long writeStamp = lock.writeLock(); // 阻塞获取写锁
try {
// 写操作...
} finally {
lock.unlockWrite(writeStamp); // 释放写锁
}
// 悲观读锁(共享锁)
long readStamp = lock.readLock(); // 阻塞获取读锁
try {
// 读操作...
} finally {
lock.unlockRead(readStamp); // 释放读锁
}
// 乐观读(无锁,仅验证数据一致性)
long optimisticStamp = lock.tryOptimisticRead();
// 读操作(不阻塞)
if (!lock.validate(optimisticStamp)) { // 检查期间是否有写操作
// 数据可能被修改,升级为悲观读锁
long readStamp = lock.readLock();
try {
// 重新读取数据...
} finally {
lock.unlockRead(readStamp);
}
}
2> 锁的尝试获取与超时
// 尝试获取写锁(非阻塞)
long stamp = lock.tryWriteLock();
if (stamp != 0L) {
try { /* 写操作 */ } finally { lock.unlockWrite(stamp); }
}
// 带超时的读锁获取
long stamp = lock.tryReadLock(1, TimeUnit.SECONDS);
if (stamp != 0L) {
try { /* 读操作 */ } finally { lock.unlockRead(stamp); }
}
3> 锁转换
// 锁升级(读锁 → 写锁,可能导致死锁!需谨慎)
long readStamp = lock.readLock();
try {
// 尝试转换为写锁
long writeStamp = lock.tryConvertToWriteLock(readStamp);
if (writeStamp == 0L) {
// 转换失败,需手动释放读锁并重新获取写锁
lock.unlockRead(readStamp);
writeStamp = lock.writeLock();
}
try { /* 写操作 */ } finally { lock.unlockWrite(writeStamp); }
} finally {
if (lock.isReadLocked()) lock.unlockRead(readStamp);
}
锁转换支持:提供 tryConvertToReadLock
、tryConvertToWriteLock
等方法,允许锁模式切换
意义:减少锁竞争开销,避免释放与重新获取锁的竞争窗口和数据不一致问题
2.3.2 实现原理
1> 状态管理
-
state 变量:64 位长整型,分为两部分:
-
低 7 位:读锁计数(最大支持 126 个读线程)。
-
高 57 位:写锁标记和版本戳(用于乐观读验证)。
-
-
戳记(Stamp):锁状态的版本号,用于验证乐观读的有效性。
2> 锁竞争机制
-
写锁:独占模式,通过 CAS 设置写锁标记。
-
读锁:共享模式,通过 CAS 增加读锁计数。
-
乐观读:仅记录当前版本戳,不修改锁状态。
3> CLH队列
-
基于 CLH(Craig, Landin, and Hagersten)队列管理等待线程,减少锁竞争开销。
2.3.3 锁的特性
1> 乐观读
-
无锁化读:仅通过版本戳(Stamp)验证数据一致性,避免阻塞。
-
轻量高效:若读期间无写操作,性能接近无锁;若检测到写操作,需升级为读锁重新读取。
2> 不可重入性:同一线程重复获取锁会导致死锁,必须严格匹配加锁/解锁次数。不可重入性减少了状态管理开销
3> 锁转换支持:提供 tryConvertToReadLock
、tryConvertToWriteLock
等方法,允许锁模式切换(需谨慎处理)
4> 非公平策略:采用非公平锁策略,允许新请求插队,提高吞吐量
2.3.4 使用注意事项
1> 乐观读的正确使用
-
必须验证 Stamp:在乐观读后调用
validate(stamp)
,确保数据未被修改。 -
升级锁的时机:若验证失败,需升级为悲观读锁重新读取数据。
2> 避免死锁
-
不可重入:同一线程不可重复获取同一锁(即使已持有)。
-
锁转换风险:升级锁(如读锁 → 写锁)可能失败,需处理回退逻辑。
3> 锁释放与资源管理
-
必须配对释放:每个
lock()
必须对应unlock()
,否则导致资源泄漏。 -
异常处理:在
finally
块中释放锁,确保异常时仍能释放。
3、乐观锁与无锁编程
3.1 CAS(Compare-And-Swap)
3.1.1 基本原理
CAS 是一种原子操作,用于在多线程环境下无锁(Lock-Free)地更新共享变量。
当变量值为A的情况下,才更改成B,更改成功;否则更改失败
底层实现:
-
依赖 CPU 的原子指令(如 x86 的
CMPXCHG
)。 -
Java 中通过
sun.misc.Unsafe
类或原子类(如AtomicInteger
)封装 CAS 操作。
3.1.2 优缺点
优点:
-
无锁并发:避免线程阻塞和上下文切换,提高高并发场景的性能。
-
轻量高效:适合简单原子操作(如计数器、状态标志)。
-
避免死锁:无锁设计天然规避了锁导致的死锁问题。
缺点:
-
ABA 问题:
变量值从 A → B → A,CAS 无法感知中间变化(需通过版本号/时间戳或时AtomicStampedReference
解决)。 -
自旋开销:
竞争激烈时,线程可能长时间自旋(循环尝试),浪费 CPU 资源。
自旋优化策略:1> 指数退避:竞争激烈时,逐步增加自旋间隔;2> 适应性自旋:JVM根据历史成功率动态调整自旋次数
替代策略:自旋可能导致性能劣化,可改用锁(如 LongAdder
替代 AtomicLong
)
-
单一变量限制:
只能保证一个共享变量的原子性,多变量原子更新需结合锁或synchronized
。
3.1.3 使用注意事项
1> 避免过度自旋:设置最大尝试次数或退避策略,防止 CPU 空转。
2> 结合业务验证:即使 CAS 成功,仍需业务逻辑验证(如分布式场景)
3> 性能监控:通过 Profiler 工具(如 JProfiler、Async Profiler)监控 CAS 成功率及耗时
4> 替代方案评估:
-
低竞争场景:优先使用 CAS。
-
高竞争场景:考虑锁、
LongAdder
或分段锁。
3.1.4 典型应用场景
1> 原子类(java.util.concurrent.atomic
)
AtomicInteger
、AtomicLong
等通过 CAS 实现原子更新
AtomicInteger count = new AtomicInteger(0);
count.incrementAndGet(); // CAS 实现自增
2> 无锁数据结构
无锁队列(如 ConcurrentLinkedQueue
)、无锁栈、无锁哈希表
// 无锁栈的 push 操作
do {
Node<T> oldTop = top.get();
newTop.next = oldTop;
} while (!top.compareAndSet(oldTop, newTop));
3> 乐观锁机制
数据库乐观锁、分布式锁(如 Redis 的 SETNX
命令)
UPDATE table SET val = new_val, version = version + 1
WHERE id = 1 AND version = old_version; -- 类似 CAS 语义
3.2 Atomic原子类
3.2.1 用法
1> 自增与获取值
低竞争场景:优先使用AtomicInteger
、AtomicLong
。
AtomicInteger atomicInt = new AtomicInteger(0);
int newValue = atomicInt.incrementAndGet(); // 1(原子自增)
int oldValue = atomicInt.getAndIncrement(); // 1(返回旧值,再自增)
2> 原子更新对象引用
AtomicReference<String> ref = new AtomicReference<>("初始值");
ref.compareAndSet("初始值", "新值"); // 成功则更新
3> 解决ABA问题
ABA问题:
线程1读取变量值为A
,线程2将值改为B
后又改回A
,此时线程1的CAS操作仍会成功,但期间变量已被修改过
解决方案:
使用AtomicStampedReference
或AtomicMarkableReference
,通过版本号或标记跟踪变更
AtomicStampedReference<String> stampedRef =
new AtomicStampedReference<>("A", 0);
int[] stampHolder = new int[1];
String currentRef = stampedRef.get(stampHolder); // 获取引用和版本号
stampedRef.compareAndSet("A", "B", stampHolder[0], stampHolder[0] + 1); // 更新引用和版本号
4> 高竞争场景
高竞争场景:使用LongAdder
或DoubleAdder
(分段累加,减少竞争)
LongAdder adder = new LongAdder();
adder.add(10); // 累加
long sum = adder.sum(); // 获取总和(非原子操作)
3.2.2 作用及核心类
原子类的作用:
-
线程安全操作:无需显式锁(如
synchronized
或Lock
),提供无锁的线程安全操作。 -
避免竞态条件:通过原子操作(如自增、CAS)保证多线程环境下的数据一致性。
-
高性能:在低/中等竞争场景下,性能优于传统锁机制。
类名 | 用途 |
---|---|
AtomicInteger | 原子操作的整型变量(如计数器)。 |
AtomicLong | 原子操作的长整型变量。 |
AtomicBoolean | 原子操作的布尔变量(如状态标志位)。 |
AtomicReference | 原子操作的对象引用(可用于实现无锁数据结构)。 |
AtomicStampedReference | 解决ABA问题,通过版本号(int stamp )跟踪引用变更。 |
AtomicMarkableReference | 类似AtomicStampedReference ,但版本标记为boolean 。 |
LongAdder /DoubleAdder | 高并发场景下的累加器,性能优于AtomicLong (分段减少竞争)。 |
AtomicIntegerArray | 原子操作的整型数组。 |
AtomicReferenceFieldUpdater | 原子更新类的指定字段(需volatile 修饰)。 |
3.2.3 原理
通过硬件指令(如cmpxchg
)实现无锁并发。操作包含三个值:
-
内存地址(
V
) -
预期原值(
A
) -
新值(
B
)
若V
的当前值等于A
,则将V
更新为B
,否则放弃操作。
方法:compareAndSet(expectedValue, newValue)
,返回boolean
表示是否成功。
AtomicInteger count = new AtomicInteger(0);
boolean updated = count.compareAndSet(0, 1); // 若当前值为0,则更新为1
3.2.4 适用场景
-
计数器:如统计请求量、在线人数。
-
状态标志:如开关状态的原子切换。
-
无锁数据结构:如实现线程安全的栈、队列。
-
高并发累加:如使用
LongAdder
统计点击量。
二、监控与分析工具
详见博客:JVM监控
1、jstack:生成线程转储文件,查看锁持有和等待状态
jstack <pid> > thread_dump.txt
-
关键信息:
-
BLOCKED
状态的线程(锁竞争)。 -
waiting on <0x000000076bf62200>
(等待的锁对象地址)。 -
locked <0x000000076bf62200>
(当前持有的锁对象地址)。
-
2、Jconsole:图形化查看线程状态、锁竞争和死锁检测
-
操作:
-
连接目标Java进程。
-
查看 线程(Threads) 标签页,检测死锁(Deadlock Detection)。
-
-
优势:直观展示线程阻塞和锁持有关系。
3、Arthas:第三方工具
-
核心命令:
-
thread -b
:快速定位死锁。 -
monitor
:监控方法调用耗时和锁竞争。 -
watch
:观察方法参数和返回值。
-
-
示例:
# 监控某个方法的锁竞争 monitor -c 5 com.example.MyClass syncMethod