读写锁在同一时刻可以允许多个读线程访问,但是在写线程访问时,所有的读
线程和其他写线程均被阻塞。读写锁维护了一对锁,一个读锁和一个写锁,通过分离读锁和写锁,使得并发性相比一般的排他锁有了很大提升。
一般情况下,读写锁的性能都会比排它锁好,因为大多数场景读是多于写的。在读多于写的情况下,读写锁能够提供比排它锁更好的并发性和吞吐量。Java并发包提供读写锁的实现是ReentrantReadWriteLock,它提供的特性如表所示:
读写锁的实现分析
1 读写状态的设计
读写锁同样依赖自定义同步器来实现同步功能,而读写状态就是其同步器的同步状态。读写锁的自定义同步器需要在同步状态(一个整型变量)上维护多个读线程和一个写线程的状态,使得该状态的设计成为读写锁实现的关键。
如果在一个整型变量上维护多种状态,就一定需要“按位切割使用”这个变量,读写锁将变量切分成了两个部分,高16位表示读,低16位表示写,划分方式如图所示:
读写锁是如何迅速确定读和写各自的状态呢?答案是通过位运算。假设当前同步状态值为S,写状态等于S&0x0000FFFF(将高16位全部抹去),读状态等于S>>>16(无符号补0右移16位)。当写状态增加1时,等于S+1,当读状态增加1时,等于S+(1<<16),也就是S+0x00010000。
根据状态的划分能得出一个推论:S不等于0时,当写状态(S&0x0000FFFF)等于0时,则读状态(S>>>16)大于0,即读锁已被获取。
2 写锁的获取与释放
写锁是一个支持重进入的排它锁,获取情况有两种
- 如果当前线程已经获取了写锁,则增加写状态。
- 如果当前线程在获取写锁时,读锁已经被获取(读状态不为0)或者该线程不是已经获取写锁的线程,则当前线程进入等待状态,代码如下所示:
protected final boolean tryAcquire(int acquires) {
Thread current = Thread.currentThread();
int c = getState();
int w = exclusiveCount(c);
if (c != 0) {
// 存在读锁或者当前获取线程不是已经获取写锁的线程
if (w == 0 || current != getExclusiveOwnerThread())
return false;
if (w + exclusiveCount(acquires) > MAX_COUNT)
throw new Error("Maximum lock count exceeded");
// 增加写锁状态
setState(c + acquires);
return true;
}
if (writerShouldBlock() || !compareAndSetState(c, c + acquires)) {
return false;
}
setExclusiveOwnerThread(current);
return true;
}
3 读锁的获取与释放
读锁是一个支持重进入的共享锁,它能够被多个线程同时获取。
- 在没有其他写线程访问(或者写状态为0)时,读锁总会被成功地获取,而所做的也只是(线程安全的)增加读状态
- 如果当前线程已经获取了读锁,则增加读状态。
- 如果当前线程在获取读锁时,写锁已被其他线程获取,则进入等待状态。
protected final int tryAcquireShared(int unused) {
for (;;) {
int c = getState();
int nextc = c + (1 << 16);
if (nextc < c)
throw new Error("Maximum lock count exceeded");
// 如果当前线程不是获取写锁的线程,则获取读锁失败
if (exclusiveCount(c) != 0 && owner != Thread.currentThread())
return -1;
// 如果当前线程是获取写锁的线程(锁降级)
// 或写锁未被线程获取,则获取读锁
if (compareAndSetState(c, nextc))
return 1;
}
}
4 锁降级
如果当前线程拥有写锁,然后将其释放,最后再获取读锁,这种分段完成的过程不能称之为锁降级。
锁降级是指把持住(当前拥有的)写锁,再获取到读锁,随后释放(先前拥有的)写锁的过程。
那么锁降级的设计的目的是什么呢?为何要在拥有写锁的前提下去获取读锁?
通过查看一些文章,写一下自己的理解:
锁降级的目的其实是为了让线程对数据变化敏感,如果先释放写锁,再获取读锁,可能在获取之前,会有其他线程获取到写锁,阻塞读锁的获取,就无法感知数据变化了。所以需要先hold住写锁,保证数据无变化,获取读锁,然后再释放写锁。
例如有多个线程对同一块数据区域data进行读写操作,要求对每次数据的更改敏感。假设t1时刻data区域被写线程将状态s0更改为s1,更改完后若直接释放锁,那么可能会有其他线程获取写锁,将data区域的状态从s1更改为s2,这样一来整个过程就无法感知到data区域的s1状态。
如果采用了锁降级,那么获取写锁的线程t将data区域状态更改为s1后便持有读锁,那么其它想获取写锁的线程将会阻塞,直到线程t将读锁释放,那么这个过程中将会感知到data区域的s1状态。