前言:
上一篇文章,已经很详细地介绍了 显式锁Lock 以及 其常用的实现方式- - ReetrantLock(重入锁),本文将介绍另一种显式锁 - - 读写锁ReadWriteLock。
前面介绍的隐式锁Synchronize、重入锁ReetrantLock都是互斥锁、独占锁,即同一个锁只能每时每刻至多由一个线程来获持有。互斥,是一种保守策略,虽然避免了“写/写”、“读/写”冲突,但也阻止了安全的“读/读”发生。然而,在不少情况下,数据结构上的操作都是“读操作”,如果此时能放宽加锁需求,允许多个读线程同时访问数据结构,那么将极大地提升程序的性能;而这种能支持共享的锁便被设计出来 - - 读写锁ReadWriteLock。
一、读写锁ReadWriteLock介绍
1、ReadWriteLock接口
首先,得明确一点,ReadWriteLock接口并没有继承Lock接口,可参考上一篇文章的继承结构图,ReadWriteLock 仅仅定义了两个方法,即readLock、writeLock方法;
Lock readLock( ): 返回用于读取操作的锁:
Lock writeLock( ): 返回用于写入操作的锁。
2、 读-写锁的性能
与互斥锁相比,读-写锁允许对共享数据进行更高级别的并发访问。虽然一次只有一个线程(writer 线程)可以修改共享数据,但在许多情况下,任何数量的线程可以同时读取共享数据(reader 线程),读-写锁利用了这一点。从理论上讲,与互斥锁相比,使用读-写锁所允许的并发性增强将带来更大的性能提高。但在实践中,只有在多处理器上频繁地访问读取数据结构,才能提高性能。 而在其他情况下,读-写锁的性能却比独占锁的性能要差一点,这是因为读-写锁的复杂性更高。所以要对程序进行分析,判断读-写锁是否能提高性能。
3、ReadWriteLock接口实现时的可选策略
尽管读-写锁的基本操作是直截了当的,但实现仍然必须作出许多决策,这些决策可能会影响给定应用程序中读-写锁的效果。这些策略的例子包括:
- 释放优先: 当一个写入操作释放写锁时,并且队列中同时存在读线程和写线程时,那么是读线程优先获得锁,还是写线程,或者说是最先发出请求的线程
- 读线程插队: 如果当读线程持有着读锁时,有写线程在等待,那么新到达的读线程能否立即获得访问权,还是应该在写线程后面等待?如果允许读线程插队到线程前面,那么将提高并发性,但却可能造成写线程发生饥饿问题。
- 重入性: 读锁、写锁是否允许重入。
- 降级: 如果一个线程持有写锁,那么它能否在不释放锁的情况下降级成一个读锁?
- 升级: 拥有读锁的线程能否优于其他正在等待的读线程和写线程而升级成为一个写锁?在大多数的读-写锁实现中并不支持升级,因为很容易造成死锁(如果两个读线程同时升级为写锁,那么二者都不会释放读取锁)
二、读写锁的实现类 - - ReentrantReadWriteLock
ReentrantReadWriteLock实现了ReadWriteLock接口。
1、ReentrantReadWriteLock的实现策略:
- 可重入的加锁;
- 提供公平锁与非公平锁(默认)的选择。与ReentrantLock类似,都是在构造方法中传入参数来决定,关于公平锁与非公平锁的详细可参考我的上一篇博文;
- 读线程不能插队: 尽管当读线程持有着读锁时,写线程等待获取锁,这时候新到达的其他读线程都必须等待它们前面的写线程使用完并释放了写锁,才能获得读锁。
- 写线程可以降级为读线程(支持降级),但是读线程不能升级为写线程(不支持升级);
2、ReentrantReadWriteLock 的读锁与写锁
ReentrantReadWriteLock实现的是ReadWriteLock接口,并没有实现Lock接口,但其管理的读锁ReentrantReadWriteLock.ReadLock 、写锁ReentrantReadWriteLock.WriteLock
都是其内部类,并且是实现Lock接口。注意以下两点:
- 读锁、写锁都支持定时获取锁、中断锁、非阻塞获取锁,与ReetrantLock相似;
- Condition 支持 :只能用于写锁,读锁是不支持的(因为读锁是共享锁)。写入锁提供了一个 Condition 实现,对于写入锁来说,该实现的行为与 ReentrantLock.newCondition() 提供的 Condition 实现对 ReentrantLock 所做的行为相同。
读取锁不支持 Condition,readLock().newCondition() 会抛出 UnsupportedOperationException;
3、ReentrantReadWriteLock 提供的监视系统状态的方法
boolean hasQueuedThread(Thread thread):
查询是否给定线程正在等待获取读取或写入锁。注意,因为随时可能发生取消操作,所以返回 true 并不保证此线程将获取锁。此方法主要用于监视系统状态。
boolean hasQueuedThreads( ):
查询是否所有的线程正在等待获取读取或写入锁。注意,因为随时可能发生取消操作,所以返回 true 并不保证任何其他线程将获取锁。此方法主要用于监视系统状态。
boolean hasWaiters(Condition condition)
查询是否有些线程正在等待与写入锁有关的给定条件。注意,因为随时可能发生取消操作,所以返回 true 并不保证任何其他线程将获取锁。此方法主要用于监视系统状态。
boolean isFair( )
如果此锁将公平性设置为 ture,则返回 true。
boolean isWriteLocked( )
查询是否某个线程保持了写入锁。
boolean isWriteLockedByCurrentThread( )
查询当前线程是否保持了写入锁。
int getWaitQueueLength(Condition condition)
返回正等待与写入锁相关的给定条件的线程估计数目。注意,因为随时可能发生超时和中断,所以只能将估计值作为实际等待线程数的上限。此方法设计用于监视系统状态,而不是同步控制。
int getWriteHoldCount( )
查询当前线程在此锁上保持的重入写入锁数量。
int getQueueLength( )
返回等待获取读取或写入锁的线程估计数目。
int getReadHoldCount( )
查询当前线程在此锁上保持的重入读取锁数量。
int getReadLockCount( )
查询为此锁保持的读取锁数量。
还有几个protected方法,不再详述。
@ Example1: 读写锁的使用实例
在使用某些种类的 Collection 时,可以使用 ReentrantReadWriteLock 来提高并发性。通常,在预期 collection 很大,读取者线程访问它的次数多于写入者线程,并且 entail 操作的开销高于同步开销时,这很值得一试。例如,以下是一个使用 TreeMap 的类,预期它很大,并且能被同时访问。
class RWDictionary {
private final Map<String, Data> m = new TreeMap<String, Data>();
private final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
private final Lock r = rwl.readLock();
private final Lock w = rwl.writeLock();
public Data get(String key) {
r.lock();
try { return m.get(key); }
finally { r.unlock(); }
}
public String[] allKeys() {
r.lock();
try { return m.keySet().toArray(); }
finally { r.unlock(); }
}
public Data put(String key, Data value) {
w.lock();
try { return m.put(key, value); }
finally { w.unlock(); }
}
public void clear() {
w.lock();
try { m.clear(); }
finally { w.unlock(); }
}
}
@ Example2: ReentrantReadWriteLock的写锁降级
public static int count = 5;
public static void main(String[] args) {
//创建一个非公平的读写锁
ReadWriteLock lock = new ReentrantReadWriteLock(false);
Thread threadA = new Thread("threadA"){
@Override
public void run() {
//获取读锁
lock.readLock().lock();
System.out.println("成功获取读锁,count的值是:"+count);
if(count<10){
lock.readLock().unlock();
//在获取写锁前,必须先释放读锁
lock.writeLock().lock();
System.out.println("成功获取写锁");
count += count*3;
//获取读锁,此时没有释放写锁,即为写锁降级为读锁
lock.readLock().lock();
//成功获取读锁,写锁降级成功,释放写锁
lock.writeLock().unlock();
System.out.println("写锁成功降级成读锁,count的值是:"+count);
}
}
};
threadA.start();
}
运行结果:
成功获取读锁,count的值是:5
成功获取写锁
写锁成功降级成读锁,count的值是:20