在Java高级的并发包里面还有一个有用的同步工具,就是 ReadWriteLock读写锁,它本身是一个接口,注意这个接口并没有继承Lock接口,因为的它的功能比较特殊,所以单独成为一个接口,我们经常需要使用它下面的子类: ReentrantReadWriteLock。
读写锁的主要应用场景是在读多写少的case下,它允许多个线程可以同时访问临界区的共享资源,因为仅仅读取不会修改是不会引起内存一致错误的,所以这样能够提升并发的吞吐量,但是对于写操作来讲是独占的,无论何时都只能有一个线程可以访问修改临界区资源。
读锁的要求是,当前没有任何线程持有写锁或者有写锁正在被阻塞排队等待被唤醒
写锁的要求是,当前必须没有任何线程持有读锁或者写锁
public class SharedIntegerArray {
private final int[] integerArray = new int[10];
private final ReadWriteLock lock = new ReentrantReadWriteLock();
private final Lock readLock = lock.readLock();
private final Lock writeLock = lock.writeLock();
public void write(int value, int index) {
writeLock.lock();
try {
integerArray[index] = value;
} finally {
writeLock.unlock();
}
}
public int read(int index) {
readLock.lock();
try {
return integerArray[index];
} finally {
readLock.unlock();
}
}
}
注意上面的读写模板,并不是任何场景下都适用的,这里仅仅是一个示例代码,读写锁内部是需要维护锁的状态,底层采用的是CAS指令,如果读和写都足够快的话,其实这里没有没必要使用读写锁,直接使用ReentrantLock或者synchronized关键字来辅助可能效率更高。
ReadWriteLock lock = new ReentrantReadWriteLock(true);
与ReentrantLock构造一样,这里面在构建锁对象的时候是支持构建公平和非公平锁两种模式的,默认情况下使用都是非公平锁。
非公平锁:
优点:通常可以提供更高的吞吐量
缺点:在一些读取非常频繁的场景下,有可能会出现线程饥饿问题,假设某种情况下读锁一直在占用,那么写锁就有可能永远的无限的等待下去。
公平锁:
优点:保证每一个线程按照先进先出的原则,按时间顺序人人都有机会得到锁
缺点:在特定case下回造成吞吐量降低
考虑下面这一种情况:
(1)线程A当前持有写锁,在临界区执行更新操作
(2)线程B是一个读锁,并且在阻塞等待A线程释放锁
(3)如果当线程A释放锁的同时,进来了一个写锁线程C,那么按照公平锁,则意味着这个线程C写锁要先挂起,知道B读锁执行完,再次唤醒C写锁。这里面其实有一个挂起和唤醒的开销。如果按照非公平锁C写锁其实不需要挂起,直接就占有锁然后执行逻辑,之后就是接着处理B读锁即可。这里公平模式会带来一定的损耗这一点需要注意。
另外一个因素,我们在考虑获取锁的时候, 可以采用非阻塞的模式来实现,或者加一个超时时间,从而使程序更加健壮。
// Try to acquire and give up if the lock
// is not available at this precise moment
readLock.tryLock();
// Try to acquire and give up if the lock
// is not available within 10 seconds
readLock.tryLock(10, TimeUnit.SECONDS);
上面第一个方法指的是一瞬间是否得到或者放弃锁,如果有人占用,那么客户线程可以可以采用非阻塞的方式来干另外别的事。
第二个方法指的在指定的周期内如果获得锁就占有锁,进入临界区执行,否则就获取失败,你可以过段时间在继续反复执行,直到占有锁。
锁的可重入性指的是当前线程已经占有锁的情况还可以再次调用加锁的其他方法,而不需要再次花费加锁和释放锁的耗时。
在可重入的读写锁的功能下,写锁可以通过锁重入直接降级为读锁,从而从写模式变成读模式,但是反过来却不行,因为从读锁升级为写锁,是必须要先释放读锁的。这一点需要注意。
本文主要介绍了关于Java并发包里面读写锁的的概念和应用场景,并介绍了锁的公平性问题,访问超时问题,重入和升级降级问题,读写锁在特定的场景下是可以提高并发吞吐量的,但是我们要了解这里面可能会出现的一些问题,并真正的思考我们的应用到底是否真的需要或者适合使用读写锁。