其他
Java多线程并发读写锁ReadWriteLock实现原理剖析
本文字数:3107字
预计阅读时间:10分钟
关于读写锁
为了提高并发性能我们会额外引入共享锁来与独占锁共同对外构成一个锁,这种就叫读写锁。
为什么叫读写锁呢?主要是因为它的使用考虑了读写场景,一般认为读操作不会改变数据所以可以多线程进行读操作,但写操作会改变数据所以只能一个线程进行写操作。
读写锁在内部维护了一对锁(读锁和写锁),它通过将锁进行分离从而得到更高的并发性能。
如下图中,存在一个读写锁,其内部包含了读锁和写锁两个对象。假如存在五个线程,其中线程一和线程二想要获取读锁,那么两个线程是可以同时获取到读锁的。但是写锁就不可以共享,它是独占锁。
比如线程三、线程四和线程五都想要持有写锁,那么只能一个个线程轮着持有。
读写锁的性质
可以多个线程同时持有读锁,某个线程成功获取读锁后其它线程仍然能成功获取读锁,即使该线程不释放读锁。
在某个线程持有读锁的情况下其它线程不能持有写锁,除非持有读锁的线程全部都释放掉读锁。
在某个线程持有写锁的情况下其它线程不能持有写锁或读锁,某个线程成功获取写锁后其它所有尝试获取读锁和写锁的线程都将进入等待状态,只有当该线程释放写锁后才其它线程能够继续往下执行。
如果我们要获取读锁则需要满足两个条件:目前没有线程持有写锁和目前没有线程请求获取写锁。
如果我们要获取写锁则需要满足两个条件:目前没有线程持有写锁和目前没有线程持有读锁。
简单版本的读写锁
acquireReadLock方法用于获取读锁,如果持有写锁的线程数量或请求读锁的线程数大于0则让线程进入等待状态。
releaseReadLock方法用于释放读锁,将读锁线程数减一并唤醒其它线程。
acquireWriteLock方法用于获取写锁,如果持有读锁的线程数量或持有写锁的线程数量大于0则让线程进入等待状态。
releaseWriteLock方法用于释放写锁,将写锁线程数减一并唤醒其它线程。
读锁升级为写锁
如下图中,线程二已经持有读锁了,而且它是唯一的一个持有读锁的线程,所以它可以成功获得写锁。
写锁降级为读锁
如下图中,线程三持有写锁,此时其它线程不可能持有读锁和写锁,所以可以安全地将写锁降为读锁。
ReentrantReadWriteLock类图
ReentrantReadWriteLock类实现了Serializable接口和ReadWriteLock接口,前者用于序列化,而后者则提供了readLock()和writeLock()两个方法。该类的内部同步器Sync基于AQS同步器实现,即继承了AbstractQueuedSynchronizer类。
同步器分为公平模式和非公平模式,分别对应着FairSync类和NonfairSync类。其中公平/非公平模式表示多个线程同时去获取锁时是否按照先到先得的顺序获得锁,如果是则为公平模式,否则为非公平模式。
该类还包含了读锁和写锁,分别对应ReadLock类和WriteLock类,它们都属于ReentrantReadWriteLock的内部类,且都集成了Lock接口。
ReentrantReadWriteLock实现思想
但是这样设计后当我们要获取读锁和写锁的状态值时则需要一些额外的计算,比如一些移位和逻辑与操作。
sharedCount方法用于获取读锁(高16位)的状态值,左移16位即能得到。exclusiveCount方法用于获取写锁(低16位)的状态值,通过掩码即能得到。
ReadLock与WriteLock
分别表示对读锁和写锁的加锁操作、释放锁操作和创建Condition对象操作,可以看到这些方法都间接调用了同步器Sync的方法,需要注意的是读锁不支持创建Condition对象。
公平/非公平模式
因为ReentrantReadWriteLock的读锁使用了共享模式,而写锁使用了独占模式,所以该父类将不同模式下的公平机制抽象为readerShouldBlock和writerShouldBlock两个抽象方法,然后子类就可以各自实现不同的公平模式。
换句话说,ReentrantReadWriteLock的公平机制就由这两个方法来决定了。
而非公平模式的writerShouldBlock方法直接返回false,表示不让当前线程进入等待队列,而是直接进行锁的获取竞争。readerShouldBlock方法则调用apparentlyFirstQueuedIsExclusive方法判断头结点的下一个节点线程是否在请求获取独占锁(写锁)。
如果是则让其它线程先获取写锁,而自己则乖乖去排队。如果不是则说明下一个节点线程是请求共享锁(读锁),此时直接与之竞争读锁。
写锁的实现
先看tryAcquire方法的逻辑,c!=0时有两种情况,一种是高16位的读锁状态不为0,一种是低16位的写锁状态不为0。w等于0时表示还有线程持有读锁,直接返回false表示获取写锁失败。
如果持有写锁的线程为当前线程,则表示写锁重入操作,此时需要将状态变量进行累加,此外需要校验的是写锁重入状态值不能超过MAX_COUNT。
通过writerShouldBlock方法判断是否需要将当前线程放入排队队列中,同时通过CAS方式对状态变量进行累加操作。
对于非公平模式,这里就是闯入操作,即线程先尝试一次竞争写锁。最后设置当前线程持有写锁。
继续看tryRelease方法的逻辑,先用isHeldExclusively方法检查当前线程必须为写锁持有线程,然后将状态值减去释放的值并获取写锁状态值,如果其值为0则表示不存在重入情况,可以彻底释放锁了。
设置无线程持有写锁,最后设置新的状态值。
读锁的实现
tryAcquireShared方法的逻辑为:先获取写锁状态,如果不为0则表示有其它线程持有写锁而且当前线程没有持有写锁,则此时尝试获取读锁失败,将当前线程放到排队队列。
注意这里如果当前线程持有写锁的话则可以继续获取读锁。然后获取读锁状态,尝试通过CAS设置新的状态值,如果成功则返回1表示成功获取读锁。如果不成功则继续调用fullTryAcquireShared方法,该方法主要是一个自旋操作。
如果写锁不为0且当前线程未持有写锁则返回-1,表示尝试获取读锁失败,将当前线程加入排队队列中。如果写锁的状态为0,则表示没有线程持有写锁,继续通过readerShouldBlock方法判断是否需要将该线程加入到排队队列中。
此外,读锁的状态值不能等于MAX_COUNT,最后通过CAS方式设置新的状态值。
tryReleaseShared方法的逻辑为:通过for循环实现自旋,自旋的逻辑就是不断计算新的状态值,然后通过CAS方式来设置新的状态值。
一个例子
其中get方法属于读取数据的操作,所以使用共享的读锁即可。
而put和clear两个方法涉及到修改数据的操作,需要使用独占的写锁。
总结
而读写则是一种共享锁,它也包含了公平模式和非公平模式。它的实现基于AQS同步器,其中最重要的点是它通过某些技巧让读锁和写锁公共了同一个状态变量。通过本文的讲解相信大家已经很好地掌握了JDK提供的读写锁的实现原理。
也许你还想看
(▼点击文章标题或封面查看)
外来规范水土不服?手把手教你怎么扩展阿里规范idea插件
2019-12-12
深度长文 | 循序渐进解读计算机中的时间—系统&硬件篇(上)
2019-11-21
深度长文 | 循序渐进解读计算机中的时间—应用篇(上)
2019-11-14
加入搜狐技术作者天团
千元稿费等你来!
戳这里!☛
您对本文有什么疑问吗?
欢迎留言讨论!
▼▼▼