栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > Java

Java并发(十一):读写锁,Java面试必备的集合源码详解

Java 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

Java并发(十一):读写锁,Java面试必备的集合源码详解

特性
  • ReentrantReadWriteLock支持非公平和公平的获取锁方式,吞吐量依然是非公平优于公平,并且默认的方式是非公平

  • ReentrantReadWriteLock支持可重入,当读线程获取了读锁之后,其能继续去获得读锁;当写线程获得了写锁之后,其能继续去获得写锁,也能去获得读锁(写线程能自己读,但其他线程不能读,也不能写)

  • 支持锁降级,当获取了写锁、再获取读锁,之后释放了写锁之后,会自动降级为读锁

使用方法

使用的方法也比较简单,对于读写锁都是

  • 创建一个ReentrantReadWriteLock

  • 利用ReentrantReadWriteLock来获取读锁或者写锁

  • 调用读锁或者写锁的lock方法进行上锁

  • 调用读锁或者写锁的unlock方法进行解锁

读写锁的实现分析

之前学习ReentrantLock的时候认识了同步状态这个概念,同步状态就是指锁被一个线程重复获取的次数,在ReentrantLock就是state这个变量

但对于读写锁来说,不仅需要维护同步状态,还需要去维护多个读线程和一个写线程的状态(写线程只可能有一个,一旦出现写线程,其余读线程全部停止)

同步状态是一个整形变量,而一个整形变量上去维护多种状态,就需要按位切割使用这个变量,而去维护这个变量是关键所在,总的来说就是怎样使用一个变量来区分读写锁的状态,为什么不使用多个变量去记录两种锁的同步状态呢?

读写锁对于该整形变量进行了按位切割使用,高16位表示读而低16位表示写

如何使用位拆分去表示两种锁的重入状态呢?

对于写锁的低16位,只要采用与运算,与0x0000FFFF进行与运算,就可以把高16位给去掉了,然后对于重入次数只要正常加减即可

对于读锁的高16位,不能像写锁一样简单地使用与运算就可以算出,因为简单的与运算只能保留高16位,但重入的次数不能通过简单的加减来得到了,不过其实也差不多,采用的是无符号右移16位的做法,因为是一个正数,所以只要无符号右移16位就可以将高16位移去低16位,并且高16位会变成全为0,然后重入的时候再进行同样的加减即可

如何判断读锁和写锁获取

当低16位不为0的时候,即代表读锁被获取,当高16位不为0的时候,即代表写锁去获取,但去判断写锁的时候,要先去移位才可以判断

写锁


写锁的使用如下

public void test(){

ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock();

ReentrantReadWriteLock.WriteLock writeLock = reentrantReadWriteLock.writeLock();

writeLock.lock();

writeLock.unlock();

}

从构造方法上看

可以看到,其调用的是另外一个构造方法,并且fair变量设为false,这也证明了前面提到的,默认的读写锁实现是一个非公平锁

public ReentrantReadWriteLock(boolean fair) {

sync = fair ? new FairSync() : new NonfairSync();

readerLock = new ReadLock(this);

writerLock = new WriteLock(this);

}

可以看到

  • 公平锁的实现由sync成员变量来决定

  • 构造方法直接实例化了ReadLock和WriteLock

  • 并且,读写锁的获取其实就是直接返回构造实例化的ReadLock和WriteLock

  • 并且,ReentrantReadWriteLock里面的内部类有5哥

  • ReadLock:读锁

  • WriteLock:写锁

  • Sync:抽象的读写锁的同步功能实现(线程安全的功能主要由Sync来保证)

  • FariSync:公平的同步功能

  • UnFairSync:不公平的同步功能

  • 并且Sync是否公平,取决于构造方法,默认是不公平的

写锁的获取 lock方法

写锁的获取是直接调用WriteLock的lock方法

