目录
前言
在实际应用中,有种普遍的并发场景:那就是读多写少;这种情况下如果使用互斥锁实现的话效率却不尽人意,一般来说读操作并不对数据的完整性造成破坏,这种互斥等待显然是不合理的;这时候就可以用到我们今天所说的主角:ReadWriteLock,接下来就让我们一步步揭开它神秘的面纱
1、ReadWriteLock介绍
1.1、定义
ReadWriteLock管理一组锁,一个是只读的锁(共享锁),一个是只写的锁(互斥锁)。读锁可以在没有写锁的时候被多个线程同时持有,写锁是独占的(每次只能有一个写线程,但是同时可以有多个线程并发地读数据)
支持公平和非公平模式,是在ReentrantReadWriteLock的构造方法中传一个boolean类型的变量控制
1.2、读写锁遵循的原则
- 一个共享变量允许同时被多个读线程读取到
- 一个共享变量在同一时刻只能被一个写线程进行写操作
- 一个共享变量在被写线程执行写操作时,此时这个共享变量不能被读线程执行读操作
1.3、访问约束情况
- 读-读:不阻塞
- 读-写:阻塞
- 写-读:阻塞
- 写-写:阻塞
1.4、锁的升降级
不支持升级(读锁变成写锁):读锁还未释放时,此时获取写锁,就会导致写锁永久等待,相应的线程也会被阻塞而无法唤醒;目的也是保证数据可见性,如果读锁已被多个线程获取,其中任意线程获取了写锁并更新了数据(假设拥有读锁的线程又申请到写锁并修改了数据,实际上并不支持),则其更新对其它获取到的读锁的线程是不可见的。
支持降级(写锁变成读锁):可在拥有写锁的情况下再去获取读锁,随后释放先前拥有的写锁,需要注意的是需要手动释放写锁;获取读锁的目的主要是为了保证数据的可见性,如果当前线程不获取读锁而是直接释放写锁,假设此刻另一个线程获取了写锁并修改了数据,这一操作对于当前线程来说是不可知的,数据错的结果自然就错了。如果当前线程获取读锁,即遵循锁降级的步骤,则别的线程将会被阻塞,直到当前线程使用完数据并释放读锁后,其它线程才能获取写锁进行数据更新。
1.5、注意点
- 无论是读锁还是写锁,锁的释放操作都要放到finally{}代码块里
- 读锁调用newCondition()会抛出UnsupportedOperationException异常,也就是说读锁不支持条件变量
2、应用
public class Test {
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 static Integer num;
// 模拟读操作
private static void read(Lock lock){
lock.lock();
try {
TimeUnit.MILLISECONDS.sleep(500);
System.out.println(Thread.currentThread().getName()+"读操作完成");
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
lock.unlock();
}
}
// 模拟写操作
private static void write(Lock lock,Integer n){
lock.lock();
try {
TimeUnit.MILLISECONDS.sleep(500);
num = n;
System.out.println(Thread.currentThread().getName()+"写操作结束"+" num="+num);
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
lock.unlock();
}
}
public static void main(String[] args) {
// 测试ReentrantLock和ReentrantReadWriteLock效率
//Runnable read = ()->{read(lock);};
Runnable read = ()->{read(readLock);};
//Runnable write = ()->{write(lock,new Random().nextInt());};
Runnable write = ()->{write(writeLock,new Random().nextInt());};
for (int i=0;i<100;i++){
new Thread(read,"r"+i).start();
}
for (int i=0;i<10;i++){
new Thread(write,"w"+i).start();
}
}
}
我们对互斥锁和读写锁的效率进行简单的测试:启动一百个进行读操作线程和十个进行写操作的线程,线程每次都睡眠500毫秒模拟操作;分别使用互斥锁和读写锁进行测试
先看下互斥锁(ReentrantLock)的效果,可看出线程是同步执行的,用时60s左右
下面看看读写锁的表现,读操作是并行的,写操作串行,用时10s左右
从上面肉眼可见的看出读写锁在处理读多写少的场景下效率是大大高于互斥锁,因为读写锁可多个线程同时读,而互斥锁不可以,所以在此种场景下读写锁的效率要更高;实际工作中同步工具的选择要视具体场景而定。
有任何错误欢迎大家指正,转载请注名出处!