存在的原因
锁的排他性, 让线程不能同时得到锁, 但是
synchronized,ReentrantLock,无论是读还是写,它们都要求获得相同的锁(排他性)。
在一些场景中,这是没有必要的,多个线程的读操作完全可以并行,在读多写少的场景中,让读操作并行可以明显提高性能。
适用
读多写少, 读的时间长
不然一般不用, 开销大
定义
就是 通过一个ReadWriteLock产生两个锁,
一个读锁,一个写锁。只有"读-读"操作是可以并行的.
ReadWriteLock
public interface ReadWriteLock {
Lock readLock();
Lock writeLock();
}
这个接口是读写锁的抽象,
里面有 readLock()
writeLock()
这2方法, 得到读锁写锁,
返回的锁是Lock
类型的(这是锁接口,ReentrantLock
也实现它)
使用例子:
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class ReadWriteLockUsage {
private final ReadWriteLock rwLock = new ReentrantReadWriteLock();
private final Lock readLock = rwLock.readLock();
private final Lock writeLock = rwLock.writeLock();
// 读线程执行该方法
public void reader() {
readLock.lock(); // 申请读锁
try {
// 在此区域读取共享变量
} finally {
readLock.unlock();// 总是在finally块中释放锁,以免锁泄漏
}
}
// 写线程执行该方法
public void writer() {
writeLock.lock(); // 申请读锁
try {
// 在此区域访问(读、写)共享变量
} finally {
writeLock.unlock();// 总是在finally块中释放锁,以免锁泄漏
}
}
}
-
ReentrantReadWriteLock
是ReadWriteLock
的默认实现 - 从
ReadWriteLock
可得到的类型是Lock
, 看起来就是普通的锁, 是不是实现了读写锁的功能, 看实现者的良心 - 读写锁都是普通
Lock
, 没啥特别的,和ReentrantLock
一样用就行了
锁的降级Downgrade
就是 有在得写锁
的时候, 可继续得读锁
,
写锁锁住, 不释放, 在其临界区内得读锁
这样严格排他的写锁,就可以降级为可共享的读锁了
ReentrantReadWriteLock
这个 读写锁的默认实现, 是个可重入锁
例子:
writeLock.lock(); // 申请写锁
try {
// 对共享数据进行更新
// ...
// 当前线程在持有写锁的情况下申请读锁readLock
readLock.lock();
} finally {
writeLock.unlock();// 释放写锁
}
为什么只能降级不能升级:
内部实现 先得读锁 再得写锁 会死锁
为什么要这么设计?
比如 同一段代码写着
先 readLock.lock();
再writeLock.unlock();
t1,t2,t3 线程都先拿到了读锁, t1去拿写锁要等t2 t3先释放读锁, 他们也在等t1 就死锁了
使用例子: 实现一个缓存类MyCache
public class MyCache {
//数据存这
private Map<String, Object> map = new HashMap<>();
private ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
private Lock readLock = readWriteLock.readLock();
private Lock writeLock = readWriteLock.writeLock();
//读要得读锁
public Object get(String key) {
readLock.lock();
try {
return map.get(key);
} finally {
readLock.unlock();
}
}
//写要写锁
public Object put(String key, Object value) {
writeLock.lock();
try {
return map.put(key, value);
} finally {
writeLock.unlock();
}
}
public void clear() {
writeLock.lock();
try {
map.clear();
} finally {
writeLock.unlock();
}
}
}
唤醒注意
内部,
它们使用同一个整数变量表示锁的状态,16位给读锁用,16位给写锁用,使用一个变量便于进行CAS操作,
锁的等待队列其实也只有一个。
- 都是获取不到锁就入队,
- 释放后, 将等待队列中的第一个线程唤醒,唤醒的可能是等待读锁的,也可能是等待写锁的
读锁获取
只要写锁没有被持有,就可以获取到
获取后,它会检查等待队列,逐个唤醒最前面的等待读锁
的线程,直到第一个等待写锁的线程。
写锁 加锁源码
获取 写锁
1.如果是本来有锁了, 只能是本来有写锁, 就是我 , 我重入
2.是不是需要排队? 这个子类实现 公平非公平锁 不同, 公平锁要前面没等的了才行
3.产生cas拿锁, 如果成功 把本线程记一下
注意: 这里int w = exclusiveCount(c);
AQS同一个state
状态 ReentrantReadWriteLock 分成2部分 又标记写锁又标记读锁,
这里是解析出读锁, 这里倒是自己实现 , 不是用的AQS
写锁 解锁 源码
unparkSuccessor
↑是AQS方法 , 和 ReentrantLock 是一样的
模板方法子类实现↓
可以看到写锁 逻辑 和ReentrantLock 的几乎是一样的
读锁 加锁(共享锁 很复杂)
复杂是因为这是共享锁
tryAcquireShared获取锁
总览 tryAcquireShared这里还和独占锁差不多↑
除了 有读锁了 还是可以降级
和独占锁一样的是 前面没有, 没到重数上限就可以cas试一下
加锁成功后, 比较复杂的是↓ 每个线程都要ThreadLocak各自记下来 加了几重读锁了, first的读锁线程 另外记 因为经常用到
tryAcquireShared
获取锁失败后doAcquireShared 排队
读锁释放
会把排队的叫醒
叫醒直到遇到读锁
确定 和StampedLock 对比
- 非公平锁的情况下, 读多写少, 写的可能永远钱不到锁,永远在排队, 饿死 ,StampedLock 不会 它有乐观读
- 效率低, 读写互斥, StampedLock 不会, 乐观读的同时可以写
网友评论