【读写锁】

一、读写锁介绍

读写锁ReadWriteLock,顾名思义一把锁分为读与写两部分,读锁允许多个线程同时获得,因为读操
作本身是线程安全的。而写锁是互斥锁,不允许多个线程同时获得写锁。并且读与写操作也是互斥
的。读写锁适合多读少写的业务场景。

二、.ReentrantReadWriteLock介绍

针对这种场景,JAVA的并发包提供了读写锁ReentrantReadWriteLock,它内部,维护了一对相
关的锁,一个用于只读操作,称为读锁;一个用于写入操作,称为写锁,描述如下:
线程进入读锁的前提条件:
没有其他线程的写锁
没有写请求或者有写请求,但调用线程和持有锁的线程是同一个。
线程进入写锁的前提条件:

  • 没有其他线程的读锁
  • 没有其他线程的写锁
    而读写锁有以下三个重要的特性:
  • 公平选择性:支持非公平(默认)和公平的锁获取方式,吞吐量还是非公平优于公平。
  • 可重入:读锁和写锁都支持线程重入。以读写线程为例:读线程获取读锁后,能够再次获取读锁。写线程在获取写锁之后能够再次获取写锁,同时也可以获取读锁。
  • 锁降级:遵循获取写锁、再获取读锁最后释放写锁的次序,写锁能够降级成为读锁

1.ReentrantReadWriteLock的使用

读写锁接口ReadWriteLock
一对方法,分别获得读锁和写锁 Lock 对象。
ReentrantReadWriteLock类结构
ReentrantReadWriteLock是可重入的读写锁实现类。在它内部,维护了一对相关的锁,一个用于
只读操作,另一个用于写入操作。只要没有 Writer 线程,读锁可以由多个 Reader 线程同时持有。也
就是说,写锁是独占的,读锁是共享的。

注意事项:

  • 读锁不支持条件变量
  • 重入时升级不支持:持有读锁的情况下去获取写锁,会导致获取永久等待
  • 重入时支持降级: 持有写锁的情况下可以去获取读锁

2.应用场景

以下是使用ReentrantReadWriteLock的常见场景:

  • 读多写少:ReentrantReadWriteLock适用于读操作比写操作频繁的场景,因为它允许多个读线程同时访问共享
    数据,而写操作是独占的。
  • 缓存:ReentrantReadWriteLock可以用于实现缓存,因为它可以有效地处理大量的读操作,同时保护缓存数据
    的一致性。

3锁降级

锁降级指的是写锁降级成为读锁。如果当前线程拥有写锁,然后将其释放,最后再获取读锁,这种
分段完成的过程不能称之为锁降级。锁降级是指把持住(当前拥有的)写锁,再获取到读锁,随后释
放(先前拥有的)写锁的过程。锁降级可以帮助我们拿到当前线程修改后的结果而不被其他线程所破
坏,防止更新丢失。
锁降级的使用示例
因为数据不常变化,所以多个线程可以并发地进行数据处理,当数据变更后,如果当前线程感知到数
据变化,则进行数据的准备工作,同时其他处理线程被阻塞,直到当前线程完成数据的准备工作。

锁降级中读锁的获取是否必要呢?答案是必要的。主要是为了保证数据的可见性,如果当前线程不
获取读锁而是直接释放写锁,假设此刻另一个线程(记作线程T)获取了写锁并修改了数据,那么当前
线程无法感知线程T的数据更新。如果当前线程获取读锁,即遵循锁降级的步骤,则线程T将会被阻
塞,直到当前线程使用数据并释放读锁之后,线程T才能获取写锁进行数据更新。
RentrantReadWriteLock不支持锁升级(把持读锁、获取写锁,最后释放读锁的过程)。目的也
是保证数据可见性,如果读锁已被多个线程获取,其中任意线程成功获取了写锁并更新了数据,则其
更新对其他获取到读锁的线程是不可见的。

4读写锁设计思路

