一、读锁和写锁概念
读锁:
也称共享锁,多个线程可以共享资源,同时读取数据和资源,但是不允许出现写操作。多个线程可以同时持有读锁,提高并发性能,因为不会对数据修改。
写锁:
也称排他锁,只允许一个线程独占地对共享资源进行写操作,其他线程无法同时持有写锁或读锁。写锁的目的是保证在写操作期间没有其他线程对资源进行读或写操作,从而确保数据的一致性。
使用场景:
如果多个线程只需要读取共享资源而不进行修改,那么可以使用读锁来提高并发性能。
如果有线程需要修改共享资源,那么必须使用写锁来保证数据的一致性。
二、ReentrantReadWriteLock
ReentrantReadWriteLock 实现了 ReadWriteLock 接口,可以获取到读锁(共享锁),写锁(独占锁)。同时,通过构造方法可以创建锁本身是公平锁还是非公锁。
默认是非公平锁,性能也更好一点。构造方法传入 true 时为公平锁。
只有读锁与读锁之间是共享的,其他的读锁与写锁,写锁与写锁都是互斥的,不能同时持有。
2.1 线程进入读锁的前提条件:
- 不持有其他线程的写锁
- 没有写请求,或有写请求但调用线程和持有锁的线程是同一个
2.2 线程进入写锁的前提条件:
- 没有其他线程的读锁
- 没有其线程的写锁
2.3 锁升级和锁降级
ReentrantLock 具备可重入的能力,即同一个线程多次获取锁,不引起阻塞
那么 ReentrantReadWriteLock 关于可重入性需要额外知晓两个概念即锁升级和降级
锁升级:读锁变为写锁
锁降级:写锁变为读锁
重入时锁升级不支持:持有读锁的情况下去获取写锁会导致获取写锁永久等待,需要先释放读锁,再去获得写锁
重入时锁降级支持:持有写锁的情况下去获取读锁,造成只有当前线程会持有读锁,因为写锁会互斥其他的锁
2.4 使用介绍
构造方法:
public ReentrantReadWriteLock():默认构造方法,非公平锁
public ReentrantReadWriteLock(boolean fair):true 为公平锁
常用API:
public ReentrantReadWriteLock.ReadLock readLock():返回读锁
public ReentrantReadWriteLock.WriteLock writeLock():返回写锁
lock():加锁
unlock():解锁
tryLock():尝试获取锁
加解锁
ReadWriteLock readWriteLock = new ReentrantReadWriteLock();Lock readLock = readWriteLock.readLock();readLock.lock();try {// ........ 临界区} finally {readLock.unlock();}
锁降级
w.lock();
try {r.lock(); // 降级为读锁, 释放写锁, 这样能够让其它线程读取缓存try {// ...} finally{w.unlock(); // 要在写锁释放之前获取读锁}
} finally{r.unlock();
}
2.5 运用场景
读写锁读多的写少的场景一般为使用缓存的场景。
即读取数据库中的数据到内存或者缓存中间件中,后期获取数据从缓存获取,当数据更新时需要同步更新缓存和数据库。
先清除缓存还是前更新数据库?
先清缓存:可能造成刚清理缓存还没有更新数据库,高并发下,其他线程直接查询了数据库过期数据到缓存中,这种情况非常严重,直接导致后续所有的请求缓存和数据库不一致。
先更新据库:可能造成刚更新数据库,还没清空缓存就有线程从缓存拿到了旧数据,这种情况概率比较小,影响范围有限,只对这一次的查询结果有问题。
实际在缓存中运用读写锁
public class CacheTest {// 缓存对象 这里使用一般map 放于内存Map<String, String> cache = new HashMap<>();// 创建读写锁ReadWriteLock readWriteLock = new ReentrantReadWriteLock();// 从缓存中获取数据 读操作public String getFromCache(String key) {// 加读锁,防止其他线程修改缓存即防止写操作readWriteLock.readLock().lock();try {String value = cache.get(key);// 缓存中存在 即缓存命中if(value != null) {return value;}} finally {// 释放读锁readWriteLock.readLock().unlock();}//如果缓存没有命中,从数据库中加载 加写锁readWriteLock.writeLock().lock();try {// 细节处理,为防止重复查询数据库, 再次验证 因为get 方法上面部分是可能多个线程进来的, 可能已经向缓存填充了数据String value = cache.get(key);if(value == null) {// 从数据库查询value = "从数据库中获取到数据并赋值 这里自行替换业务";cache.put(key, value);}return value;} finally {// 解锁写readWriteLock.writeLock().unlock();}}// 更新数据public void updateData(String key, String value) {// 加写锁readWriteLock.writeLock().lock();try {// 更新操作TODO 更新数据库// 清空缓存cache.remove(key);} finally {// 解锁写readWriteLock.writeLock().unlock();}}
}