介绍
ReadWriteLock是JDK1.5提供的读写分离锁,它维护了一对锁,一个读锁和一个写锁。读写锁允许多个线程同时读,写写操作和读写操作依然需要互相等待和持有锁。如果系统中读操作次数远大于写操作次数,读写锁能够提供比排它锁更好的并发性和吞吐量。
读写锁访问约束情况如下:
| 读 | 写 | |
|---|---|---|
| 读 | 非阻塞 | 阻塞 |
| 写 | 阻塞 | 阻塞 |
特性
Java并发包提供读写锁的实现是ReentrantReadWriteLock。如下为ReentrantReadWriteLock的特性:
| 特性 | |
|---|---|
| 公平锁选择 | 支持非公平(默认)和公平锁的获取方式,非公平吞吐量由于公平。公平锁中等待时间最长的线程优先获得锁,非公平锁中,线程获取访问许可的顺序是不确定的 |
| 重进入 | 该锁支持重进入,以读写线程为例:读线程在获得读锁以后,能够再次获得读锁。而写线程获得写锁以后能够再次获得写锁,同时也可以获取读锁 |
| 锁降级 | 遵循获取写锁、获取读锁再释放写锁的次序,写锁能够降级为读锁。读锁升级为写锁是不可以的(这样做会导致死锁,如果两个读线程试图同时升级为写线程,那么二者都不会释放读取锁) |
读写状态分析
读写锁有一对锁,一个读锁,一个写锁,那么读写锁是怎么维护这两个状态的那,下面我们来分析下。
读写锁同样依赖自定义同步器(AQS)的同步状态来实现。需要在同步状态(一个整型变量)上维护多个读线程和一个写线程的状态。在一个整型变量维护多种状态则需要“按位拆分”。
如下图所示展示了读写锁的状态的划分方式。
可以看出读写锁将变量高16位表示读,低16位表示写。上图表示一个线程获取了写锁并重入了两次,同时也连续两次获取了读锁。
读写锁确定读和写的状态是通过位运算。假设当前状态为S,
写状态:S & 0x0000FFFF (将高16位全部抹去)
读状态:S>>>16 (无符号补0右移16位)
写状态增加1:S+1
读状态增加1: S + (1<<16) 或者 S+0x00010000
写锁的获取和释放
protected final boolean tryAcquire(int acquires) {
/*
* Walkthrough:
* 1. If read count nonzero or write count nonzero
* and owner is a different thread, fail.
* 2. If count would saturate, fail. (This can only
* happen if count is already nonzero.)
* 3. Otherwise, this thread is eligible for lock if
* it is either a reentrant acquire or
* queue policy allows it. If so, update state
* and set owner.
*/
Thread current = Thread.currentThread();
//获取同步状态
int c = getState();
//获取写锁的数量 (S & 0x0000FFFF)
int w = exclusiveCount(c);
if (c != 0) {
// (Note: if c != 0 and w == 0 then shared count != 0)
//1.读锁不等0则获取失败
//2.写锁不等于0并且持有锁的线程不是当前线程则获取失败
if (w == 0 || current != getExclusiveOwnerThread())
return false;
//写锁的重入次数不能超过MAX_COUNT(2的16次方)
if (w + exclusiveCount(acquires) > MAX_COUNT)
throw new Error("Maximum lock count exceeded");
// Reentrant acquire
//重进入获取写锁
setState(c + acquires);
return true;
}
//1.线程是否要阻塞,非公平锁返回false,公平锁需要判断等待队列是否有线程,有的话返回true,没有的话返回false
//2.当writerShouldBlock()返回false,则通过compareAndSetState原子操作更新同步状态,修改成功返回true,修改失败则有其它线程已经变更了同步状态则返回false
if (writerShouldBlock() ||
!compareAndSetState(c, c + acquires))
return false;
setExclusiveOwnerThread(current);
return true;
}
/*
* Note that tryRelease and tryAcquire can be called by
* Conditions. So it is possible that their arguments contain
* both read and write holds that are all released during a
* condition wait and re-established in tryAcquire.
*/
protected final boolean tryRelease(int releases) {
//检查当前线程是否是所有者
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
//写锁的数量减去releases
int nextc = getState() - releases;
//减去releases后写锁的数量等于0,则设置独占所有者线程属性为null
boolean free = exclusiveCount(nextc) == 0;
if (free)
setExclusiveOwnerThread(null);
//更新同步状态
setState(nextc);
return free;
}
读锁的获取和释放
读锁获取时,如果其它线程已经获取到写锁,则获取读锁失败,进入等待状态。如果当前线程获取了写锁或者写锁未被获取,则当前线程增加读状态(线程安全,依靠CAS保证),成功获取读锁。
读锁的释放(线程安全,可能多个读锁同时释放读锁)减少读状态(减少1<<16)。
读锁获取和释放的代码原理在此不做详细分析。
锁降级
写锁降级为读锁即为锁降级,下面看一个锁降级例子:
public class ReadWriteDemo {
private static ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
private static Lock readLock = readWriteLock.readLock();
private static Lock writeLock = readWriteLock.writeLock();
private static volatile Boolean update;
public void processData(){
readLock.lock();
if(!update){
//必须先释放读锁
readLock.unlock();
//锁降级从写锁获取开始
writeLock.lock();
try {
if(!update){
//准备数据的流程(略)
update = true;
}
readLock.lock();
} finally {
writeLock.unlock();
}
//锁降级完成,写锁降级为读锁
}
try {
//使用数据的流程(略)
} finally {
readLock.unlock();
}
}
}
锁降级中读锁的获取是必要的,可以获取当前线程写锁变更的数据(该数据在锁降级的读锁获取后操作范围不会被其它线程修改)。例如本例中在锁降级后释放写锁,此时该线程持有读锁,则其他线程获取不到写锁,则不能对共享数据进行变更。
读写锁不支持升级的原因
读写锁升级说的是读锁升级为写锁(持有读锁,获取写锁,最后释放读锁的过程)。
不支持升级的主要原因我认为是以下两点:
-
不能保证数据的可见性(例如多个线程获取读锁,其中一个线程获取了写锁更新共享数据,更新后的数据对其它读线程是不可见的)
-
可能会造成死锁(例如一个线程A获取了读锁,在升级获取写锁前,另外一个线程B也获取了读锁,在A线程获取写锁的时候由于B线程获取了读锁,所以会等待B释放读锁,B线程后来也去升级写锁,此时A锁持有读锁,所以会去等待A释放读锁。这样就会造成死锁的情况)
读写锁使用简单示例:
public class ReadWriteLockDemo {
private static Lock lockOri = new ReentrantLock();
private static ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
private static Lock readLock = readWriteLock.readLock();
private static Lock writeLock = readWriteLock.writeLock();
private int value;
/**
* 此用例用于计时
*/
private static CountDownLatch countDownLatch = new CountDownLatch(20);
public Object handleRead(Lock lock) throws InterruptedException {
// 模拟读操作
lock.lock();
try {
//读操作的耗时越多,读写锁的优势就越明显
Thread.sleep(1000);
return value;
} finally {
lock.unlock();
countDownLatch.countDown();
}
}
public Object handleWrite(Lock lock, int index) throws InterruptedException {
lock.lock();
try {
Thread.sleep(1000);
value = index;
return value;
} finally {
lock.unlock();
countDownLatch.countDown();
}
}
public static void main(String[] args) {
final ReadWriteLockDemo test1Controller = new ReadWriteLockDemo();
Runnable readRunnable = new Runnable() {
@Override
public void run() {
try {
test1Controller.handleRead(readLock); // 1
//test1Controller.handleRead(lockOri); // 2
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
Runnable writeRunnable = new Runnable() {
@Override
public void run() {
try {
test1Controller.handleWrite(writeLock, new Random().nextInt()); // 3
//test1Controller.handleWrite(lockOri, new Random().nextInt()); //4
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
long start = System.currentTimeMillis();
for (int i = 0;i < 18; i++){
new Thread(readRunnable).start();
}
for (int i = 18; i<20; i++){
new Thread(writeRunnable).start();
}
try {
countDownLatch.await();
long end = System.currentTimeMillis();
System.out.println("执行耗时:" + (end - start)/1000 + "s");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
第一次放开代码中的1和3标注的代码,注释2和4的标注的代码,可见使用读写锁(ReentrantReadWriteLock)执行后会输出:
执行耗时:4s
第二次放开代码中的2和4标注的代码,注释1和3的标注的代码,可见使用重入锁(ReentrantLock)执行后会输出:
执行耗时:20s
上面的示例使用读写锁耗时大概是4s,使用重入锁的话耗时大概为20s。可见读操作次数远大于写操作次数,读写锁能够提供比排它锁更好的并发性和吞吐量。
参考书籍:《Java高并发程序设计(第2版)》《Java并发编程实战》《Java并发编程的艺术》