读写状态的设计
设计的精髓:用一个变量如何维护多种状态
在 ReentrantLock 中,使用 Sync ( 实际是 AQS )的 int 类型的 state 来表示同步状态,表示锁被一个线程重复获取
的次数。但是,读写锁 ReentrantReadWriteLock 内部维护着一对读写锁,如果要用一个变量维护多种状态,需要
采用“按位切割使用”的方式来维护这个变量,将其切分为两部分:高16为表示读,低16为表示写。
分割之后,读写锁是如何迅速确定读锁和写锁的状态呢?通过位运算。假如当前同步状态为S,那么:
写状态,等于 S & 0x0000FFFF(将高 16 位全部抹去)。 当写状态加1,等于S+1.
读状态,等于 S >>> 16 (无符号补 0 右移 16 位)。当读状态加1,等于S+(1<<16)
根据状态的划分能得出一个推论:S不等于0时,当写状态(S&0x0000FFFF)等于0时,则读状态(S>>>16)大于
0,即读锁已被获取。
代码实现:java.util.concurrent.locks.ReentrantReadWriteLock.Sync
exclusiveCount(int c) 静态方法,获得持有写状态的锁的次数。
sharedCount(int c) 静态方法,获得持有读状态的锁的数量。不同于写锁,读锁可以同时被多个线程持有。而每
个线程持有的读锁支持重入的特性,所以需要对每个线程持有的读锁的数量单独计数,这就需要用到
HoldCounter 计数器
HoldCounter 计数器
读锁的内在机制其实就是一个共享锁。一次共享锁的操作就相当于对HoldCounter 计数器的操
作。获取共享锁,则该计数器 + 1,释放共享锁,该计数器 - 1。只有当线程获取共享锁后才能对共享
锁进行释放、重入操作。
通过 ThreadLocalHoldCounter 类,HoldCounter 与线程进行绑定。HoldCounter 是绑定线程
的一个计数器,而 ThreadLocalHoldCounter 则是线程绑定的 ThreadLocal。
HoldCounter是用来记录读锁重入数的对象
ThreadLocalHoldCounter是ThreadLocal变量,用来存放不是第一个获取读锁的线程的其他线程的读锁重入数对象。

三、 StampedLock介绍

如果我们深入分析ReentrantReadWriteLock,会发现它有个潜在的问题:如果有线程正在读,写
线程需要等待读线程释放锁后才能获取写锁,即读的过程中不允许写,这是一种悲观的读锁。
为了进一步提升并发执行效率,Java 8引入了新的读写锁:StampedLock。
StampedLock和ReentrantReadWriteLock相比,改进之处在于:读的过程中也允许获取写锁后
写入!在原先读写锁的基础上新增了一种叫乐观读(Optimistic Reading)的模式。该模式并不会加
锁,所以不会阻塞线程,会有更高的吞吐量和更高的性能。
它的设计初衷是作为一个内部工具类,用于开发其他线程安全的组件,提升系统性能,并且编程模
型也比ReentrantReadWriteLock 复杂,所以用不好就很容易出现死锁或者线程安全等莫名其妙的问
题。

1 StampedLock的使用

StampLock三种访问模式

  • Writing(独占写锁):writeLock 方法会使线程阻塞等待独占访问,可类比ReentrantReadWriteLock 的写锁
    模式,同一时刻有且只有一个写线程获取锁资源;
  • Reading(悲观读锁):readLock方法,允许多个线程同时获取悲观读锁,悲观读锁与独占写锁互斥,与乐观读
    共享。
  • Optimistic Reading(乐观读):这里需要注意了,乐观读并没有加锁,也就是不会有 CAS 机制并且没有阻塞
    线程。仅当当前未处于 Writing 模式 tryOptimisticRead 才会返回非 0 的邮戳(Stamp),如果在获取乐观读
    之后没有出现写模式线程获取锁,则在方法validate返回 true ,允许多个线程获取乐观读以及读锁,同时允许一个写线程获取写锁。

2.使用场景和注意事项

对于读多写少的高并发场景 StampedLock的性能很好,通过乐观读模式很好的解决了写线程“饥
饿”的问题,我们可以使用StampedLock 来代替ReentrantReadWriteLock ,但是需要注意的
是 StampedLock 的功能仅仅是 ReadWriteLock 的子集,在使用的时候,还是有几个地方需要注意一
下。
StampedLock 写锁是不可重入的,如果当前线程已经获取了写锁,再次重复获取的话就会死锁,使用过程中一
定要注意;
悲观读、写锁都不支持条件变量 Conditon ,当需要这个特性的时候需要注意;
如果线程阻塞在 StampedLock 的 readLock() 或者 writeLock() 上时,此时调用该阻塞线程的 interrupt() 方
法,会导致 CPU 飙升。所以,使用 StampedLock 一定不要调用中断操作,如果需要支持中断功能,一定使用
可中断的悲观读锁 readLockInterruptibly() 和写锁 writeLockInterruptibly()。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值