1. 为什么我们需要并发控制?
当多个线程同时访问共享资源时,如果没有适当的同步机制,就会出现数据竞争(Data Race)问题。想象一下银行转账场景:账户A有100元,账户B有100元,两个线程同时执行A向B转账50元的操作。如果没有锁保护,可能会出现以下执行序列:
- 线程1读取A的余额:100
- 线程2读取A的余额:100
- 线程1计算新余额:100-50=50
- 线程2计算新余额:100-50=50
- 线程1写入A的新余额:50
- 线程2写入A的新余额:50
最终A的余额变成了50元而不是预期的0元,这就是典型的并发问题。Java提供了两种主要的并发控制机制:锁和CAS(Compare-And-Swap)。
关键点:数据竞争不仅会导致数值错误,在复杂对象操作中还可能破坏对象内部一致性,引发更隐蔽的问题。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 锁机制深度解析
2.1 synchronized关键字
synchronized是Java最基本的锁机制,使用方式有三种:
java复制// 实例方法锁
public synchronized void method() {}
// 静态方法锁
public static synchronized void staticMethod() {}
// 代码块锁
public void blockMethod() {
synchronized(this) {}
}
锁的获取和释放由JVM自动管理,底层通过对象头中的Mark Word实现。在JDK1.6之前,synchronized是重量级锁,性能较差;之后引入了锁升级机制:
无锁 → 偏向锁 → 轻量级锁 → 重量级锁
2.2 ReentrantLock详解
ReentrantLock是JUC包中的显式锁实现,相比synchronized提供了更多高级功能:
java复制Lock lock = new ReentrantLock();
lock.lock();
try {
// 临界区代码
} finally {
lock.unlock();
}
主要特性包括:
- 可重入性:同一个线程可以多次获取同一把锁
- 公平性选择:构造时可指定是否为公平锁
- 条件变量:通过newCondition()创建多个等待队列
- 可中断的获取锁:lockInterruptibly()
- 超时获取锁:tryLock(long timeout, TimeUnit unit)
2.3 读写锁ReentrantReadWriteLock
适用于读多写少的场景,允许多个读线程同时访问,但写线程独占:
java复制ReadWriteLock rwLock = new ReentrantReadWriteLock();
Lock readLock = rwLock.readLock();
Lock writeLock = rwLock.writeLock();
实际使用中发现:当写锁等待时,后续的读锁也会被阻塞,防止写线程饥饿。这种锁降级机制需要特别注意。
3. CAS原理与实践
3.1 CAS工作机制
CAS(Compare And Swap)是CPU提供的原子指令,基本逻辑:
java复制boolean compareAndSwap(V expected, V newValue) {
if (currentValue == expected) {
currentValue = newValue;
return true;
}
return false;
}
Java通过Unsafe类提供CAS操作,Atomic系列类基于此实现。以AtomicInteger为例:
java复制public final int getAndIncrement() {
return unsafe.getAndAddInt(this, valueOffset, 1);
}
3.2 ABA问题与解决方案
CAS操作存在ABA问题:值从A→B→A,CAS无法感知中间变化。解决方案是使用版本号,如AtomicStampedReference:
java复制AtomicStampedReference<Integer> asr = new AtomicStampedReference<>(100, 0);
int stamp = asr.getStamp();
asr.compareAndSet(100, 101, stamp, stamp+1);
3.3 CAS性能考量
在低竞争场景下,CAS性能优于锁;但在高竞争时,大量自旋会消耗CPU资源。经验值:当线程数超过CPU核心数2倍时,应考虑使用锁替代。
4. 锁与CAS的实战对比
4.1 适用场景分析
| 特性 | 锁 | CAS |
|---|---|---|
| 实现复杂度 | 简单 | 复杂 |
| 阻塞 | 是 | 否 |
| 内存开销 | 较高 | 较低 |
| 适用竞争程度 | 高竞争 | 低竞争 |
| 公平性支持 | 是 | 否 |
| 死锁风险 | 有 | 无 |
| 代码可读性 | 高 | 低 |
4.2 性能测试对比
创建计数器接口的两种实现:
java复制interface Counter {
void increment();
long getCount();
}
// 锁实现
class LockCounter implements Counter {
private long count = 0;
private final Lock lock = new ReentrantLock();
public void increment() {
lock.lock();
try {
count++;
} finally {
lock.unlock();
}
}
}
// CAS实现
class CasCounter implements Counter {
private final AtomicLong count = new AtomicLong(0);
public void increment() {
count.incrementAndGet();
}
}
测试结果(4核CPU,单位:ops/ms):
| 线程数 | LockCounter | CasCounter |
|---|---|---|
| 1 | 12,345 | 56,789 |
| 4 | 3,456 | 23,456 |
| 16 | 1,234 | 5,678 |
| 64 | 789 | 1,234 |
4.3 复合操作处理
对于需要多个变量保持一致的复合操作,CAS实现较为复杂。例如转账操作:
java复制class Account {
private AtomicInteger balance;
boolean transfer(Account to, int amount) {
while (true) {
int fromBal = balance.get();
if (fromBal < amount) return false;
int toBal = to.balance.get();
if (balance.compareAndSet(fromBal, fromBal - amount)) {
to.balance.compareAndSet(toBal, toBal + amount);
return true;
}
}
}
}
这种实现存在中间状态不一致的问题,实际项目中建议使用锁处理复合操作。
5. 高级并发模式
5.1 锁分段技术
ConcurrentHashMap使用的分段锁技术,将数据分成多个段,每段独立加锁:
java复制// 简化版分段锁实现
class Segment<K,V> extends ReentrantLock {
HashMap<K,V> map = new HashMap<>();
}
class ConcurrentHashMap<K,V> {
final Segment<K,V>[] segments;
public V put(K key, V value) {
int segmentIndex = key.hashCode() % segments.length;
Segment<K,V> segment = segments[segmentIndex];
segment.lock();
try {
return segment.map.put(key, value);
} finally {
segment.unlock();
}
}
}
5.2 无锁队列实现
基于CAS的无锁队列示例:
java复制class Node<E> {
final E item;
volatile Node<E> next;
}
class ConcurrentLinkedQueue<E> {
private volatile Node<E> head;
private volatile Node<E> tail;
public boolean offer(E e) {
Node<E> newNode = new Node<>(e);
while (true) {
Node<E> t = tail;
Node<E> next = t.next;
if (t == tail) {
if (next == null) {
if (CAS(t.next, null, newNode)) {
CAS(tail, t, newNode);
return true;
}
} else {
CAS(tail, t, next);
}
}
}
}
}
5.3 线程本地存储
ThreadLocal为每个线程创建变量副本,避免共享:
java复制ThreadLocal<SimpleDateFormat> dateFormat =
ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd"));
实现原理:每个Thread对象维护ThreadLocalMap,以ThreadLocal为key存储值。
6. 常见问题排查
6.1 死锁检测与解决
使用jstack检测死锁:
bash复制jstack <pid>
输出示例:
code复制Found one Java-level deadlock:
"Thread-1":
waiting to lock monitor 0x00007f88e4003e58 (object 0x000000076ab270c8, a java.lang.Object),
which is held by "Thread-0"
"Thread-0":
waiting to lock monitor 0x00007f88e4003d58 (object 0x000000076ab270d8, a java.lang.Object),
which is held by "Thread-1"
解决方案:
- 按固定顺序获取锁
- 使用tryLock设置超时
- 减少锁粒度
6.2 锁竞争优化
通过JVisualVM或Arthas观察锁竞争情况。优化策略:
- 减小临界区范围
- 降低锁粒度
- 使用读写锁
- 考虑无锁数据结构
6.3 CAS失败处理
当CAS操作频繁失败时,可以考虑:
- 增加补偿机制(如短暂休眠)
- 改用锁机制
- 重新设计算法减少竞争
7. 最佳实践总结
-
选择原则:
- 简单场景优先使用synchronized
- 需要高级功能时使用ReentrantLock
- 读多写少用读写锁
- 低竞争计数器用CAS
-
性能调优:
- 通过JMH进行基准测试
- 监控锁竞争情况
- 避免在锁内执行耗时操作
-
代码可维护性:
- 为锁添加注释说明保护的对象
- 使用try-finally确保锁释放
- 避免嵌套锁
-
在分布式系统中,这些单机并发控制机制需要结合分布式锁(如Redis、Zookeeper实现)使用,但基本原理相通。
