前言
并发编程中,确保线程间共享数据的原子性是很重要的。在大多数情况下,我们可以使用锁,使对共享数据的并行访问转变成串行,从而保证数据的原子性:
1 | lock(); // 加锁 |
但是,我们也知道,在一些情况下,我们只需要去读取数据,而不需要对数据进行更改。这种读操作,并不对数据进行更改,是幂等的。通常情况下,我们可以认为它们之间并不需要加锁:
1 | read(data); // 多个线程可以一起读 |
在读远远多于写的情况下,对并发的读也进行加锁、改并行为串行,无疑是一种低效的处理方式。那么应该如何处理呢?也就是说,需要一种锁,可以保证读写、写写互斥,而读读却是可以并发执行的。
这种锁,我们称之为读写锁。java 中,读写锁的使用非常简单:
1 | private final Map<String, Data> m = new TreeMap<>(); |
我们可以看出,读写锁有两种锁,一是读锁,而是写锁,而使用中,我们只需更具需要操作读锁或写锁就可以了。
导航
ReentrantReadWriteLock
ReentrantReadWriteLock 是读写锁接口 ReadWriteLock 的一个实现类:
1 | public class ReentrantReadWriteLock implements ReadWriteLock, java.io.Serializable { |
可以看到,ReentrantReadWriteLock 有两种模式,一种是公平的,另一种是非公平的。公平锁可以按序排队获得锁,而非公平性锁则不能保证按序获取。但是,非公平性的吞吐量更大。
读锁和写锁
读锁和写锁的类非常清晰,我们先来看读锁:
1 | public static class ReadLock implements Lock, java.io.Serializable { |
读锁的锁操作,基本上是通过 sync 对象来实现的。而熟悉 AQS(AbstractQueuedSynchronizer)的读者们,看到 sync 对象这几个方法,可能已经意识到了 sync 继承了 AQS 类。事实也是如此。因此,读锁利用了 AQS 中的共享模式来实现。而写锁,则是利用独占锁来实现的,这里就不在粘贴代码了。
Sync
通过上文可知,读写锁有两把锁:一是读锁,二是写锁。读锁通过 AQS 的共享锁实现,写锁则通过独占锁来实现。而 ReentrantReadWriteLock 中,继承 AQS 的来实际实现加锁解锁的辅助类是 Sync:
1 | abstract static class Sync extends AbstractQueuedSynchronizer { |
写锁(独占锁)
在这里,我们姑且可以将独占锁看做是写锁,共享锁看做是读锁。继承 AQS 所需要实现的独占锁相关方法如下:
1 | // 暗示虚拟机保持足够栈空间 |
读锁(共享锁)
读锁的抢占分为了三个步骤:
- 其他线程正持有读锁,失败。
- 判断该线程是否应该堵塞。不堵塞则直接尝试通过 CAS 加锁(增加读锁持有数)。
- 上一步失败,采用乐观锁不断尝试加锁,直至成功。
1 |
|
公平与非公平:FairSync 与 NonfairSync
回到 ReentrantReadWriteLock 的构造方法:
1 | public ReentrantReadWriteLock(boolean fair) { |
我们看到,事实上起到同步作用的对象是 Sync 的两个子类:FairSync 与 NonfairSync。这两个子类实现了 Sync 锁定义的两个抽象方法:writerShouldBlock 与 readerShouldBlock,也就是给线程抢占读锁或写锁时,是否应该堵塞,等待其他线程。
1 | static final class FairSync extends Sync { |