简单说就是写操作加入锁,读操作也加入锁。写锁也可以称之为独占锁
,读锁也可以称之为共享锁
。这里我们先不过多描述,直接演示代码看效果,然后总结。
ReadWriteLock 维护了一对相关的锁,一个用于只读操作,另一个用于写入操作。只要没有 writer,读取锁可以由多个 reader 线程同时保持。写入锁是独占的。
所有 ReadWriteLock 实现都必须保证 writeLock 操作的内存同步效果也要保持与相关 readLock 的联系。也就是说,成功获取读锁的线程会看到写入锁之前版本所做的所有更新。
与互斥锁相比,读-写锁允许对共享数据进行更高级别的并发访问。虽然一次只有一个线程(writer 线程)可以修改共享数据,但在许多情况下,任何数量的线程可以同时读取共享数据(reader 线程),读-写锁利用了这一点。从理论上讲,与互斥锁相比,使用读-写锁所允许的并发性增强将带来更大的性能提高。在实践中,只有在多处理器上并且只在访问模式适用于共享数据时,才能完全实现并发性增强。
与互斥锁相比,使用读-写锁能否提升性能则取决于读写操作期间读取数据相对于修改数据的频率,以及数据的争用——即在同一时间试图对该数据执行读取或写入操作的线程数。例如,某个最初用数据填充并且之后不经常对其进行修改的 collection,因为经常对其进行搜索(比如搜索某种目录),所以这样的 collection 是使用读-写锁的理想候选者。但是,如果数据更新变得频繁,数据在大部分时间都被独占锁,这时,就算存在并发性增强,也是微不足道的。更进一步地说,如果读取操作所用时间太短,则读-写锁实现(它本身就比互斥锁复杂)的开销将成为主要的执行成本,在许多读-写锁实现仍然通过一小段代码将所有线程序列化时更是如此。最终,只有通过分析和测量,才能确定应用程序是否适合使用读-写锁。
先看下面这个例子:我们模拟了生活总写作者和读者之间的关系,写作者要写一本书(也就是共享资源),然后读者可以读,这里想要模拟两个读者,两个写作者,并且还需要保证两个读者能够同时观看这本书,并且还要保证这本写的书是安全的(也就是读者和写作者不能同时发生,互斥行为)
public class WriteReadLockDemo {
public static final Lock lock = new ReentrantLock();
public static void main(String[] args) throws InterruptedException {
new Thread(()->{
write();
},"写作者1:").start();
new Thread(()->{
write();
},"写作者2:").start();
TimeUnit.SECONDS.sleep(3);
new Thread(()->{
read();
},"【读】者1:").start();
new Thread(()->{
read();
},"【读】者2:").start();
}
private static void read() {
// 读操作
System.out.println(Thread.currentThread().getName()+"想进入图书馆看书(还没有抢到【读】锁)");
lock.lock();
System.out.println(Thread.currentThread().getName()+"成功抢到【读】锁开始,开始看书,等待:5秒");
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(Thread.currentThread().getName()+"5秒到了,书看完了,释放【读】锁结束");
lock.unlock();
}
private static void write() {
// 写操作
System.out.println(Thread.currentThread().getName()+"想写一本书(还没有抢到【写】锁)");
lock.lock();
System.out.println(Thread.currentThread().getName()+"成功抢到【写】锁 开始写作,等待:10秒");
try {
TimeUnit.SECONDS.sleep(10);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(Thread.currentThread().getName()+"10秒到了,写书结束,释放【写】锁结束");
lock.unlock();
}
}
运行结果如下:
写作者1:想写一本书(还没有抢到【写】锁)
写作者1:成功抢到【写】锁 开始写作,等待:10秒
写作者2:想写一本书(还没有抢到【写】锁)
【读】者1:想进入图书馆看书(还没有抢到【读】锁)
【读】者2:想进入图书馆看书(还没有抢到【读】锁)
写作者1:10秒到了,写书结束,释放【写】锁结束
写作者2:成功抢到【写】锁 开始写作,等待:10秒
写作者2:10秒到了,写书结束,释放【写】锁结束
【读】者1:成功抢到【读】锁开始,开始看书,等待:5秒
【读】者1:5秒到了,书看完了,释放【读】锁结束
【读】者2:成功抢到【读】锁开始,开始看书,等待:5秒
【读】者2:5秒到了,书看完了,释放【读】锁结束
Process finished with exit code 0
分析上述运行结果:
第14行睡眠3s,是为了能想让写作者1,2开始执行线程,然后调用第46行代码睡眠10s,表示写作者1或者2持有这把锁时间是10s,10s后才会把锁放开。然后3s后开始执行读者,读者1,2开始尝试加锁,但是现在锁还在写作者1或者2手中,需等10s中过后,写作者释放了锁,读者才开始竞争这把锁,只要有一个读者竞争到锁,就又会占用这把锁5s时间,另一个读者就需要等待了,这就非常的不友好了,为什么读者还需要等待呢,我想让两个读者一起去看这本书,中间不要等待。
总结:从运行结果我们可以看出,使用传统的Lock
锁的时候,在我们读写操作中,每次只能允许一个线程获取到这把锁才能进行读写操作,别的线程想要获取锁呢,就必须等待着持有锁的线程把锁给我释放喽。但是现实中,一般读操作都是很多人并发操作的,谁会等着你,所以这里使用传统的Lock
锁加锁保证数据安全性,效率有点低,进而衍生出了共享锁诞生,既能保证数据安全性,又能提高读操作的吞吐量。
Java
中也为我们提供了共享锁的实现,下面先演示代码在总结。
public class WriteReadLockDemo {
public static final Lock lock = new ReentrantLock();
public static ReentrantReadWriteLock wrlock = new ReentrantReadWriteLock();
static ReentrantReadWriteLock.WriteLock writeLock = wrlock.writeLock();
static ReentrantReadWriteLock.ReadLock readLock = wrlock.readLock();
public static void main(String[] args) throws InterruptedException {
new Thread(()->{
read();
},"【读】者1:").start();
new Thread(()->{
read();
},"【读】者2:").start();
TimeUnit.SECONDS.sleep(3);
new Thread(()->{
write();
},"写作者1:").start();
new Thread(()->{
write();
},"写作者2:").start();
}
private static void read() {
// 读操作
System.out.println(Thread.currentThread().getName()+"想进入图书馆看书(还没有抢到【读】锁)");
readLock.lock();
System.out.println(Thread.currentThread().getName()+"成功抢到【读】锁开始,开始看书,等待:5秒");
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(Thread.currentThread().getName()+"5秒到了,书看完了,释放【读】锁结束");
readLock.unlock();
}
private static void write() {
// 写操作
System.out.println(Thread.currentThread().getName()+"想写一本书(还没有抢到【写】锁)");
writeLock.lock();
System.out.println(Thread.currentThread().getName()+"成功抢到【写】锁 开始写作,等待:15秒");
try {
TimeUnit.SECONDS.sleep(15);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(Thread.currentThread().getName()+"15秒到了,写书结束,释放【写】锁结束");
writeLock.unlock();
}
}
运行结果如下:
【读】者1:想进入图书馆看书(还没有抢到【读】锁)
【读】者1:成功抢到【读】锁开始,开始看书,等待:5秒
【读】者2:想进入图书馆看书(还没有抢到【读】锁)
【读】者2:成功抢到【读】锁开始,开始看书,等待:5秒
写作者1:想写一本书(还没有抢到【写】锁)
写作者2:想写一本书(还没有抢到【写】锁)
【读】者1:5秒到了,书看完了,释放【读】锁结束
【读】者2:5秒到了,书看完了,释放【读】锁结束
写作者1:成功抢到【写】锁 开始写作,等待:15秒
写作者1:15秒到了,写书结束,释放【写】锁结束
写作者2:成功抢到【写】锁 开始写作,等待:15秒
写作者2:15秒到了,写书结束,释放【写】锁结束
Process finished with exit code 0
我们来分析运行结果:第18行代码睡眠3s,也就是3s后启动写作1、2线程,先执行读者1、2线程,然后在看到第34行代码,是让读者线程睡眠5s,也就是只要有一个读者线程进来了,他可以持有这把锁5s中,5s过后才会释放。
但是我们从运行结果可以看到,读者1线程进来了抢夺到了锁,持有锁的时间是5s,但是5s中还没有到呀,发现读者2线程就开始来抢夺这把锁,并且还成功了,就感觉有点锁失效了的效果。其实这个就是共享锁的效果,可以并发操作。现在假设3s过后,写作者1、2线程开始启动,也开始抢夺这把锁,但是,由于我们的读者线程持有锁,并且还没有释放,要等到5s中过后,等这两个读者线程都释放锁,写作者线程就可以开始尝试加锁了。
从上面的结果得到,写作者1、2线程都是要等到读者线程都释放锁了之后才开始竞争这把锁。
总结一句话:读读共享,读写互斥