在Java的世界中,多线程如同一场精密的交响乐。而“锁”,就是指挥家手中的那根指挥棒——它决定了谁先演奏、谁后进入、谁必须等待。
本文将带你走进两种常见的同步机制:普通互斥锁(如 synchronized 和 ReentrantLock) 与 读写分离的读写锁(ReentrantReadWriteLock) ,通过概念对比、代码示例、性能测试和最佳实践,帮助你理解它们的本质区别与适用场景。
掌握锁的使用之道,才能让并发程序运行得更加流畅高效。
在多线程环境中,多个线程可能会同时访问共享资源,例如一个缓存列表、一个计数器或一份配置文件。若不加以控制,就可能导致数据错乱、状态异常等严重问题。
这时,“锁”就派上了用场——它像一把门锁,确保同一时间只有一个人可以操作资源,从而保障数据的一致性和完整性。
Java 提供了多种锁机制:
synchronized
、ReentrantLock
;ReentrantReadWriteLock
;每种锁都有其擅长的舞台。选择合适的锁,就像给不同的乐器安排合适的位置,才能奏出和谐的乐章。
这是一种最基础的同步机制,遵循“排他性”原则:
synchronized
关键字ReentrantLock
private final Lock mutex = new ReentrantLock();
private List<String> sharedList = new ArrayList<>();
public void write(String data) {
mutex.lock();
try {
sharedList.add(data);
} finally {
mutex.unlock();
}
}
public String read(int index) {
mutex.lock();
try {
return sharedList.get(index);
} finally {
mutex.unlock();
}
}
这段代码展示了互斥锁的基本用法,无论是写入还是读取,都必须获取锁,导致读操作也被阻塞。
这种设计使得读多写少的场景下效率大幅提升。
ReentrantReadWriteLock
private final ReadWriteLock rwLock = new ReentrantReadWriteLock();
private final Lock readLock = rwLock.readLock();
private final Lock writeLock = rwLock.writeLock();
private List<String> sharedList = new ArrayList<>();
public void write(String data) {
writeLock.lock();
try {
sharedList.add(data);
} finally {
writeLock.unlock();
}
}
public String read(int index) {
readLock.lock();
try {
return sharedList.get(index);
} finally {
readLock.unlock();
}
}
这段代码展示了读写锁如何分别控制读与写的访问权限。当没有写操作时,多个线程可同时进行读取,显著提升并发性能。
为了更直观地展示两者的性能差异,我们进行了 JMH 基准测试,模拟 100 线程并发访问共享资源,设定三种典型场景:
读写比例 | 互斥锁吞吐量(ops/sec) | 读写锁吞吐量(ops/sec) | 性能提升 |
---|---|---|---|
9:1 | 54,231 | 187,629 | ~246% |
5:5 | 82,145 | 95,312 | ~16% |
1:9 | 78,321 | 62,419 | -20% |
这些数字背后藏着怎样的故事?
因此,选择锁类型不能一概而论,而是要看实际业务场景是否匹配其特性。
这是读写锁的一个强大功能。写锁释放前,你可以将其降级为读锁,保证后续读操作的可见性。
public void upgradeExample() {
writeLock.lock();
try {
// 写操作...
readLock.lock(); // 降级为读锁
try {
writeLock.unlock(); // 释放写锁
// 继续读取...
} finally {
readLock.unlock();
}
} finally {
if (writeLock.isHeldByCurrentThread()) {
writeLock.unlock();
}
}
}
这种方式避免了在写完之后重新获取读锁时可能出现的并发问题。
读写锁不支持直接从读锁升级为写锁。如果你尝试这样做,很可能会陷入死锁。
public void wrongUpgrade() {
readLock.lock();
try {
writeLock.lock(); // ❗ 死锁风险!
try {
// ...
} finally {
writeLock.unlock();
}
} finally {
readLock.unlock();
}
}
这是因为当前线程持有读锁时,试图获取写锁会失败,除非你先释放读锁。正确的做法是:先获取写锁,再降级为读锁。
锁的饥饿问题,是指某些线程长时间无法获取锁,导致任务堆积甚至崩溃。
解决方案很简单:
private final ReadWriteLock rwLock = new ReentrantReadWriteLock(true); // 开启公平模式
公平模式下,锁按照请求顺序分配,虽牺牲部分性能,却能有效防止饥饿现象。
选择锁就像是选车:跑高速当然要选快车,堵车时反而是小巧灵活的自行车更合适。
以下是一些实用建议:
ReentrantReadWriteLock
,提升并发读性能;ReentrantLock
,减少锁状态切换开销;synchronized
或 ReentrantLock
,避免读写锁带来的并发隐患;StampedLock
,进一步提升乐观读的性能。除了合理选择锁之外,还可以通过一些策略来提升整体并发性能:
对大型集合进行分区加锁,比如 ConcurrentHashMap
的实现思路。
将读操作和写操作分发到不同的服务实例或线程池,降低锁竞争概率。
对于写操作敏感的场景,可以采用异步方式提交写请求,立即返回结果,延迟处理变更。
这些方法结合锁的使用,能让并发程序在高负载下依然保持稳定表现。
场景 | 推荐锁类型 | 理由说明 |
---|---|---|
缓存系统(读多写少) | ReentrantReadWriteLock | 并发读性能提升显著 |
计数器更新(写操作频繁) | ReentrantLock | 读写锁状态管理开销反而降低性能 |
强一致性要求的金融系统 | synchronized/ReentrantLock | 避免读写锁的并发读带来的一致性问题 |
配置中心(读操作占绝对主导) | StampedLock | 支持乐观读,进一步提升无竞争读的性能 |
最后提醒一句:不要盲目追求锁的复杂度,而应根据实际业务特点选择最适合的方案。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。