并发包之读写锁

之前的锁基本都是排他锁,这些锁在同一时刻只允许一个线程进行访问,而读写锁在同一时刻可以允许多个读线程访问,但是在写线程访问时,所有的读线程和其他写线程均被阻塞,读写锁维护了一对锁,一个读锁和一个写锁,通过分离读锁和写锁,使得并发性相比一般的排它锁有了很大的提升。

没有读写锁的时候,要完成读写功能就要使用Java的等待通知机制,就是当写操作开始时,所有晚于写操作的读操作均会进入等待状态,只有写操作完成并进行通知后,所有等待的读操作才能继续进行,这样的目的是不会出现脏读。改用读写锁实现上述功能,只需要在读操作时获取读锁,写操作时获取写锁即可。当写锁被获取到后,后续的读写操作都会被阻塞,写锁释放以后,读写操作继续执行,相对于等待通知机制,更加简单明了。

在读大于写的情况下,读写锁能够提供比排他锁更好的并发性和吞吐量。并发包中读写锁的实现是ReentrantReadWriteLock,支持公平性选择,重入,锁降级(遵循获取写锁,获取读锁再释放写锁的次序,写锁能够降级成为读锁。)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
读写锁的例子
public class Cache {
static Map<String,String> map = new HashMap<>();
static ReentrantReadWriteLock readWriteLock =new ReentrantReadWriteLock();
static Lock read = readWriteLock.readLock();
static Lock write =readWriteLock.writeLock();
public static final String get(String key)
{
read.lock();
try
{
return map.get(key);
}finally {
read.unlock();
}
}
public static final String set(String key, String value)
{
write.lock();
try
{
return map.put(key,value);
}finally {
write.unlock();
}
}
public static final void clear()
{
write.lock();
try
{
map.clear();
}
finally {
write.unlock();
}
}
}

ReadWriteLock管理一组锁,一个是只读的锁,一个是写锁,读锁可以在没有写锁的时候被多个线程同时持有,写锁是独占的

一个获得了读锁的线程必须能看到前一个释放的写锁所更新的内容。

读写锁比互斥锁允许对于共享数据更大程度的并发,每次只能有一个写线程,但是同时可以有多个线程并发地读数据,ReadWriteLock适用于读多写少的并发情况。

ReadWriteLock是一个接口,主要有两个方法

1
2
3
4
5
public interface ReadWriteLock
{
Lock readLock()//返回读锁
Lock writeLock();//返回写锁
}

源码分析

1
2
3
4
5
6
7
8
9
public ReentrantReadWriteLock() {
this(false);
}

public ReentrantReadWriteLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
readerLock = new ReadLock(this);
writerLock = new WriteLock(this);
}

默认构造方法时非公平模式,创建的Sync是NonfairSync对象,然后初始化读锁和写锁。

Sync 继承了AbstractQueuedSynchronizer,而Sync 是一个抽象类,NonfairSync 和FairSync 继承了Sync, 并重写了其中的抽象方法。

Sync 分析

Sync中提供了很多方法,但是有两个方法是抽象的,子类必须实现。下面以FairSync为例,分析一下这两个抽象方法:

1
2
3
4
5
6
7
8
9
static final class FairSync extends Sync {
private static final long serialVersionUID = -2274990926L;
final boolean writerShouldBlock() {
return hasQueuedPredecessors();
}
final boolean readerShouldBlock() {
return hasQueuedPredecessors();
}
}

writerShouldBlock和readerShouldBlock方法都表示当有别的线程也在尝试获取锁时,是否应该阻塞。
对于公平模式,hasQueuedPredecessors()方法表示前面是否有等待线程。一旦前面有等待线程,那么为了遵循公平,当前线程也就应该被挂起。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
static final class NonfairSync extends Sync {
private static final long serialVersionUID = -815965037L;
final boolean writerShouldBlock() {
return false; // writers can always barge
}
final boolean readerShouldBlock() {
/* As a heuristic to avoid indefinite writer starvation,
* block if the thread that momentarily appears to be head
* of queue, if one exists, is a waiting writer. This is
* only a probabilistic effect since a new reader will not
* block if there is a waiting writer behind other enabled
* readers that have not yet drained from the queue.
*/
return apparentlyFirstQueuedIsExclusive();
}
}

从上面可以看到,非公平模式下,writerShouldBlock直接返回false,说明不需要阻塞;而readShouldBlock调用了apparentFirstQueuedIsExcluisve()方法。该方法在当前线程是写锁占用的线程时,返回true;否则返回false。也就说明,如果当前有一个写线程正在写,那么该读线程应该阻塞。
继承AQS的类都需要使用state变量代表某种资源,ReentrantReadWriteLock中的state代表了读锁的数量和写锁的持有与否,整个结构如下:

可以看到state的高16位代表读锁的个数;低16位代表写锁的状态。