![在这里插入图片描述](https://img-blog.csdnimg.cn/3d421f404dd94cb9a41d516b7127d110.png?x-oss-process=image/watermark

【一线大厂Java面试题解析+后端开发学习笔记+最新架构讲解视频+实战项目源码讲义】

开源分享完整内容戳这里

,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBAR0RVVF9FbWJlcg==,size_20,color_FFFFFF,t_70,g_se,x_16#pic_center)

从代码上可以看到,写锁的获取直接依赖于Sync同步类来实现

再深入,发现其底层实现就是AQS,可以看到很多锁的底层实现都是AQS,下面就来看看这段代码是干什么的

public final void acquire(int arg) {

//尝试去获取锁

//如果获取锁失败就去进行入队等待

if (!tryAcquire(arg) &&

acquireQueued(addWaiter(Node.EXCLUSIVE), arg))

//如果获取锁失败,并且入队成功

//将自己设为中断状态???

selfInterrupt();

}

tryAcquire

这个方法就是去获取锁,可以看到进来

其由ReentrantReadWriteLock去实现的

protected final boolean tryAcquire(int acquires) {

//从注释上我们可以看到三种情况

//1.如果有人获取了读锁或者有人获取了写锁,那就获取锁失败

//2.如果计数达到饱和也会停止,也就是可重入次数达到上线了

//3.其余状态都可以进行获取锁了

//获取当前线程

Thread current = Thread.currentThread();

//获取state变量,也就是获取同步状态

int c = getState();

//计算wrieteLock的同步状态

int w = exclusiveCount©;

//如果state不为0,代表有其他线程获取读锁或者写锁

if (c != 0) {

// (Note: if c != 0 and w == 0 then shared count != 0)

//如果w为0.代表有人获取了读锁,return false

//如果w不为0,但当前线程并不是已经获取了写锁的线程,return false

//以上两种情况都不能获取锁了,因为读锁有人获取了,或者没人获取读锁,但写锁的拥有者不是自己

if (w == 0 || current != getExclusiveOwnerThread())

return false;

//如果w不为0,且写锁的拥有者是自己,那就代表发生重入锁了

//改变writeLock的同步状态

if (w + exclusiveCount(acquires) > MAX_COUNT)

//如果超过了最大值

//最大值为16左移一位并且减一(刚好6位)

//抛出异常

throw new Error(“Maximum lock count exceeded”);

// Reentrant acquire

//重新设置同步状态

setState(c + acquires);

//返回true代表加锁成功

return true;

}

//如果状态栏为0

//判断写线程是否需要阻塞

//如果不需要阻塞,使用CAS来重新设置同步状态

if (writerShouldBlock() ||

!compareAndSetState(c, c + acquires))

return false;

//不需要阻塞且CAS重新设置同步状态成功

//就将当前写锁的拥有线程设为自己!!!

//相当于贴上自己的标签

setExclusiveOwnerThread(current);

//返回true

return true;

}

整个加锁的过程很简单

  • 获取当前线程

  • 获取当前锁的同步状态

  • 如果同步状态不为0,代表写锁或者读锁被获取了

  • 判断是不是被人获取了写锁

  • 如果没人获取写锁,那就代表读锁被获取了,返回false,加锁失败

  • 如果有人获取写锁,判断获取写锁的是不是当前线程,如果不是,也返回false,代表加锁失败(这里是比较巧妙的,重要的是判断当前线程有没有获取写锁,如果获取了,那就代表是重入情况而已,如果其他线程获取了写锁,更加不需要考虑了)

  • 如果当前线程已经获取了写锁,那就代表是可重入情况

  • 修改写锁的同步状态

  • 如果修改写锁之后的同步状态没有大于最大值(2^6-1)

  • 加锁成功

  • 如果同步状态为0,代表没有线程获取锁,但不排除当前会存在竞争情况

  • 判断写操作是否需要进行阻塞

  • 如果不需要进行阻塞,使用CAS去更换写锁的状态量,此时如果发生竞争,谁CAS成功就谁获取锁成功

  • CAS更换同步状态量成功后,就将写锁的拥有者改为自己,相当于贴上自己的标签!!!

getExclusiveOwnerThread

可以看到,这个变量来自AbstractOwnableSynchronizer的,专门用来存储不需要进行同步的线程

exclusive

从上面代码可以看到,这个方法是用来重新计算写锁的状态量的

因为写锁规定了,重入次数不能超过2 ^ 6,最大值就是2 ^ 6-1,而且,写锁的同步状态量是位于前16位的,所以要进行与运算来取出前6位

转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/531808.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号