前面学习的synchronized、lock与ReentrantLock都是独占锁(有的也称为互斥锁),了解锁的概念的时候,有一种锁叫共享锁,今天就学习一下共享锁向光的接口和实现类。
ReadWriteLock的了解
在J.U.C(java.util.concurrent)中提供了一种共享锁的实现,也就是读写锁。
ps:
我对读写锁这个词印象颇深,刚开始工作的时候,当时处于对并发完全不知道的状态,面试官问我什么是读写锁,结果整个人都懵了,啥东西,我们说的是同一种编程语言么。。。。
读写锁实际上是为了解决一种特殊场景问题而提出一种概念。举例说明:
有一个气候发布系统,共有三十个不同类别的气候展示面板,这三十个面板展示的数据都是来自气候传感器,有一个线程每隔一段时间从传感器读取数据,更新到对应的变量上,而三十个面板的线程随时都可能读取这些变量的值,并且要求读到的值是一样的。
三十个面板线程对应读锁,数据刷新线程对应写锁。
这种场景要求:
1、读取的线程读取到的数据都是最新的;
2、写数据的线程写数据的时候,读线程不能读取数据;
在java源码中,读写锁的顶级接口里面只提供了两个基本方法:
public interface ReadWriteLock {
/**
* Returns the lock used for reading.
*
* @return the lock used for reading
*/
Lock readLock();
/**
* Returns the lock used for writing.
*
* @return the lock used for writing
*/
Lock writeLock();
}
但是正如源码注释里面所说,虽然看起来很简单,但是实现类仍然需要注意几个方面:
1、写锁优先级高于读锁
2、重入的问题;
3、写锁降级问题和读锁升级问题;
另外读写锁的性能并不一定优于独占锁,性能受限与读写的频率和操作时间。源码是这么说的:
Whether or not a read-write lock will improve performance over the use of a mutual exclusion lock depends on the frequency that the data is read compared to being modified, the duration of the read and write operations, and the contention for the data - that is, the number of threads that will try to read or write the data at the same time. For example, a collection that is initially populated with data and thereafter infrequently modified, while being frequently searched (such as a directory of some kind) is an ideal candidate for the use of a read-write lock. However, if updates become frequent then the data spends most of its time being exclusively locked and there is little, if any increase in concurrency. Further, if the read operations are too short the overhead of the read-write lock implementation (which is inherently more complex than a mutual exclusion lock) can dominate the execution cost, particularly as many read-write lock implementations still serialize all threads through a small section of code. Ultimately, only profiling and measurement will establish whether the use of a read-write lock is suitable for your application.
在使用互斥锁时, 读写锁定是否会提高性能取决于读取数据的频率与被修改的次数、读写操作的持续时间以及数据的争用, 这是, 将尝试同时读取或写入数据的线程数。 例如, 最初用数据填充的集合, 然后在频繁地搜索 (如某种目录) 的情况下不经常修改, 是使用读写锁的理想候选对象。但是, 如果更新变得频繁, 那么数据的大部分时间都是完全锁定的, 并且在并发性方面的增加很少。此外, 如果读取操作太短, 读写锁实现的开销 (本质上比互斥锁更复杂) 可以控制执行成本, 特别是许多读写锁实现仍然序列化 所有线程通过一小部分代码。最终, 只有性能分析和度量才能确定是否使用读写锁定适合您的应用程序。
ReadWriteLock的实现ReentrantReadWriteLock
想要理解这个实现,需要理解一下几个方面:
– 公平锁与非公平锁的实现
– 读写锁的实现
– 读写锁的使用
– 锁的升级与降级
其中公平锁与非公平锁的实现和ReentrantLock是一样的,不需要再详细了解。
读写锁的实现
在之前额学习中了解到,lock实现类中的同步是通过AQS同步器来实现的,在ReentrantReadWriteLock也是通过同步器实现的,只不过读锁调用的是同步器的获取共享锁方法,写锁调用的是独占锁方法。
读锁的实现:
public static class ReadLock implements Lock, java.io.Serializable {
public void lock() {
sync.acquireShared(1);
}
public void lockInterruptibly() throws InterruptedException {
sync.acquireSharedInterruptibly(1);
}
........
}
实际上就是通过同步器调用获取共享锁方法。
写锁的实现:
public static class WriteLock implements Lock, java.io.Serializable {
public void lock() {
sync.acquire(1);
}
public void lockInterruptibly() throws InterruptedException {
sync.acquireInterruptibly(1);
}
.........
}
写锁获取的都是独占锁。
既然知道读写锁的实现,那同步器是谁?
读锁的构造方法:
protected ReadLock(ReentrantReadWriteLock lock) {
sync = lock.sync;
}
写锁的构造方法:
protected WriteLock(ReentrantReadWriteLock lock) {
sync = lock.sync;
}
读写锁的初始化:
public ReentrantReadWriteLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
readerLock = new ReadLock(this);
writerLock = new WriteLock(this);
}
fair参数就不用说了(是否是公平锁),读写锁初始化的参数是this,也就是当前对象,当前对象是ReentrantReadWriteLock,也就是说读写锁使用的AQS都是ReentrantReadWriteLock的同步器,读锁使用其中共享锁方法,写锁使用独占锁方法。
读写锁的使用
常用写法:
public class ReadWriteLockTest {
public static void main (String[] args) {
Test test = new Test();
new WriteThread(test).start(); // 写数据
for (int i = 0; i < 20; i++) { //读取线程
new ReadThread(test).start();
}
new WriteThread(test).start(); // 写数据
}
static class WriteThread extends Thread {
private Test test;
public WriteThread (Test test) {
this.test = test;
}
@Override
public void run () {
test.add();
}
}
static class ReadThread extends Thread {
private Test test;
public ReadThread (Test test) {
this.test = test;
}
@Override
public void run () {
test.getNum();
}
}
static class Test {
private int num;
private ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
private ReentrantReadWriteLock.ReadLock readLock = rwl.readLock();
private ReentrantReadWriteLock.WriteLock writeLock = rwl.writeLock();
private int getNum() {
readLock.lock();
try {
System.out.println("读数据==" + num);
return num;
} finally {
readLock.unlock();
}
}
private void add() {
writeLock.lock();
try {
num++;
System.out.println("写数据==" + num);
} finally {
writeLock.unlock();
}
}
}
}
输出结果:
读数据==0
读数据==0
读数据==0
读数据==0
读数据==0
写数据==1
读数据==1
读数据==1
读数据==1
读数据==1
读数据==1
读数据==1
读数据==1
读数据==1
读数据==1
读数据==1
读数据==1
读数据==1
读数据==1
读数据==1
读数据==1
写数据==2
在大多数使用读写锁的时候,都是很多线程去读,少量的写线程写数据。这种写法中没有涉及降级升级的问题。
锁的升级与降级
之前学习同步的相关知识中都没有设计的升级降级的问题。
在读写锁中存在两种锁,读锁和写锁,两种锁的并发级别是不一样的,写锁的并发级别高于读锁,读写锁有两个特征:
- 写锁会阻塞其它的写锁和所有的读锁
- 读锁会阻塞写锁,不会阻塞读锁
所谓降级:写锁降级为读锁;
所谓升级:读锁升级为写锁;
首先一点,在ReentrantReadWritreLock中是不支持锁升级的,至于为什么,我不理解,网上很多人都说不支持,所以这里就主要学习一下锁降级的问题。
这是根据网上的讲解写的一个例子:
public class DowngradeTest {
private boolean valid = false;
private int num = 0;
private ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
private ReentrantReadWriteLock.ReadLock readLock = rwl.readLock();
private ReentrantReadWriteLock.WriteLock writeLock = rwl.writeLock();
public static void main (String[] args) {
DowngradeTest test = new DowngradeTest();
new DowngradeThread(test).start();
SleepUtil.sleep(1000);
new WriteThread(test).start();
}
static class WriteThread extends Thread {
private DowngradeTest test;
public WriteThread (DowngradeTest test) {
this.test = test;
}
@Override
public void run () {
test.changeData();
}
}
static class DowngradeThread extends Thread {
private DowngradeTest test;
public DowngradeThread (DowngradeTest test) {
this.test = test;
}
@Override
public void run () {
test.downgrade();
}
}
private void downgrade() {
while (true) {
readLock.lock();
if (valid) {
readLock.unlock();
writeLock.lock();
System.out.println("写锁第二次获取锁");
num++;
valid = false;
readLock.lock();
writeLock.unlock();
System.out.println("修改后的num===" + num);
System.out.println("修改后的valid====" + valid);
SleepUtil.sleep(500);
}
readLock.unlock();
}
}
private void changeData() {
writeLock.lock();
try {
System.out.println("写锁第一次修改数据");
valid = true;
num++;
} finally {
writeLock.unlock();
}
}
}
输出结果:
写锁第一次修改数据
写锁第二次获取锁
修改后的num===2
修改后的valid====false
readLock.lock();
writeLock.unlock();
网上说这两句代码的顺序不能错,否则就可能出现错误数据,我解释不了,但是<
System.out.println("修改后的num===" + num);
System.out.println("修改后的valid====" + valid);
在我看来,所谓升级降级,只不过是在读线程中使用了写锁,在写线程中使用了读锁。
总结
读写锁的特征:
- 写锁的并发级别高于读锁
- 读锁是共享锁,写锁是独占锁
- ReentrantReadWriteLock中写锁可以降级为读锁,读锁不能升级为写锁
- ReentrantReadWriteLock支持公平锁和非公平锁