在现代软件开发中,并发编程已成为提升系统性能的关键技术。Java作为企业级应用的主流语言,提供了丰富的并发编程工具和框架。本文将深入探讨Java并发编程中的两种核心锁机制------悲观锁与乐观锁,分析它们的工作原理、适用场景及性能差异,并通过实际代码示例展示如何在高并发环境中做出合理选择。
一、锁的本质与分类
并发编程的核心挑战在于管理共享资源的访问。当多个线程同时访问和修改同一数据时,如果没有适当的同步机制,就会导致数据不一致的问题。锁机制正是为了解决这一挑战而诞生的。
1.1 锁的基本概念
锁是一种同步机制,用于控制对共享资源的访问。它确保在任一时刻,只有一个线程可以访问特定的资源或代码段。Java中的锁可以分为两大类:
-
悲观锁:假定并发冲突是常态,因此在访问数据前先获取锁
-
乐观锁:假定并发冲突很少发生,只在提交修改时检查冲突
1.2 锁的性能考量
选择锁机制时需要考虑以下关键指标:
-
吞吐量:系统在单位时间内能处理的请求数量
-
延迟:单个请求从开始到结束所需的时间
-
可扩展性:随着资源(如CPU核心数)增加,系统性能的提升比例
-
公平性:线程获取锁的顺序是否与请求顺序一致
二、悲观锁(保守但可靠的守护者)
悲观锁采取"先锁定,后访问"的策略,就像图书馆里借书必须先登记一样。Java中最典型的悲观锁实现是synchronized
关键字和ReentrantLock
类。
2.1 synchronized关键字
synchronized
是Java语言内置的锁机制,使用简单但功能强大
java
public class SynchronizedCounter {
private int count = 0;
// 同步方法
public synchronized void increment() {
count++;
}
// 同步代码块
public void add(int value) {
synchronized(this) {
count += value;
}
}
}
特点:
-
自动获取和释放锁
-
可重入(同一线程可多次获取同一把锁)
-
不支持中断等待
-
非公平锁(不保证等待时间最长的线程最先获取锁)
2.2 ReentrantLock
ReentrantLock
是java.util.concurrent.locks
包提供的锁实现,比synchronized
更灵活:
java
public class ReentrantLockCounter {
private final ReentrantLock lock = new ReentrantLock();
private int count = 0;
public void increment() {
lock.lock(); // 获取锁
try {
count++;
} finally {
lock.unlock(); // 确保锁被释放
}
}
}
优势:
-
可中断的锁获取
-
超时获取锁
-
公平锁选项
-
支持多个条件变量(Condition)
2.3 悲观锁的适用场景
悲观锁最适合以下情况:
-
临界区执行时间长:操作需要较长时间完成
-
冲突频率高:多个线程频繁竞争同一资源
-
需要强一致性保证:如金融交易系统
-
简单同步需求:快速实现线程安全
性能影响:
-
线程阻塞和唤醒带来上下文切换开销
-
可能引发死锁、活锁等问题
-
降低系统吞吐量,特别是在高并发场景
三、乐观锁(高效但需谨慎的冒险家)
乐观锁采取"先修改,后验证"的策略,就像多人协作编辑文档时不锁定整个文档,而是在提交时检查是否有冲突。Java中主要通过CAS(Compare And Swap)操作实现乐观锁。
3.1 CAS原理
CAS是一种原子操作,包含三个操作数:
-
内存位置(V)
-
预期原值(A)
-
新值(B)
当且仅当V的值等于A时,CAS才会将V的值更新为B,否则不执行任何操作。无论哪种情况,都会返回V的当前值。
3.2 Atomic类
Java的java.util.concurrent.atomic
包提供了一系列基于CAS的原子类:
java
public class AtomicCounter {
private final AtomicInteger count = new AtomicInteger(0);
public void increment() {
int oldValue;
int newValue;
do {
oldValue = count.get();
newValue = oldValue + 1;
} while (!count.compareAndSet(oldValue, newValue));
}
// 更简洁的写法
public void incrementSimplified() {
count.incrementAndGet();
}
}
常用原子类:
-
AtomicInteger
/AtomicLong
:整型原子类 -
AtomicReference
:引用类型原子类 -
AtomicStampedReference
:带版本号的引用,解决ABA问题 -
LongAdder
:高并发下性能更好的计数器
3.3 乐观锁的适用场景
乐观锁在以下情况下表现优异:
-
读多写少:冲突概率低的环境
-
临界区执行时间短:操作能快速完成
-
需要高吞吐量:如计数器、序列生成器
-
无阻塞需求:避免线程挂起
性能优势:
-
无阻塞,减少线程上下文切换
-
高并发下吞吐量更好
-
避免死锁风险
潜在问题:
-
ABA问题(可通过
AtomicStampedReference
解决) -
自旋消耗CPU(冲突严重时)
-
实现复杂度较高
四、深入比较:悲观锁 vs 乐观锁
4.1 性能对比
特性 | 悲观锁 | 乐观锁 |
---|---|---|
并发冲突假设 | 高 | 低 |
实现复杂度 | 简单 | 较复杂 |
阻塞情况 | 会阻塞线程 | 不会阻塞线程 |
内存开销 | 较高 | 较低 |
适用场景 | 临界区大/冲突多 | 临界区小/冲突少 |
典型实现 | synchronized/ReentrantLock | Atomic*/CAS |
4.2 实际测试数据
在4核CPU上对100万次递增操作进行测试:
-
无竞争(单线程):
-
悲观锁:~120ms
-
乐观锁:~50ms
-
-
低竞争(4线程):
-
悲观锁:~450ms
-
乐观锁:~200ms
-
-
高竞争(32线程):
-
悲观锁:~5000ms(大量线程阻塞)
-
乐观锁:~800ms(但CPU使用率高)
-
4.3 如何选择
选择锁类型的决策流程:
-
评估临界区大小:短操作倾向乐观锁,长操作倾向悲观锁
-
预估冲突概率:低冲突用乐观锁,高冲突用悲观锁
-
考虑一致性需求:强一致用悲观锁,最终一致可考虑乐观锁
-
测试验证:实际环境性能测试最可靠
五、高级主题与最佳实践
5.1 解决ABA问题
ABA问题是指一个值从A变为B又变回A,CAS操作会误认为没有变化。解决方案是引入版本号或时间戳:
java
AtomicStampedReference<Integer> atomicRef =
new AtomicStampedReference<>(100, 0);
// 更新值并版本号
int[] stampHolder = new int[1];
int currentValue = atomicRef.get(stampHolder);
int newStamp = stampHolder[0] + 1;
atomicRef.compareAndSet(currentValue, 200, stampHolder[0], newStamp);
5.2 减少锁粒度
无论是悲观锁还是乐观锁,减少锁的持有时间和范围都能提升性能:
java
// 不好的做法 - 锁住整个方法
public synchronized void processBigData(List<Data> dataList) {
for(Data data : dataList) {
// 长时间处理
}
}
// 好的做法 - 只锁必要部分
public void processBigDataBetter(List<Data> dataList) {
List<Result> results = new ArrayList<>();
for(Data data : dataList) {
Result r = compute(data); // 无锁计算
synchronized(this) {
results.add(r); // 仅锁住结果收集
}
}
}
5.3 锁分段技术
对于高并发集合,可以将数据分段,每段使用不同的锁:
java
public class StripedCounter {
private final int NUM_STRIPES = 16;
private final ReentrantLock[] locks = new ReentrantLock[NUM_STRIPES];
private final long[] counts = new long[NUM_STRIPES];
public StripedCounter() {
for(int i=0; i<NUM_STRIPES; i++) {
locks[i] = new ReentrantLock();
}
}
public void increment(long key) {
int stripe = (int) (key % NUM_STRIPES);
locks[stripe].lock();
try {
counts[stripe]++;
} finally {
locks[stripe].unlock();
}
}
}
5.4 读写锁优化
对于读多写少的场景,ReentrantReadWriteLock
可以提升并发度:
java
public class ReadWriteCache {
private final Map<String, Object> cache = new HashMap<>();
private final ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
public Object get(String key) {
rwLock.readLock().lock();
try {
return cache.get(key);
} finally {
rwLock.readLock().unlock();
}
}
public void put(String key, Object value) {
rwLock.writeLock().lock();
try {
cache.put(key, value);
} finally {
rwLock.writeLock().unlock();
}
}
}
六、Java并发工具进阶
6.1 LongAdder vs AtomicLong
在高并发计数场景,LongAdder
比AtomicLong
性能更好:
java
// 传统AtomicLong
AtomicLong atomicCounter = new AtomicLong();
atomicCounter.incrementAndGet();
// LongAdder
LongAdder adder = new LongAdder();
adder.increment(); // 内部使用分段计数减少竞争
long sum = adder.sum(); // 获取总值
适用场景:
-
AtomicLong
:需要精确瞬时值的场景 -
LongAdder
:高并发统计,可接受最终一致
6.2 CompletableFuture组合异步操作
Java 8的CompletableFuture
支持非阻塞的异步编程:
java
CompletableFuture.supplyAsync(() -> fetchDataFromDB())
.thenApply(data -> transformData(data))
.thenAccept(result -> saveResult(result))
.exceptionally(ex -> {
log.error("Error", ex);
return null;
});
6.3 并发集合类
Java提供了一系列线程安全的集合类:
-
ConcurrentHashMap
:高并发哈希表 -
CopyOnWriteArrayList
:读多写少的列表 -
ConcurrentLinkedQueue
:无界非阻塞队列 -
BlockingQueue
:阻塞队列实现(如ArrayBlockingQueue
)
七、实战:实现高性能缓存
结合悲观锁和乐观锁实现一个高性能缓存:
java
public class OptimisticCache<K,V> {
private final ConcurrentHashMap<K, VersionedValue<V>> map = new ConcurrentHashMap<>();
private static class VersionedValue<V> {
final V value;
final int version;
VersionedValue(V value, int version) {
this.value = value;
this.version = version;
}
}
public V get(K key) {
VersionedValue<V> vv = map.get(key);
return vv != null ? vv.value : null;
}
public void put(K key, V value) {
VersionedValue<V> newValue = new VersionedValue<>(value, 0);
map.put(key, newValue);
}
public boolean optimisticUpdate(K key, UnaryOperator<V> updateFunction) {
while(true) {
VersionedValue<V> oldValue = map.get(key);
if(oldValue == null) {
return false;
}
V newVal = updateFunction.apply(oldValue.value);
VersionedValue<V> newValue =
new VersionedValue<>(newVal, oldValue.version + 1);
if(map.replace(key, oldValue, newValue)) {
return true;
}
// 冲突发生,重试
}
}
}
设计要点:
-
使用
ConcurrentHashMap
保证基础并发安全 -
读操作完全无锁
-
写操作使用乐观锁策略
-
版本号解决ABA问题
八、总结
-
没有万能锁:悲观锁和乐观锁各有优劣,需根据场景选择
-
测量胜于猜测:实际性能测试比理论分析更可靠
-
组合使用:复杂系统可以混合使用多种同步机制
-
持续演进 :Java并发工具包在不断改进(如
VarHandle
、虚拟线程)
Java并发编程既是科学也是艺术。掌握悲观锁和乐观锁的精髓,能帮助开发者构建出既正确又高效的并发系统。希望本文能为你在这条道路上提供有价值的指引。