博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
设计:读写锁
阅读量:3959 次
发布时间:2019-05-24

本文共 4642 字,大约阅读时间需要 15 分钟。

文章目录

分析reentrantReadWriteLock

读写锁具有读写分离的思想,读锁是一个共享锁,而写锁是一个互斥锁。线程如果要读某个资源那么便对该资源加读锁,一个资源可以被多个线程加读锁,而一个线程如果需要对某个资源进行写操作便需要加写锁,如果该资源上已经被其他线程加入了写锁,便不可以被加读锁。

加锁本质上就是修改一个变量,其他线程读取一个被加锁的共享资源时需要先查看锁变量释放被人占用。加锁成功就是成功对变量进行了相应修改(置位或者自增),而加锁失败则会自旋或阻塞,直到锁变量被重置/复位/释放。读锁写锁的释放,说白了就是相应的读状态/写状态自增自减罢了。

这里讨论的是一个抽象的读写锁实现思想,不同的产品有具体的实现细节(如mysql锁、java锁等),一般情况下,单个线程对某个资源加上写锁,那么它便宣称了对资源的独占,因此写写、写读重入都是允许的,而某个线程仅是对资源加了读锁,那么无法做到读写重入,写操作 将被阻塞。

互斥锁中,我们没有获得锁就不能读或写变量,获取锁失败的线程会被阻塞。而现在引入读写锁后,访问变量的操作被细分为读和写,A线程和B线程都是读操作,因此它们可以获得共享锁后一起访问变量,而A线程和B线程都是写操作(至少有一个写操作),则重新退化为互斥锁。其中不变的是,访问变量必须先获得锁,没有获得锁的变量被阻塞在同步代码之外,只不过获得的锁可以分为共享锁和互斥锁

java中读写锁的实现就是ReentrantReadWriteLock,并且实现了ReadWriteLock接口、内部通过成员Syn依赖AQS框架,两个内部类WriteLock和readLock分别实现了实现了Lock接口,而ReadWriteLock接口对应的readLock和writeLock方法返回的就是两个内部类WriteLock和readLock的实例对象。

public ReentrantReadWriteLock.WriteLock writeLock() {
return writerLock; } public ReentrantReadWriteLock.ReadLock readLock() {
return readerLock; }

而ReentrantReadWriteLock的锁变量使用的仍然是AQS维护的同步状态state,这个state是int类型,高16位用于表示读状态,低16位用于表示写状态。其中是否处于上锁状态直接看相应的低位即可(通过掩码和移位操作),而更高的位代表重入次数。

其中读状态是所有线程获取读锁次数的总和,每个线程获取读锁的次数存在于threadLocal中,而写状态记录的是owner的重人次数,其中写锁获得者通过变量标识,读写锁能够被分别获取的次数不超过2的16次方。

写操作需要被其他线程的读操作可见,而存在读操作时不可以获取写锁,这本质上就是为了避免读写冲突。(mysql基于MVCC可以实现非锁定读并避免读写冲突,而ReentrantReadWriteLock中的读写都是同步的,因此读写操作不能同时进行)

ReentrantReadWriteLock支持锁降级,如果有一段代码,先是少量的写然后是大量的读,那么写锁的释放就不必等待整个代码全部执行完毕,而是执行完写的代码后就可以释放写锁——当前线程先保持写锁,然后拿到读锁,释放写锁。(它和普通的申请锁操作不同的是,它可以在不释放写锁的前提下,申请读锁)

ReentrantReadWriteLock不支持锁升级——先拿到读再拿到写,再放掉读。因为锁升级存在死锁风险:A和B如果都想锁升级,那么他们就必须等待对方的读锁释放,但是它们自己又不主动释放读锁,这满足死锁条件——线程持有互斥的写锁,不主动放弃,等待对方释放读锁,并且当前线程与对方线程处于循环等待的状态

这导致两个线程进行僵死状态而不能向下推进。(我们只能不持有任何锁的前提下,去申请写锁)

设计读写锁

面试中设计读写锁的题目时常有,但是通常要求不算严格,重要的是体现读写分离以及锁分离的思想,即怎么对一把锁进行封装,达成两种不同类型锁的效果。

synchronized实现

之前说过,锁本质上就是变量,这一点在原生实现上体现的更是深刻。

ReadWriteLock lock = new ReadWriteLock();        lock.lockWrite();        //访问共享变量        lock.unlockWrite();

我们使用两个变量分别去记录读写状态,基于synchronized相当于对synchronized对应monitor进一步封装,其实底层使用的仍然是同一个全局的monitor,但是我们再高层将monitor细分为读锁和写锁,注意我们这里使用synchronized实现锁对象,即读锁和写锁底层仍然对应的是同一个monitor,因此基于synchronized实现读写锁更多关注点是线程通信,基于线程通信实现上锁失败的阻塞状态,以及通过修改变量实现上锁和解锁的效果。

