java 从零手写实现 ReadWriteLock 读写锁

点赞再看 , 已成习惯 。
序言我们在前面的文章中详细介绍了 jdk 自带的可重入读写锁使用及其源码 。
本节就让我们一起来实现一个读写锁 。
最基础的版本思路我们先实现一个最基础版本的读写锁 , 便于大家理接最核心的部分 。
后续将在这个基础上持续优化 。
java 从零手写实现 ReadWriteLock 读写锁文章插图
思维导图
接口定义为了后续拓展 , 我们统一定义基础的接口 , 一共 4 个方法:
package com.github.houbb.lock.api.core;/** * 读写锁定义接口 * @author binbin.hou * @since 0.0.2 */public interface IReadWriteLock {/*** 获取读锁* @since 0.0.2*/void lockRead();/*** 释放读锁*/void unlockRead();/*** 获取写锁* @since 0.0.2*/void lockWrite();/*** 释放写锁*/void unlockWrite();}类定义/** * 读写锁实现 * * @author binbin.hou * @since 0.0.2 */public class LockReadWrite implements IReadWriteLock {private static final Log log = LogFactory.getLog(LockReadWrite.class);/*** 读次数统计*/private int readCount = 0;/*** 写次数统计*/private int writeCount = 0;}我们这里实现 IReadWriteLock 接口 , LockReadWrite 定义了两个属性 , 用于计算读写的次数 。
获取读锁这里通过 tryLock 获取读锁 , 通过 wait 进入等待 。
如果获取读锁成功 , 则 readCount++ , 这个值主要用于标识是否有读操作 。
/** * 获取读锁,读锁在写锁不存在的时候才能获取 * * @since 0.0.2 */@Overridepublic synchronized void lockRead() {try {// 写锁存在,需要waitwhile (!tryLockRead()) {wait();}readCount++;} catch (InterruptedException e) {Thread.interrupted();// 忽略打断}}/** * 尝试获取读锁 * * @return 是否成功 * @since 0.0.2 */private boolean tryLockRead() {if (writeCount > 0) {log.debug("当前有写锁 , 获取读锁失败");return false;}return true;}tryLockRead 尝试获取读锁 , 读写互斥 , 读读不互斥 , 所以有写锁操作的时候 , 会导致获取读锁失败 。
释放读锁释放读锁的操作非常简单 , 直接 readCount-1 , 然后唤醒所有等待的线程 。
/** * 释放读锁 * * @since 0.0.2 */@Overridepublic synchronized void unlockRead() {readCount--;notifyAll();}获取写锁尝试获取写锁的条件和读写有一些差异:
写操作和读写操作都是互斥的 , 所以当前如果存在其他操作 , 都会获取锁失败 。
/** * 获取写锁 * * @since 0.0.2 */@Overridepublic synchronized void lockWrite() {try {// 写锁存在,需要waitwhile (!tryLockWrite()) {wait();}// 此时已经不存在获取写锁的线程了,因此占坑,防止写锁饥饿writeCount++;} catch (InterruptedException e) {Thread.interrupted();}}/** * 尝试获取写锁 * * @return 是否成功 * @since 0.0.2 */private boolean tryLockWrite() {if (writeCount > 0) {log.debug("当前有其他写锁 , 获取写锁失败");return false;}// 读锁if (readCount > 0) {log.debug("当前有其他读锁 , 获取写锁失败 。 ");return false;}return true;}释放写锁释放写锁的操作也非常简单 , 写操作计数器-1 , 并且唤醒所有的等待线程 。
/** * 释放写锁 * * @since 0.0.2 */@Overridepublic synchronized void unlockWrite() {writeCount--;notifyAll();}思考为什么使用 notifyAll() 而不是 notify()?
要解释这个原因 , 我们可以想象下面一种情形:
如果有线程在等待获取读锁 , 同时又有线程在等待获取写锁 。 如果这时其中一个等待读锁的线程被notify方法唤醒 , 但因为此时仍有请求写锁的线程存在(writeRequests>0) , 所以被唤醒的线程会再次进入阻塞状态 。 然而 , 等待写锁的线程一个也没被唤醒 , 就像什么也没发生过一样(译者注:信号丢失现象) 。 如果用的是notifyAll方法 , 所有的线程都会被唤醒 , 然后判断能否获得其请求的锁 。
用notifyAll还有一个好处 。 如果有多个读线程在等待读锁且没有线程在等待写锁时 , 调用unlockWrite()后 , 所有等待读锁的线程都能立马成功获取读锁 —— 而不是一次只允许一个 。
锁是属于谁的?问题【java 从零手写实现 ReadWriteLock 读写锁】上面的实现是一个最基本的读写锁实现流程 , 但是存在一个很大的问题 , 没有校验释放锁的归属权问题 。
想想你正在带薪 , 忽然一位哥们把你的们直接打开了 , 多尴尬 。
所以我们需要通过 CAS 进行比较是否为预期的线程信息 , 然后才能进行替换和锁的释放等操作 。
java 从零手写实现 ReadWriteLock 读写锁文章插图
火漆
类定义我们重新实现一个可以校验锁归属的实现读写锁实现:
/** * 读写锁实现-保证释放锁时为锁的持有者 * * @author binbin.hou * @since 0.0.2 */public class LockReadWriteOwner implements IReadWriteLock {private static final Log log = LogFactory.getLog(LockReadWriteOwner.class);/*** 如果使用类似 write 的方式 , 会导致读锁只能有一个 。* 调整为使用 HashMap 存放读的信息** @since 0.0.2*/private final Map readCountMap = new HashMap