JUC读写锁
文章目录
- 一、读写锁概述
- 1.1 核心目标
- 1.2 核心思想
- 1.3 关键规则与保证
- 1.4 核心组件
- 二、使用示例
- 2.1 采用独占锁的姿势读、写数据
- 2.2 使用读写锁读、写数据
- 2.3 锁降级 **(Lock Downgrading)**
- 三、应用场景
- 3.1 缓存系统【高频读、低频更新】
- 3.2 配置中心【配置读取多、更新少】
- 3.3 金融交易系统【账户查询多、转账少】
- 3.4 实时数据看板【数据读取多、更新少】
- 四、场景分析图表
- 4.1 读写锁在缓存系统的工作流程
- 4.2 锁降级过程图解
- 4.3 不同场景下锁的选择
- 五、最佳实践与注意事项
- 5.1 锁选择原则
- 5.2 避免常见陷阱
- 5.3 性能调优技巧
- 5.4 替代解决方案
- 六、典型应用场景总结
- 七、没有了
一、读写锁概述
Java 中读写锁(ReadWriteLock
)的核心概念。读写锁是 Java 并发包(java.util.concurrent.locks
)中解决特定并发场景下性能问题的重要工具。
1.1 核心目标
提高【读多写少】场景下的并发性能
想象一个共享资源(比如一个配置字典、一个缓存或一个大型数据集):
- 读操作(
read
):通常不会修改数据,只是获取数据。多个线程同时读取同一个数据通常是安全的。 - 写操作(
write
):会修改数据。为了保证数据一致性,写操作通常需要独占访问资源。即写操作进行时,不允许其他任何线程(读或写)访问资源。
传统的互斥锁(如 synchronized
或 ReentrantLock
)在访问共享资源时,无论读写,都只允许一个线程访问。这在“读多写少”的场景下会造成巨大的性能瓶颈:大量只读线程被强制串行执行,即使它们之间本可以安全地并发读取
1.2 核心思想
读写锁的核心思想:分离读锁和写锁
读写锁巧妙地解决了这个问题,它维护了一对锁
-
读锁(共享锁,
read lock
)- 共享性:多个线程可以同时持有读锁。
- 目的:允许多个线程并发地读取共享资源,极大地提升读取的吞吐量。
- 约束:当一个(或多个)线程持有读锁时,任何线程都无法获取写锁。这是为了保证读取数据的一致性——防止在读取过程中数据被修改。
-
写锁(独占锁,
write lock
-
排他性:同一时刻只能有一个线程持有写锁。
-
目的:保证写操作的原子性和数据一致性。写操作需要独占访问资源。
-
约束:
- 当一个线程持有写锁时,其他任何线程(无论是想读还是想写)都无法获取读锁或写锁。
- 在获取写锁之前,必须等待所有已持有的读锁释放。同样,在获取读锁之前,必须等待已持有的写锁释放
-
1.3 关键规则与保证
读写锁的行为严格遵循以下规则,这些规则是理解其并发语义的基础:
-
读读共享(Read-Read Sharing):多个线程可以同时获取并持有读锁,进行并发读取操作。这是性能提升的关键。
-
读写互斥(Read-Write Mutual Exclusion):
- 如果一个线程持有读锁,另一个线程尝试获取写锁会被阻塞,直到所有读锁释放。
- 如果一个线程持有写锁,另一个线程尝试获取读锁会被阻塞,直到写锁释放。
-
写写互斥(Write-Write Mutual Exclusion):同一时刻只能有一个线程持有写锁。尝试获取写锁的线程会被阻塞,直到当前写锁释放。
-
可重入性(Reentrancy):
- Java 的标准实现
ReentrantReadWriteLock
支持锁的可重入。 - 一个线程可以重复获取它已经持有的读锁或写锁(需要相应次数的释放)。
- 持有写锁的线程可以再次获取读锁(锁降级的关键)。
- 持有读锁的线程不能直接获取写锁(尝试获取写锁会阻塞,可能导致死锁)。如果需要升级,必须先释放所有读锁,再尝试获取写锁,但这个操作不是原子的,中间状态可能被其他写线程抢占。因此,锁升级通常不被推荐,且标准实现不支持。
- Java 的标准实现
-
公平性(Fairness):
- 类似于
ReentrantLock
,ReentrantReadWriteLock
可以构造为公平锁或非公平锁(默认)。 - 公平锁:线程按照请求锁的顺序(近似 FIFO)获取锁。这有助于避免线程饥饿(如写线程一直被读线程抢占),但可能降低整体吞吐量。
- 非公平锁:允许“插队”。当锁可用时,一个等待线程(无论等待了多久)可能比更早等待的线程优先获得锁。这能提高吞吐量,但可能导致某些线程(尤其是写线程)长时间饥饿。
- 对读写锁的影响:在非公平模式下,一个释放写锁的线程,如果此时有等待的读线程和写线程,读线程通常能更快地集体获得读锁(因为允许多个读),导致等待的写线程可能延迟。公平模式则严格按照队列顺序,写线程有机会更快获得锁,但整体并发读性能可能稍低。
1.4 核心组件
Java 标准库通过 java.util.concurrent.locks.ReentrantReadWriteLock
类提供了读写锁的实现。它是 ReadWriteLock
接口的具体实现。
核心组件:
ReentrantReadWriteLock.ReadLock
: 实现读锁的嵌套类。ReentrantReadWriteLock.WriteLock
: 实现写锁的嵌套类。
-
Sync
(内部抽象类): 继承自AbstractQueuedSynchronizer
(AQS),是实现锁同步机制的核心。ReentrantReadWriteLock
内部维护一个Sync
实例。-
状态表示 (
state
):Sync
使用一个int
类型的state
变量(32位)来同时表示读锁和写锁的状态。- 低 16 位 (0x0000FFFF): 表示写锁的重入次数。
- 高 16 位 (0xFFFF0000): 表示持有读锁的线程数(更精确地说,是每个获取读锁的线程持有的读锁计数之和,因为读锁可重入)。
-
写锁获取: 检查
state
是否为 0(无锁)或低16位不为0且当前线程是写锁持有者(重入)。否则加入等待队列。 -
读锁获取: 检查是否有写锁持有(
state
低16位不为0)且持有者不是当前线程(防止读锁升级死锁)。还要检查读锁数量是否溢出。在非公平模式下,如果队列头是写线程等待,新来的读线程可能被阻塞(避免写线程饥饿);公平模式则严格排队。 -
锁释放: 相应减少
state
的高位(读)或低位(写)计数
-
二、使用示例
2.1 采用独占锁的姿势读、写数据
package cn.tcmeta.rwlock;import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;public class Counter {private int value; // 修改的值/*** 读取操作* @param lock*/public void read(Lock lock){lock.lock();try {System.out.println(Thread.currentThread().getName() + " --> \t" + " 正在读取数据....");try {TimeUnit.MILLISECONDS.sleep(100);System.out.println(Thread.currentThread().getName() + " --> \t" + " 数据读取完成了... 读取到的值是: " + value);}catch (Exception e){e.printStackTrace();}}catch(Exception e){e.printStackTrace();}finally {lock.unlock();}}/*** 写操作* @param lock*/public void write(Lock lock, int newValue){lock.lock();try {System.out.println(Thread.currentThread().getName() + " --> \t" + " 正在修改值 ... ");try {TimeUnit.MILLISECONDS.sleep(200);value = newValue;System.out.println(Thread.currentThread().getName() + " --> \t" + " 修改完成了, 最新值是: " + value);}catch (Exception e){e.printStackTrace();}}catch(Exception e){e.printStackTrace();}finally {lock.unlock();}}
}
测试用例:
package cn.tcmeta.rwlock;import java.util.concurrent.locks.ReentrantLock;/*** @author: laoren* @description: 独占锁测试* @version: 1.0.0*/
public class T1 {public static void main(String[] args) {ReentrantLock reentrantLock = new ReentrantLock();Counter counter = new Counter();// 2个线程,执行写入操作.for (int i = 0; i < 2; i++) {int tmp = i;new Thread(() -> {counter.write(reentrantLock, tmp);}, "write-thread: ").start();}// 18个线程,执行读取操作for (int i = 0; i < 18; i++) {int temp = i;new Thread(() -> {counter.read(reentrantLock);}, "read-thread: ").start();}}
}
2.2 使用读写锁读、写数据
public class T2 {public static void main(String[] args) {// 创建资源类对象Counter counter = new Counter();// 使用独占锁的姿势读、写操作ReentrantLock reentrantLock = new ReentrantLock();ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock();// 获取读锁ReentrantReadWriteLock.ReadLock readLock = reentrantReadWriteLock.readLock();// 获取写锁ReentrantReadWriteLock.WriteLock writeLock = reentrantReadWriteLock.writeLock();// 2个线程,执行写入操作.for (int i = 0; i < 2; i++) {int tmp = i;new Thread(() -> {counter.write(writeLock, tmp);}, "write-thread: ").start();}// 18个线程,执行读取操作for (int i = 0; i < 18; i++) {int temp = i;new Thread(() -> {counter.read(readLock);}, "read-thread: ").start();}}
}
经过测试可以发现, 使用读写锁的时候, 因为读是共享的,所以效率较快.写是独享的
2.3 锁降级 (Lock Downgrading)
- 这是读写锁提供的一个安全且有用的特性。
- 指一个线程先获取写锁 -> 再获取读锁 -> 然后释放写锁的过程。
- 目的:在持有写锁修改数据后,不立即释放所有锁,而是先获取读锁(因为持有写锁时获取读锁总是成功的),再释放写锁。这样该线程在后续读取操作时仍然持有读锁(保证了读取自己修改后数据的可见性),同时允许其他读线程并发访问(因为写锁已释放)。
- 关键点:降级过程(获取读锁 -> 释放写锁)在持有写锁的线程内部完成,是原子性的。其他线程无法在降级过程中插入获取写锁,保证了数据在降级后对其他读线程可见时的一致性。
示例流程【伪代码】:
writeLock.lock(); // 1. 获取写锁 (独占)
try {// ... 修改共享数据 ...readLock.lock(); // 2. 在释放写锁前先获取读锁 (锁降级开始,此时写锁未释放,读锁一定成功)
} finally {writeLock.unlock(); // 3. 释放写锁 (锁降级完成,现在只持有读锁)
}
try {// ... 读取刚刚修改的数据 (其他读线程此时也可以并发读取了) ...
} finally {readLock.unlock(); // 4. 释放读锁
}
示例代码:
package cn.tcmeta.rwlock;import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantReadWriteLock;/*** 锁降级*/
public class LockDowngradingDemo {private static final ReentrantReadWriteLock LOCK = new ReentrantReadWriteLock();private static int shareCount; // 共享变量public static void main(String[] args) {Thread writeThread = new Thread(() -> {LOCK.writeLock().lock();try {System.out.println(Thread.currentThread().getName() + " --> \t" + " 开始更新共享变量");shareCount++;try {TimeUnit.MILLISECONDS.sleep(100);} catch (Exception e) {e.printStackTrace();}System.out.println(Thread.currentThread().getName() + " --> \t" + " 更新完成的共享变量的值是: " + shareCount);// 在持有写锁的情况下, 获取读锁LOCK.readLock().lock();} catch (Exception e) {e.printStackTrace();} finally {// 释放写锁LOCK.writeLock().unlock();}// 进行读操作try {System.out.println(Thread.currentThread().getName() + " --> \t" + " 值是: " + shareCount);} finally {LOCK.readLock().unlock();}}, "write-thread:");Thread readThread = new Thread(() -> {LOCK.readLock().lock();try {System.out.println(Thread.currentThread().getName() + " --> \t" + " 当前的值是: " + shareCount);} catch (Exception e) {e.printStackTrace();} finally {LOCK.readLock().unlock();}}, "read-thread:");writeThread.start();try {TimeUnit.MILLISECONDS.sleep(3000);} catch (Exception e) {e.printStackTrace();}readThread.start();}
}
写线程无获取写锁,然后更新共享资源的值,并通过获取读锁实现降级.读线程直接获取读锁进行读操作.
三、应用场景
读写锁在Java并发编程中扮演着重要角色,特别适用于读多写少的场景。
3.1 缓存系统【高频读、低频更新】
package cn.tcmeta.rwlock;import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.locks.*;public class Cache<K, V> {private final Map<K, V> cacheMap = new HashMap<>();private final ReadWriteLock rwLock = new ReentrantReadWriteLock();private final Lock readLock = rwLock.readLock();private final Lock writeLock = rwLock.writeLock();// 获取缓存数据(多个线程可同时读取)public V get(K key) {readLock.lock();try {return cacheMap.get(key);} finally {readLock.unlock();}}// 更新缓存数据(独占写入)public void put(K key, V value) {writeLock.lock();try {cacheMap.put(key, value);} finally {writeLock.unlock();}}// 按需加载(经典读写锁应用)public V getOrLoad(K key, DataLoader<K, V> loader) {V value;// 先尝试读取readLock.lock();try {value = cacheMap.get(key);if (value != null) {return value;}} finally {readLock.unlock();}// 未找到数据,获取写锁加载writeLock.lock();try {// 双重检查(避免重复加载)value = cacheMap.get(key);if (value == null) {value = loader.load(key);cacheMap.put(key, value);}return value;} finally {writeLock.unlock();}}public interface DataLoader<K, V> {V load(K key);}
}
3.2 配置中心【配置读取多、更新少】
package cn.tcmeta.rwlock;import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;public class ConfigCenter {private volatile Map<String, String> config = new HashMap<>();private final ReadWriteLock rwLock = new ReentrantReadWriteLock(true); // 公平锁// 获取配置值public String getConfig(String key) {rwLock.readLock().lock();try {return config.get(key);} finally {rwLock.readLock().unlock();}}// 热更新配置public void updateConfig(Map<String, String> newConfig) {rwLock.writeLock().lock();try {Map<String, String> updated = new HashMap<>(config);updated.putAll(newConfig);config = updated; // volatile保证可见性} finally {rwLock.writeLock().unlock();}}
}
3.3 金融交易系统【账户查询多、转账少】
public class AccountService {private final Map<Long, Account> accounts = new ConcurrentHashMap<>();private final ReadWriteLock rwLock = new ReentrantReadWriteLock();// 查询余额(高并发)public BigDecimal getBalance(long accountId) {rwLock.readLock().lock();try {Account acc = accounts.get(accountId);return acc != null ? acc.getBalance() : BigDecimal.ZERO;} finally {rwLock.readLock().unlock();}}// 转账操作(需要独占)public void transfer(long from, long to, BigDecimal amount) {rwLock.writeLock().lock();try {Account source = accounts.get(from);Account target = accounts.get(to);if (source.getBalance().compareTo(amount) < 0) {throw new InsufficientFundsException();}source.withdraw(amount);target.deposit(amount);} finally {rwLock.writeLock().unlock();}}
}
3.4 实时数据看板【数据读取多、更新少】
public class DashboardData {private volatile DataSnapshot currentSnapshot;private final ReadWriteLock rwLock = new ReentrantReadWriteLock();// 获取当前数据快照public DataSnapshot getCurrentData() {rwLock.readLock().lock();try {return currentSnapshot;} finally {rwLock.readLock().unlock();}}// 更新数据(使用锁降级保证数据一致性)public void updateData(DataProcessor processor) {rwLock.writeLock().lock();try {// 处理数据(独占写权限)DataSnapshot newSnapshot = processor.process(currentSnapshot);// 锁降级开始rwLock.readLock().lock();try {currentSnapshot = newSnapshot;} finally {rwLock.writeLock().unlock(); // 释放写锁,保留读锁}// 此时其他读线程可以访问新数据logChanges(newSnapshot);} finally {rwLock.readLock().unlock();}}
}
四、场景分析图表
4.1 读写锁在缓存系统的工作流程
4.2 锁降级过程图解
4.3 不同场景下锁的选择
五、最佳实践与注意事项
5.1 锁选择原则
- 读写比例 > 10:1:优先考虑读写锁
- 读写比例 < 3:1:使用互斥锁更高效
- 超高频读取:考虑StampedLock的乐观读
5.2 避免常见陷阱
// 错误示例:在读锁保护下修改数据
public void unsafeIncrement() {readLock.lock();try {counter++; // 危险操作!} finally {readLock.unlock();}
}// 正确做法:写锁保护写操作
public void safeIncrement() {writeLock.lock();try {counter++;} finally {writeLock.unlock();}
}
5.3 性能调优技巧
// 1. 使用tryLock避免长时间阻塞
if (writeLock.tryLock(100, TimeUnit.MILLISECONDS)) {try {// 关键操作} finally {writeLock.unlock();}
}// 2. 公平锁防止写线程饥饿
new ReentrantReadWriteLock(true); // 创建公平锁// 3. 监控锁竞争情况
ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
System.out.println("读队列长度: " + lock.getReadLockCount());
System.out.println("写队列长度: " + lock.getQueueLength());
5.4 替代解决方案
场景 | 推荐方案 | 优点 | 缺点 |
---|---|---|---|
读多写少 | ReentrantReadWriteLock | 成熟稳定,支持锁降级 | 写线程可能饥饿 |
超高并发读 | StampedLock | 乐观读性能极高 | API复杂,不可重入 |
读写均衡 | ReentrantLock | 简单高效 | 读操作无法并发 |
数据快照 | CopyOnWriteArrayList | 读完全无锁 | 写开销大,内存占用高 |
六、典型应用场景总结
- 数据检索系统:
- 场景:商品目录、用户资料查询
- 特点:95%读操作,5%数据更新
- 实现:读写锁保护核心数据集合
- 实时监控系统:
- 场景:服务器监控、交易大盘
- 特点:高频读取,周期性数据刷新
- 实现:锁降级保证数据更新一致性
- 多级缓存同步:
- 场景:本地缓存与中央缓存同步
- 特点:本地读为主,偶尔批量更新
- 实现:写锁保护缓存更新过程
- 配置管理系统:
- 场景:微服务配置中心
- 特点:服务启动时密集读取,偶尔热更新
- 实现:读写分离保证高可用性
读写锁在Java并发工具箱中是一个强大的工具,合理使用可以显著提升系统吞吐量。但必须注意其适用边界——在真正的"读多写少"场景中才能发挥最大价值。对于临界区短小的操作,或写操作频繁的场景,传统的互斥锁可能是更简单高效的选择。
七、没有了
学习愉快!
资料内容:
链接: https://pan.baidu.com/s/1_igGW3DT7pGsNJPMoOpV6g 提取码: r35a
如资料失效,请评论区留言或者留邮箱. 🎁