readWriteLock
简介
现实中有这样一种场景:对共享资源有读和写的操作,且写操作没有读操作那么频繁。在没有写操作的时候,多个线程同时读一个资源没有任何问题,所以应该允许多个线程同时读取共享资源;但是如果一个线程想去写这些共享资源,就不应该允许其他线程对该资源进行读和写的操作了。
针对这种场景,JAVA的并发包提供了读写锁ReentrantReadWriteLock,它表示两个锁,一个是读操作相关的锁,称为共享锁;一个是写相关的锁,称为排他锁,描述如下:
线程进入读锁的前提条件:
没有其他线程的写锁,
没有写请求或者有写请求,但调用线程和持有锁的线程是同一个。
线程进入写锁的前提条件:
没有其他线程的读锁
没有其他线程的写锁
而读写锁有以下三个重要的特性:
(1)公平选择性:支持非公平(默认)和公平的锁获取方式,吞吐量还是非公平优于公平。
(2)重进入:读锁和写锁都支持线程重进入。
(3)锁降级:遵循获取写锁、获取读锁再释放写锁的次序,写锁能够降级成为读锁。
package com.ln.juc.utils.locks;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
/**
* @ProjectName: java-concurrency
* @Package: com.ln.juc.utils.locks
* @Name:ReadWriteLockExample
* @Author:linianest
* @CreateTime:2021/1/15 19:57
* @version:1.0
* @Description TODO:ReentrantReadWriteLock
*/
public class ReadWriteLockExample {
// 公平的显示锁lock
private final static ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock(true);
private final static Lock readLock = readWriteLock.readLock();
private final static Lock writeLock = readWriteLock.writeLock();
private final static List<Long> data = new ArrayList<>();
public static void main(String[] args) throws InterruptedException {
new Thread(ReadWriteLockExample::write).start();
new Thread(ReadWriteLockExample::write).start();
TimeUnit.MILLISECONDS.sleep(100);
new Thread(ReadWriteLockExample::read).start();
new Thread(ReadWriteLockExample::read).start();
}
public static void write() {
try {
writeLock.lock();
data.add(System.currentTimeMillis());
System.out.println(Thread.currentThread().getName()+" is writing data");
TimeUnit.MILLISECONDS.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
writeLock.unlock();
}
}
public static void read() {
try {
readLock.lock();
data.forEach(System.out::println);
TimeUnit.MILLISECONDS.sleep(100);
System.out.println(Thread.currentThread().getName() + "=============");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
readLock.unlock();
}
}
}
ReentrantReadWriteLock优势及使用
1)优势
多线程读取并修改一个资源时,我们过去通常使用 synchronized同步锁,这个是有性能损失的,很多情况下:资源对象总是被大量并发读取,偶尔有一个线程进行修改,也就是说:以读为主,修改不是 很频繁,那么我们在JDK5.0中用ReentrantReadWriteLock就获得比synchronized更高并发性能,高并发性能是我使用 JDK5.0主要目的,而不是annotation和泛型等设计优点。
ReentrantReadWriteLock被大量使用在缓存中,因为缓存中的对象总是被共享大量读操作,偶尔修改这个对象中的子对象,比如状态,我们可以使用ReentrantReadWriteLock对根对象中生命周期短的子对象在内存中直接更新,不必依赖数据库锁,这又是一个摆脱数据库锁的进步。
2)使用
ReentrantReadWriteLock竞争条件
ReentrantReadWriteLock会使用两把锁来解决问题,一个读锁,一个写锁
线程进入读锁的前提条件:
没有其他线程的写锁,
没有写请求或者有写请求,但调用线程和持有锁的线程是同一个
线程进入写锁的前提条件:
没有其他线程的读锁
没有其他线程的写锁
。。。。。。
private SomeClass someClass; //锁的资源
private final ReadWriteLock lock = new ReentrantReadWriteLock();
private final Lock r = lock.readLock();
private final Lock w = lock.writeLock();
。。。。。。
//读方法
。。。。。。
r.lock();
try {
result = someClass.someMethod();
} catch (Exception e) {
// process
} finally {
r.unlock();
}
。。。。。。。
//写方法
。。。。。。
//产生新的SomeClass实例tempSomeClass
。。。。。。。
w.lock();
try{
//释放老的资源
this.someClass.dispose();
//更新成新的实例
this.someClass = tempSomeClass;
}finally{
w.unlock();
}
。。。。。。。
3)性能测试
接下来对比下使用ReentrantReadWriteLock和不使用任何锁的性能比较情况:
我们使用100个读线程并发进行压力测试,发现在100%读的情况,性能没有任何损失,
之后我们在100个读线程的基础上加了一个写线程,每分钟写一次,性能几乎没有损失。
4)小结
使用ReentrantReadWriteLock可以推广到大部分读,少量写的场景,因为读线程之间没有竞争,所以比起sychronzied,性能好很多。
如果需要较为精确的控制缓存,使用ReentrantReadWriteLock倒也不失为一个方案。
ReadWriteLock需要严格区分读写操作,如果读操作使用了写入锁,那么降低读操作的吞吐量,如果写操作使用了读取锁,那么就可能发生数据错误。
另外ReentrantReadWriteLock还有以下几个特性:
- 公平性
- 非公平锁(默认) 这个和独占锁的非公平性一样,由于读线程之间没有锁竞争,所以读操作没有公平性和非公平性,写操作时,由于写操作可能立即获取到锁,所以会推迟一个或多个读操作或者写操作。因此非公平锁的吞吐量要高于公平锁。
- 公平锁 利用AQS的CLH队列,释放当前保持的锁(读锁或者写锁)时,优先为等待时间最长的那个写线程分配写入锁,当前前提是写线程的等待时间要比所有读线程的 等待时间要长。同样一个线程持有写入锁或者有一个写线程已经在等待了,那么试图获取公平锁的(非重入)所有线程(包括读写线程)都将被阻塞,直到最先的写 线程释放锁。如果读线程的等待时间比写线程的等待时间还有长,那么一旦上一个写线程释放锁,这一组读线程将获取锁。
- 重入性
- 读写锁允许读线程和写线程按照请求锁的顺序重新获取读取锁或者写入锁。当然了只有写线程释放了锁,读线程才能获取重入锁。
- 写线程获取写入锁后可以再次获取读取锁,但是读线程获取读取锁后却不能获取写入锁。
- 另外读写锁最多支持65535个递归写入锁和65535个递归读取锁。
- 锁降级
- 写线程获取写入锁后可以获取读取锁,然后释放写入锁,这样就从写入锁变成了读取锁,从而实现锁降级的特性。
- 锁升级
- 读取锁是不能直接升级为写入锁的。因为获取一个写入锁需要释放所有读取锁,所以如果有两个读取锁视图获取写入锁而都不释放读取锁时就会发生死锁。
- 锁获取中断
- 读取锁和写入锁都支持获取锁期间被中断。这个和独占锁一致。
- 条件变量
- 写入锁提供了条件变量(Condition)的支持,这个和独占锁一致,但是读取锁却不允许获取条件变量,将得到一个
UnsupportedOperationException
异常。
- 写入锁提供了条件变量(Condition)的支持,这个和独占锁一致,但是读取锁却不允许获取条件变量,将得到一个
- 重入数
- 读取锁和写入锁的数量最大分别只能是65535(包括重入数)。