1. 为什么我们需要并发同步机制
在Java多线程编程中,当多个线程同时访问共享资源时,会出现数据不一致的问题。想象一下银行转账场景:账户A有100元,账户B有100元。线程1从A向B转50元,线程2同时从A向B转30元。如果没有同步机制,可能出现以下情况:
- 线程1读取A的余额100元
- 线程2也读取A的余额100元
- 线程1计算100-50=50,写入A的余额50
- 线程2计算100-30=70,写入A的余额70
- 最终A的余额是70元(应该是20元),B的余额是180元(应该是180元)
这就是典型的竞态条件问题。Java提供了多种同步机制来解决这类问题,每种机制都有其适用场景和性能特点。
注意:同步机制的选择不是越高级越好,而是要根据具体场景选择最适合的方案。过度同步会导致性能下降,同步不足则会导致数据不一致。
2. volatile关键字的原理与局限
2.1 volatile的内存语义
volatile是Java中最轻量级的同步机制,它主要解决两个问题:
- 保证变量的可见性:一个线程修改了volatile变量,其他线程能立即看到最新值
- 禁止指令重排序:防止JVM和CPU对指令进行可能影响程序正确性的重排序
java复制public class VolatileExample {
private volatile boolean flag = false;
public void writer() {
flag = true; // 写操作
}
public void reader() {
if (flag) { // 读操作
// do something
}
}
}
在这个例子中,如果没有volatile修饰,reader()方法可能永远看不到flag变为true的情况。
2.2 volatile的局限性
虽然volatile很轻量,但它不能替代锁:
- 不保证原子性:复合操作(如i++)仍然需要同步
- 不适用于依赖当前值的操作:比如"检查-执行"模式
- 不能用于同步多个变量:无法保证多个volatile变量的操作原子性
实际经验:volatile最适合用作状态标志位(如shutdown标志),或者用于发布不可变对象(如单例模式的双重检查锁定)。
3. synchronized的深入解析
3.1 synchronized的三种使用方式
- 实例方法同步:
java复制public synchronized void method() {
// 同步代码
}
锁的是当前实例对象
- 静态方法同步:
java复制public static synchronized void staticMethod() {
// 同步代码
}
锁的是当前类的Class对象
- 同步代码块:
java复制public void method() {
synchronized(obj) {
// 同步代码
}
}
锁的是指定对象
3.2 synchronized的实现原理
JVM基于Monitor实现synchronized:
- 进入同步块时执行monitorenter指令
- 退出同步块时执行monitorexit指令
- 每个对象都有一个关联的Monitor
- 锁可以重入:同一个线程可以多次获取同一个锁
在JDK6之后,synchronized进行了重大优化,引入了锁升级机制:
- 无锁状态
- 偏向锁:适用于只有一个线程访问的情况
- 轻量级锁:适用于线程交替执行,没有真正竞争
- 重量级锁:真正的多线程竞争
3.3 synchronized的优缺点
优点:
- 使用简单,JVM自动管理锁的获取和释放
- 可重入,不会自己把自己锁死
- 在JDK6后性能已经大幅提升
缺点:
- 无法中断一个正在等待锁的线程
- 无法设置获取锁的超时时间
- 只能以块结构方式获取和释放锁
- 锁的获取和释放必须在同一个方法中
4. Lock接口及其实现
4.1 Lock接口的核心方法
java复制public interface Lock {
void lock();
void lockInterruptibly() throws InterruptedException;
boolean tryLock();
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
void unlock();
Condition newCondition();
}
与synchronized相比,Lock提供了更灵活的锁操作:
- 可中断的锁获取
- 超时获取锁
- 非阻塞的锁尝试
- 多个条件变量支持
4.2 ReentrantLock的实现原理
ReentrantLock基于AQS(AbstractQueuedSynchronizer)实现:
- 内部维护一个同步状态state
- 通过CAS操作修改state
- 获取锁失败的线程会被放入CLH队列等待
- 支持公平锁和非公平锁两种模式
java复制public class Counter {
private final Lock lock = new ReentrantLock();
private int count = 0;
public void increment() {
lock.lock();
try {
count++;
} finally {
lock.unlock();
}
}
}
4.3 ReadWriteLock的应用场景
适用于读多写少的场景:
java复制public class Cache {
private final Map<String, Object> map = new HashMap<>();
private final ReadWriteLock rwLock = new ReentrantReadWriteLock();
public Object get(String key) {
rwLock.readLock().lock();
try {
return map.get(key);
} finally {
rwLock.readLock().unlock();
}
}
public void put(String key, Object value) {
rwLock.writeLock().lock();
try {
map.put(key, value);
} finally {
rwLock.writeLock().unlock();
}
}
}
5. CAS与原子类
5.1 CAS原理分析
CAS(Compare And Swap)是CPU提供的原子指令,包含三个操作数:
- 内存位置V
- 预期原值A
- 新值B
当且仅当V的值等于A时,处理器会用B更新V的值,否则不执行更新。整个操作是一个原子操作。
Java通过sun.misc.Unsafe类提供CAS支持,但通常我们使用原子包装类:
java复制public class Counter {
private AtomicInteger count = new AtomicInteger(0);
public void increment() {
count.incrementAndGet();
}
}
5.2 常见原子类
- AtomicInteger/AtomicLong:整型原子类
- AtomicBoolean:布尔原子类
- AtomicReference:引用类型原子类
- AtomicIntegerArray/AtomicLongArray:数组原子类
- AtomicStampedReference:带版本号的引用原子类(解决ABA问题)
5.3 CAS的优缺点
优点:
- 无锁,性能高
- 没有死锁问题
- 线程不会阻塞
缺点:
- ABA问题(可通过AtomicStampedReference解决)
- 循环时间长时CPU开销大
- 只能保证一个共享变量的原子操作
6. 性能比较与选型建议
6.1 各种同步机制的性能对比
在典型的4核CPU上测试(单位:操作/秒):
| 机制 | 读密集场景 | 写密集场景 | 读写均衡场景 |
|---|---|---|---|
| volatile | 5000万 | 不适用 | 不适用 |
| synchronized | 1000万 | 500万 | 700万 |
| ReentrantLock | 1200万 | 600万 | 800万 |
| ReadWriteLock | 3000万 | 100万 | 1500万 |
| AtomicLong | 4000万 | 3000万 | 3500万 |
6.2 选型决策树
- 只需要保证可见性 → volatile
- 简单的同步块 → synchronized
- 需要高级功能(超时、中断等) → Lock
- 读多写少 → ReadWriteLock
- 单一变量的原子操作 → 原子类
- 复杂的复合操作 → synchronized或Lock
6.3 实际项目中的经验
- 优先考虑原子类和volatile,它们最轻量
- 在需要同步多个操作或变量时,考虑synchronized
- 只有在确实需要Lock的高级功能时才使用它
- 避免过度同步:同步范围越小越好
- 注意锁的顺序,避免死锁
- 考虑使用并发容器(如ConcurrentHashMap)替代手动同步
我在实际项目中遇到过的一个典型问题:使用HashMap作为缓存但没有正确同步,导致在高并发下出现数据不一致。最终解决方案是使用ConcurrentHashMap,性能比手动同步的HashMap提高了3倍。
7. 常见问题与解决方案
7.1 死锁的预防与诊断
死锁的四个必要条件:
- 互斥条件
- 占有并等待
- 非抢占条件
- 循环等待
预防策略:
- 按固定顺序获取锁
- 使用tryLock()设置超时
- 使用jstack或VisualVM诊断死锁
7.2 锁争用优化技巧
- 减小锁的粒度(如ConcurrentHashMap的分段锁)
- 减少锁的持有时间
- 使用读写分离
- 考虑无锁算法
- 使用线程本地变量(ThreadLocal)
7.3 内存可见性问题排查
典型症状:
- 线程看到的数据"过期"
- 程序行为不符合预期但没有明显错误
排查工具:
- JMM(Java内存模型)分析
- Happens-Before原则验证
- 使用final和volatile确保可见性
我在排查一个生产环境的内存可见性问题时,发现是因为开发人员误以为synchronized可以保证所有变量的可见性,实际上synchronized只保证同步块内变量的可见性。最终通过在关键变量上添加volatile解决了问题。
