在使用synchronize关键字修饰方法后,只允许一个线程进行访问,这个虽然有利于保证数据安全,却实际场景背道而驰的。实际中数据都是读取多,写入少,我们需要更粗细粒的并发锁。JVM concurrent.locks包给我们提供ReadWriteLock读写锁,内置两把锁,读锁、写锁,满足多个线程并发读取数据,写入时互斥所有线程,既保证了数据安全,又提升了响应量。
概念
读锁: 可以理解成共享锁,允许多个线程同时读取
写锁: 独占锁,有且只允许一个线程访问
读写互斥: 在获取写锁时,必须等待所有读锁全部释放,才能获取成功,读锁会堵塞写锁,写锁会堵塞所有的线程。
锁升级: 在使用读锁时,已经获取读锁线程在没有释放读锁的情况下,去获取写锁这就是锁升级。这是不被允许的,锁升级会造成死锁。
1 | // 这个会造成死锁 |
锁降级: 已经获取到写锁线程,被允许在没有释放锁的情况下去获取读锁的,值得注意读锁、写锁仍然需要单独释放。
1 | //并不会造成死锁 |
使用官方例子演示ReentrantReadWriteLock 使用场景,每次获取缓存时,先判断缓存是否已经失效了,如果失效了使用写锁更新缓存。
1 | class CachedData { |
代码很少,但是非常有代表性,非常适合缓存这种读取多,更新少的场景。在每次读取缓存时,先开启读锁,检查缓存状况,需要更新缓存时。先释放读锁然后再去获取写锁,在更新前先判断缓存又没被其他线程更新过了,更新完数据后降级到读锁,再释放写锁,使用缓存释放读锁。
源码解析
这里源码分析只有简单讲解两个锁的获取、释放原理,看阅读源码之前,自备AQS的知识点。
ReentrantReadWriteLock是实现ReadWriteLock接口的实现类,内部使用AQS的int state
来表示读写锁的状态
如上图所示,两个锁的获取、释放都是同时使用int state
来进行,使用低16位表示写锁获取次数、高16位表示读锁获取次数。使用内部类Sync 单独编写共享锁、独占锁的获取释放具体实现,再使用ReadLock、WriteLock分别调用共享锁、独占锁的方法。源码阅读先从Sync内部类开始。
内部属性
1 | abstract static class Sync extends AbstractQueuedSynchronizer { |
我这里认为读锁做一个共享锁在重入次数上,state不能准确表达出每一个线程到底重入了多少次,所以需要用到HoldCounter来记录每一个线程获取锁次数,在释放锁的时候,会看下如何使用的。
共享锁的获取和释放
1 | protected final int tryAcquireShared(int unused) { |
readerShouldBlock 是一个队列堵塞策略方法,用于区分公平锁和非公平锁的实现,当返回true时,会堵塞所有获取读锁线程。
1 | final int fullTryAcquireShared(Thread current) { |
总结下: 当获取共享锁时,只有检测到独占锁时,获取锁方法会立即返回失败。
看下共享锁释放
1 | protected final boolean tryReleaseShared(int unused) { |
HoldCounter用于维护每一个线程释放锁数量,保证释放不会超过自身持有的数量。
独占锁获取和释放
1 | protected final boolean tryRelease(int releases) { |
writerShouldBlock:当返回true会堵塞获取锁的线程,用于区分公平锁和非公平锁实现。结合上面代码,当返回true时,不会去获取锁,直接失败了。
独占锁释放
1 | protected final boolean tryRelease(int releases) { |
公平锁和非公平锁
ReentrantReadWriteLock内部有两个锁可以选择,公平锁和非公平锁。通过构造参数进行选择,默认使用非公平锁。
1 | public ReentrantReadWriteLock() { |
非公平锁: 在获取读锁或者写锁时,获取锁的线程并不是顺序的,在堵塞队列中的线程可能长期等待,获取不到锁,而没有在堵塞队列中等待线程反而能快速获取到锁,这个会造成线程饥饿,但是会比公平锁有更高的吞吐量。
公平锁: 保证每一个等待最久线程最先获取到线程执行权,线程都会按照AQS堵塞顺序获取锁,这样有利于避免线程饥饿的产生,但是在在获取锁需要判断队列有一定性能损耗,所以吞吐量不如非公平高。
公平锁和非公平锁区别在在于writerShouldBlock 、readerShouldBlock 方法实现不同而已。
公平锁实现
1 | static final class FairSync extends Sync { |
hasQueuedPredecessors: 返回true则说明AQS中存在堵塞线程,只有在出现写锁的时候,才会将获取锁线程放入队列中,所以readerShouldBlock在读锁获取时,会永远返回false。
非公平锁
1 | static final class NonfairSync extends Sync { |
从上面代码知道,只有这两个方法返回true,都不能去竞争锁,公平锁的策略非常明显,只有堵塞队列有线程,就会放弃锁竞争。而非公平锁则是在写锁时,无论队列有无线程都会尝试竞争,写锁时只有队列最前面的线程为写锁时,才会放弃竞争,总的来说公平锁和非公平锁逻辑和ReentrantLock 逻辑基本一样。
tryLock
在读锁、写锁的对象中,都存在tryLock 方法,它跟lock方法有“亿点点”不同,虽然他们都是调用了内部Sync方法,但是在获取锁方法上,和上面分析tryAcquire、tryAcquireShared基本一致,唯独缺少了readerShouldBlock、writerShouldBlock使用。使用这个方法获取锁,无论公平锁还非公平锁,获取锁逻辑都一样。无论堵塞队列是否有线程,会直接竞争获取锁,在非公平锁中读锁会让步队列中第一个写锁,写锁优先级会高于读锁。但tryLock不存在,所有锁的竞争的公平的,快速的,可以理解这个方法在获取锁上会有更高的优先级(相比lock)。