只要没有writer,读锁可以由多个reader线程同时保持。 写锁是独占的。
与互斥锁相比,使用读写锁能否提升性能则取决于读写操作期间读取数据相对于修改数据的频率,以及数据的争用,即在同一时间试图对该数据执行读取或写入操作的线程数
。
读写锁适用于读多写少
的场景。
ReentrantReadWriteLock
基于 AbstractQueuedSynchronizer
实现,具有如下属性
此锁允许reader和writer按照 ReentrantLock 的样式重新获取读/写锁。在写线程保持的所有写锁都已释放后,才允许重入reader使用读锁 writer可以获取读取锁,但reader不能获取写入锁。
重入还允许从写锁降级为读锁,实现方式是:先获取写锁,然后获取读取锁,最后释放写锁。但是,从读取锁升级到写入锁是不可能的
。
读锁和写锁都支持锁获取期间的中断。
写锁提供了一个 Condition 实现,对于写锁来说,该实现的行为与 ReentrantLock.newCondition()
提供的 Condition 实现对 ReentrantLock 所做的行为相同。当然,此 Condition 只能用于写锁
。
读锁不支持 Condition,readLock().newCondition() 会抛UnsupportedOperationException
此类支持一些确定是读锁还是写锁的方法。这些方法设计用于监视系统状态,而不是同步控制。
记录当前加锁的是哪个线程,初始化状态下,这个变量是null
接着线程1跑过来调用ReentrantLock的lock()方法尝试进行加锁,这个加锁的过程,直接就是用CAS操作将state值从0变为1。 如果之前没人加过锁,那么state的值肯定是0,此时线程1就可以加锁成功。 一旦线程1加锁成功了之后,就可以设置当前加锁线程是自己。
加锁线程变量
Reentrant打头,意思是一个可重入锁。 可重入锁就是你可以对一个ReentrantLock对象多次执行lock()加锁和unlock()释放锁,也就是可以对一个锁加多次,叫做可重入加锁。 看明白了那个state变量之后,就知道了如何进行可重入加锁!
其实每次线程1可重入加锁一次,会判断一下当前加锁线程就是自己,那么他自己就可以可重入多次加锁,每次加锁就是把state的值给累加1,别的没啥变化。
接着,如果线程1加锁了之后,线程2跑过来加锁会怎么样呢?
线程2跑过来一下看到,哎呀!state的值不是0啊?所以CAS操作将state从0变为1的过程会失败,因为state的值当前为1,说明已经有人加锁了!
接着线程2会看一下,是不是自己之前加的锁啊?当然不是了,“加锁线程”这个变量明确记录了是线程1占用了这个锁,所以线程2此时就是加锁失败。
接着,线程2会将自己放入AQS中的一个等待队列,因为自己尝试加锁失败了,此时就要将自己放入队列中来等待,等待线程1释放锁之后,自己就可以重新尝试加锁了
所以大家可以看到,AQS是如此的核心!AQS内部还有一个等待队列,专门放那些加锁失败的线程!
接着,线程1在执行完自己的业务逻辑代码之后,就会释放锁! 他释放锁的过程非常的简单,就是将AQS内的state变量的值递减1,如果state值为0,则彻底释放锁,会将“加锁线程”变量也设置为null!
接下来,会从等待队列的队头唤醒线程2重新尝试加锁。 好!线程2现在就重新尝试加锁,这时还是用CAS操作将state从0变为1,此时就会成功,成功之后代表加锁成功,就会将state设置为1。 此外,还要把“加锁线程”设置为线程2自己,同时线程2自己就从等待队列中出队了。
ReentrantLock 里,状态值表示重入计数
一个状态是没法既表示读锁,又表示写锁的,显然不够用啊,那就辦成两份用了! 状态的高位部分表示读锁,低位表示写锁 由于写锁只有一个,所以写锁的重入计数也解决了,这也会导致写锁可重入的次数减小。
由于读锁可以同时有多个,肯定不能再用辦成两份用的方法来处理了
但我们有 ThreadLocal
,可以把线程重入读锁的次数作为值存在 ThreadLocal
对于公平性的实现,可以通过AQS的等待队列和它的抽象方法来控制 在状态值的另一半里存储当前持有读锁的线程数。