上锁是为了同步,那么上锁本身也必须进行同步,封装之前monitor保证同步代码被线程互斥执行,而封装之后monitor保证读锁和写锁能够被同步的申请和释放,而保证用户代码同步执行的任务转移给了封装之后的读写锁。

private int reader;    private int write;    private int writeRequests;//写请求可以防止饥饿事件发生

为了防止写锁饥饿,我们增加一个变量用于“写请求预约”操作。

其中变量没有使用volatile修饰,因为synchronized可以保证可见性

public synchronized void lockRead() throws InterruptedException {
while (write > 0 || writeRequests > 0) {
wait(); } reader++; }

加读锁时,如果存在写锁或者锁请求就阻塞,如果能够退出循环说明上锁成功,上锁成功后读状态自增

public synchronized void unlockRead() {
reader--; notifyAll(); }

解锁完成需要唤醒等待的请求写锁的线程。

public synchronized void lockWrite() throws InterruptedException {
writeRequests++; while (reader > 0 || write > 0) {
wait(); } writeRequests--; write++; }

如果当前存在读锁或者写锁,则阻塞。被唤醒后如果退出循环则上锁成功,对应对写请求自减已经写状态自增

public synchronized void unlockWrite() {
write--; notifyAll(); }

基于reentrantLock也可以实现,不过将上面的synchronized块替换为对应的lock对象的lock和unlock操作,其中线程通信使用condition对象的await和signal替换,其中可以创建两个condition对象readConditon和writeCondition实现精确唤醒。其中lock和unlock包围的块也可以保证可见性。

注意,上面的实现是不可重入的(仅读读可重入)。

借助AQS框架

模仿java读写锁,将state变量分为读状态和写状态。

缺陷说明:
仅实现读读重入,其他重入将会造成死锁
不提供写请求变量,可能会造成死锁
reentrantReadWrite可重入读写锁将state一分为2(高低16位),有充足的空间保存重入次数(但不是无限重入的),本实现写状态只使用低一位(即01表示写状态),state>>1 可以获得读锁状态。

继承AQS并且重写钩子方法

获取互斥锁操作:

protected boolean tryAcquire(int arg) {
int old = getState(); if(old==0){
//写状态仅使用低一位,因此这里的写锁是不可重入的 if(compareAndSetState(0,1)){
setExclusiveOwnerThread(Thread.currentThread()); return true; } } return false; }

释放互斥锁操作:

protected boolean tryRelease(int arg) {
if(getState()==1){
setExclusiveOwnerThread(null); setState(0); return true; } throw new IllegalMonitorStateException(); }

写放共享锁操作:

protected int tryAcquireShared(int arg) {
while ((getState()&1)==0){
int old = getState(); //有可能多个线程同时获取读锁 if(compareAndSetState(old,old+(1<<1))){
return 1; } } return -1; }

释放共享锁操作:

protected boolean tryReleaseShared(int arg) {
while (true){
int cur = getState(); if((cur>>1)<=0)return true; //有可能多个线程同时释放锁,自旋CAS保证锁被成功释放 if(compareAndSetState(cur,cur+(1<<1))){
return true; } } }

这里仅重写相应方法,就不提供具体内部类成员以及具体实现相应接口了。

转载地址:http://dttzi.baihongyu.com/

你可能感兴趣的文章
hdu——1358Period(kmp专练)
查看>>
hust——1010F - The Minimum Length(kmp专练)
查看>>
poj——2406Power Strings(kmp专练)
查看>>
poj——2752Seek the Name, Seek the Fame(kmp专练 找出前后相同的字串)
查看>>
校赛 选修课网址 1096: Is The Same?(kmp或者find)
查看>>
选修课网址 1088: The Owl and the Fox
查看>>
校赛 选修课网址 1097: Meeting
查看>>
hdu——2084数塔
查看>>
hdu——1010Tempter of the Bone
查看>>
hdu——1062Text Reverse(反转函数reverse)
查看>>
hdu——1061Rightmost Digit(快速幂)
查看>>
无向图最短路径dijkstra算法
查看>>
hdu 1284钱币兑换问题(dp)
查看>>
hdu 1028Ignatius and the Princess III(dp)
查看>>
hdu 1398Square Coins(dp或者母函数)
查看>>
hdu 2069Coin Change(dp)
查看>>
hdu 1159Common Subsequence(dp 最大不连续的子序列)
查看>>
hdu 1003Max Sum(dp)
查看>>
hdu 1874畅通工程续(dijkstra算法)
查看>>
hdu 1231最大连续子序列
查看>>