1. 多线程同步机制的核心挑战
在Java开发中,多线程编程就像指挥一个交响乐团——每个乐器(线程)都需要在正确的时间发出正确的声音。但当多个线程同时访问共享资源时,就会出现如同乐器走调般的混乱局面。这就是为什么我们需要synchronized、volatile和CAS这三种不同的"指挥棒"来协调线程间的协作。
我曾在生产环境遇到过这样一个案例:一个简单的计数器在多线程环境下出现了严重的数值偏差。当时团队花了三天时间排查,最终发现是因为没有正确理解这三种同步机制的区别。这个教训让我深刻认识到,掌握它们的本质差异是每个Java开发者必备的技能。
2. synchronized:重量级的同步卫士
2.1 同步机制的本质
synchronized是Java中最基础的同步工具,它就像音乐厅的独奏时段——同一时刻只允许一个演奏者使用舞台。从JVM层面看,它通过对象头的Mark Word实现锁机制,包含偏向锁、轻量级锁和重量级锁三种状态转换。
java复制public class TicketSales {
private int tickets = 100;
public synchronized void sellTicket() {
if(tickets > 0) {
tickets--;
System.out.println(Thread.currentThread().getName() + "售出一张票,剩余:" + tickets);
}
}
}
2.2 锁的升级过程
当第一个线程访问同步块时,JVM会启用偏向锁,在对象头记录线程ID。这就像给常客办理VIP卡,下次入场时可以直接识别。当有其他线程竞争时,偏向锁升级为轻量级锁(自旋锁),线程通过CAS尝试获取锁。如果自旋超过一定次数(JDK6默认10次),就会升级为重量级锁,线程进入阻塞状态。
注意:在JDK15后,偏向锁被标记为废弃,因为现代多核处理器环境下维护偏向锁的开销可能超过其收益。
2.3 适用场景与性能考量
在以下场景优先考虑synchronized:
- 需要保证操作原子性的复合操作(如转账)
- 临界区执行时间较长(超过2ms)
- 需要等待-通知机制(wait/notify)
但要注意它的性能瓶颈:
- 锁的获取和释放需要操作系统介入,涉及用户态到内核态的切换
- 不当的锁粒度会导致并发度下降
- 可能引发死锁(如哲学家就餐问题)
3. volatile:轻量级的可见性保证
3.1 内存可见性原理
volatile解决的问题就像舞台灯光控制室和演员之间的通信——确保灯光状态的改变能被所有演员立即看到。它通过两个关键机制实现:
- 禁止指令重排序(内存屏障)
- 保证变量的读写直接操作主内存
java复制public class StageLight {
private volatile boolean lightsOn = false;
public void toggleLights() {
lightsOn = !lightsOn; // 非原子操作!
}
}
3.2 典型应用场景
volatile最适合作为状态标志位:
- 服务启停控制
- 配置热更新标记
- 事件发生通知
但必须记住它的局限性:
- 不保证复合操作的原子性(如i++)
- 不能替代锁机制
- 过度使用可能导致缓存行频繁失效(伪共享)
3.3 与happens-before的关系
volatile变量的写操作与后续读操作建立happens-before关系。这意味着:
- 写操作前的所有修改对读操作可见
- JVM不会进行可能破坏这种关系的优化
4. CAS:无锁并发的利器
4.1 底层实现原理
CAS(Compare-And-Swap)就像拍卖会的举牌竞价——只有当前价格等于你的预期时,出价才会被接受。现代CPU通过cmpxchg指令提供原子CAS支持,Java通过Unsafe类暴露这个能力。
java复制public class Auction {
private AtomicInteger currentBid = new AtomicInteger(100);
public boolean placeBid(int expected, int newBid) {
return currentBid.compareAndSet(expected, newBid);
}
}
4.2 ABA问题解决方案
ABA问题就像借书场景:你看到书还在原位,但可能已经被借走又归还了。解决方案:
- 版本号标记(AtomicStampedReference)
- 布尔标记(AtomicMarkableReference)
java复制AtomicStampedReference<String> book = new AtomicStampedReference<>("Java", 0);
// 借书时增加版本号
int[] stamp = new int[1];
String current = book.get(stamp);
book.compareAndSet(current, "Borrowed", stamp[0], stamp[0]+1);
4.3 高性能并发组件中的应用
CAS是现代并发容器的基石:
- ConcurrentHashMap的节点操作
- CopyOnWriteArrayList的写时复制
- Disruptor框架的核心机制
它的优势在于:
- 无锁设计减少线程阻塞
- 细粒度并发控制
- 可预测的性能表现
5. 深度对比与选型指南
5.1 特性对比矩阵
| 特性 | synchronized | volatile | CAS |
|---|---|---|---|
| 原子性 | 是 | 否 | 单变量是 |
| 可见性 | 是 | 是 | 是 |
| 有序性 | 是 | 是 | 否 |
| 线程阻塞 | 是 | 否 | 否(但可能自旋) |
| 适用场景 | 复杂临界区 | 状态标志 | 计数器/累加器 |
| JDK实现类 | 语言原生支持 | 语言特性 | AtomicXxx系列 |
5.2 性能测试数据
通过JMH基准测试(纳秒/操作):
- 单线程场景:volatile ≈ CAS < synchronized
- 低竞争(4线程):CAS ≈ synchronized < volatile(因不保证原子性)
- 高竞争(32线程):CAS > synchronized > volatile
5.3 选型决策树
- 是否需要保证复合操作原子性?
- 是 → synchronized
- 否 → 进入2
- 是否仅需保证可见性?
- 是 → volatile
- 否 → 进入3
- 是否是单变量原子操作?
- 是 → CAS
- 否 → synchronized
6. 实战中的陷阱与优化
6.1 锁粒度优化案例
错误示范:
java复制public class UserService {
private final Object lock = new Object();
public void updateUser(User user) {
synchronized(lock) {
// 包含DB操作和日志记录
db.update(user);
log.info("Updated user: {}", user);
}
}
}
优化方案:
java复制public class UserService {
private final ConcurrentMap<Long, Object> idLocks = new ConcurrentHashMap<>();
public void updateUser(User user) {
Object lock = idLocks.computeIfAbsent(user.getId(), k -> new Object());
synchronized(lock) {
db.update(user);
}
log.info("Updated user: {}", user); // 移出同步块
}
}
6.2 伪共享问题诊断
使用@Contended注解解决(需开启JVM参数-XX:-RestrictContended):
java复制public class FalseSharing {
@Contended
volatile long value1;
@Contended
volatile long value2;
}
6.3 锁与CAS的混合策略
在高度竞争场景下的优化模式:
java复制public class HybridCounter {
private AtomicInteger casCounter = new AtomicInteger();
private int threshold = 10;
public void increment() {
int failures = 0;
while(true) {
int current = casCounter.get();
if(casCounter.compareAndSet(current, current+1)) {
return;
}
if(++failures > threshold) {
synchronized(this) {
casCounter.set(casCounter.get()+1);
}
return;
}
}
}
}
7. JVM层面的实现细节
7.1 synchronized的字节码表现
同步方法会添加ACC_SYNCHRONIZED标志:
code复制public synchronized void method();
descriptor: ()V
flags: ACC_PUBLIC, ACC_SYNCHRONIZED
同步块会生成monitorenter/monitorexit指令:
code复制3: monitorenter
...
20: monitorexit
7.2 volatile的内存屏障
在x86架构下,volatile写对应StoreLoad屏障:
code复制lock addl $0x0,(%rsp) ; 将栈顶值加0,利用lock实现屏障
7.3 CAS的本地方法实现
Unsafe.compareAndSwapInt的HotSpot实现:
cpp复制UNSAFE_ENTRY(jboolean, Unsafe_CompareAndSwapInt(JNIEnv *env, jobject unsafe, jobject obj, jlong offset, jint e, jint x))
oop p = JNIHandles::resolve(obj);
jint* addr = (jint *)index_oop_from_field_offset_long(p, offset);
return Atomic::cmpxchg(x, addr, e) == e;
UNSAFE_END
8. 并发模式的最佳实践
8.1 单例模式的双重检查锁
正确实现方案:
java复制public class Singleton {
private static volatile Singleton instance;
public static Singleton getInstance() {
if(instance == null) {
synchronized(Singleton.class) {
if(instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
8.2 无锁队列的实现
基于CAS的Michael-Scott队列:
java复制public class LockFreeQueue<T> {
private static class Node<T> {
volatile T item;
volatile Node<T> next;
}
private volatile Node<T> head;
private volatile Node<T> tail;
public void enq(T item) {
Node<T> node = new Node<>();
node.item = item;
while(true) {
Node<T> last = tail;
Node<T> next = last.next;
if(last == tail) {
if(next == null) {
if(compareAndSet(last.next, next, node)) {
compareAndSet(tail, last, node);
return;
}
} else {
compareAndSet(tail, last, next);
}
}
}
}
}
8.3 并发累加器的选择
根据场景选择最优方案:
- 低竞争:AtomicLong
- 高竞争:LongAdder(分段CAS)
- 精确控制:synchronized块
java复制// LongAdder使用示例
LongAdder adder = new LongAdder();
parallelStream().forEach(i -> {
adder.increment();
});
long total = adder.sum();
9. 常见面试问题深度解析
9.1 synchronized和ReentrantLock的区别
关键差异点:
- 实现机制:synchronized是JVM内置,ReentrantLock是JDK实现
- 功能特性:ReentrantLock支持公平锁、条件变量等
- 性能表现:JDK6后两者性能接近
- 使用方式:synchronized自动释放锁,ReentrantLock需要手动unlock
9.2 volatile和final的可见性
final字段的特殊保证:
- 正确构造的对象,final字段对所有线程可见
- 不需要同步即可保证初始化安全性
- 但仅限于构造期间赋值的引用
9.3 CAS的底层硬件支持
现代CPU的CAS实现:
- x86: CMPXCHG指令
- ARM: LDREX/STREX指令对
- PowerPC: lwarx/stwcx指令对
10. 并发调试与性能分析
10.1 线程转储分析
识别锁竞争:
code复制"Thread-1" #12 prio=5 os_prio=0 tid=0x00007f48740f8000 nid=0x2a1f waiting for monitor entry [0x00007f486b7f6000]
java.lang.Thread.State: BLOCKED (on object monitor at com.example.Test.method(Test.java:15))
10.2 JFR监控锁争用
使用JCMD开启飞行记录:
bash复制jcmd <pid> JFR.start duration=60s filename=recording.jfr
分析Lock Instances视图中的:
- Wait Time
- Blocked Time
- Wait Count
10.3 基准测试注意事项
JMH测试要点:
- 使用@State(Scope.Thread)
- 避免死代码消除(Blackhole.consumeCPU)
- 考虑不同竞争程度场景
- 预热足够次数(@Warmup)