前言
本文隶属于专栏《100个问题搞定Java并发》,该专栏为笔者原创,引用请注明来源,不足和错误之处请在评论区帮忙指出,谢谢!
本专栏目录结构和参考文献请见100个问题搞定Java并发
正文
ReadWriteLock 是 JDK5 中提供的读写分离锁。
读写分离锁可以有效地帮助减少锁竞争, 提升系统性能。
用锁分离的机制来提升性能非常容易理解,比如线程 A1 、 A2 、 A3 进行写操作, B1 、 B2 、 B3 进行读操作,如果使用重入锁或者内部锁,从理论上说所有读之间、读与写之间、写和写之间都是串行操作。
当 B1 进行读取时, B2 、 B3 则需要等待锁。
由于读操作并不对数据的完整性造成破坏,这种等待显然是不合理的。
因此,读写锁就有了发挥功能的余地。
在这种情况下,读写锁允许多个线程同时读,使得 B1 、 B2 、 B3 之间真正并行。
但是,考虑到数据完整性,写写操作和读写操作间依然是需要相互等待和持有锁的。
总的来说,读写锁的访问约束情况如表所示。
\ | 读 | 写 |
---|---|---|
读 | 非阻塞 | 阻塞 |
写 | 阻塞 | 阻塞 |
读-读不互斥:读读之间不阻塞。
读-写互斥:读阻塞写,写也会阻塞读
写-写互斥:写写阻塞
如果在系统中,读操作的次数远远大于写操作的次数,则读写锁就可以发挥最大的功效,提升系统的性能。
源码(JDK8)
/**
* ReadWriteLock 维护一对关联的locks ,一个用于只读操作,一个用于写入。
*
* read lock可以由多个读线程同时持有,只要没有 writer。 write lock 是独家的。
*
* 所有ReadWriteLock实现必须保证writeLock操作的内存同步效应(如在指定Lock接口)也保持相关联的readLock 。
*
* 也就是说,一个线程成功获取读锁定将会看到在之前发布的写锁定所做的所有更新。
*
* 读写锁允许访问共享数据时的并发性高于互斥锁所允许的并发性。
*
* 它利用了这样一个事实:一次只有一个线程( 写入线程)可以修改共享数据,在许多情况下,任何数量的线程都可以同时读取数据(reader 线程)。
*
* 从理论上讲,通过使用读写锁允许的并发性增加将导致性能改进超过使用互斥锁。
*
* 实际上,并发性的增加只能在多处理器上完全实现,然后只有在共享数据的访问模式是合适的时才可以。
*
* 读写锁是否会提高使用互斥锁的性能取决于数据被读取的频率与被修改的频率相比,读取和写入操作的持续时间以及数据的争用
*
* - 即是,将尝试同时读取或写入数据的线程数。
*
* 例如,最初填充数据的集合,然后经常被修改的频繁搜索(例如某种目录)是使用读写锁的理想候选。
*
* 然而,如果更新变得频繁,那么数据的大部分时间将被专门锁定,并且并发性增加很少。
*
* 此外,如果读取操作太短,则读写锁定实现(其本身比互斥锁更复杂)的开销可以支配执行成本,特别是因为许多读写锁定实现仍将序列化所有线程通过小部分代码。
*
* 最终,只有剖析和测量将确定使用读写锁是否适合您的应用程序。
*
* 虽然读写锁的基本操作是直接的,但是执行必须做出许多策略决策,这可能会影响给定应用程序中读写锁定的有效性。
*
* 这些政策的例子包括:
*
* 在写入器释放写入锁定时,确定在读取器和写入器都在等待时是否授予读取锁定或写入锁定。
*
* writer 偏好是常见的,因为 write 预计会很短,很少见。
*
* reader 喜好不常见,因为如果 reader 经常和长期的预期,write可能导致漫长的延迟。
*
* 公平的或“按顺序”的实现也是可能的。
*
* 确定在 reader 处于活动状态并且 writer 正在等待时请求读取锁定的 reader 是否被授予读取锁定。
*
* reader 的偏好可以无限期地拖延 writer,而对 writer 的偏好可以减少并发的潜力。
*
* 确定锁是否可重入:
*
* 一个具有写锁的线程是否可以重新获取?
*
* 持有写锁可以获取读锁吗?
*
* 读锁本身是否可重入?
*
* 写入锁可以降级到读锁,而不允许插入写者?
*
* 读锁可以升级到写锁,优先于其他等待 reader 或 writer 吗?
*
* 在评估应用程序的给定实现的适用性时,应考虑所有这些问题。
*/
public interface ReadWriteLock {
/**
* 返回用于读取的锁。
*
* @return 用来读的锁
*/
Lock readLock();
/**
* 返回用于写入的锁。
*
* @return 用来写的锁
*/
Lock writeLock();
}
实践
package com.shockang.study.java.concurrent.lock;
import java.util.Random;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class ReadWriteLockDemo {
private static Lock lock = new ReentrantLock();
private static ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
private static Lock readLock = readWriteLock.readLock();
private static Lock writeLock = readWriteLock.writeLock();
private int value;
public Object handleRead(Lock lock) throws InterruptedException {
try {
lock.lock(); //模拟读操作
Thread.sleep(1000); //读操作的耗时越多,读写锁的优势就越明显
return value;
} finally {
lock.unlock();
}
}
public void handleWrite(Lock lock, int index) throws InterruptedException {
try {
lock.lock(); //模拟写操作
Thread.sleep(1000);
value = index;
} finally {
lock.unlock();
}
}
public static void main(String[] args) {
final ReadWriteLockDemo demo = new ReadWriteLockDemo();
Runnable readRunnable = new Runnable() {
@Override
public void run() {
try {
demo.handleRead(readLock);
//demo.handleRead(lock);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
Runnable writeRunnable = new Runnable() {
@Override
public void run() {
try {
demo.handleWrite(writeLock, new Random().nextInt());
//demo.handleWrite(lock, new Random().nextInt());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
for (int i = 0; i < 18; i++) {
new Thread(readRunnable).start();
}
for (int i = 18; i < 20; i++) {
new Thread(writeRunnable).start();
}
}
}
上述代码中,第 18 行和第 28 行分别模拟了一个非常耗时的操作,让线程耗时 1 秒,它们分别对应读耗时和写耗时。
代码第 41 和 52 行,分别是读线程和写线程。
在这里,第 41 行使用读锁,第 42 行使用写锁。
第 60 ~ 62 行开启了 18 个读线程,第 64 ~ 66 行,开启了两个写线程。
由于这里使用了读写分离,因此,读线程完全并行,而写会阻塞读,因此,实际上这段代码运行大约 2 秒多就能结束(写线程之间实际是串行的)。
而如果使用第 42 行代替第 41 行,使用第 53 行代替第 52 行执行上述代码,即使用普通的重入锁代替读写锁,所有的读和写线程之间也都必须相互等待,因此整个程序的执行时间将长达 20 余秒。