文章目录
是什么
读写锁在同一时刻可以允许多个读线程访问,但是写线程操作时,所有的读线程和其他写线程均被阻塞。
使用例子
public class LockTest {
private static ReadWriteLock lock = new ReentrantReadWriteLock();
/**
* 读取资源
*/
public void read() throws InterruptedException {
lock.readLock().lock();
try{
readSomething();
}finally {
lock.readLock().unlock();
}
}
/**
* 修改资源
*/
public void write() throws InterruptedException {
lock.writeLock().lock();
try{
writeSomething();
}finally {
lock.writeLock().unlock();
}
}
}
对比于排他锁
排他锁在同一时刻只允许一个线程进行访问,而读写锁在同一时刻可以允许多个读线程访问,但是在写线程访问时,其他线程会被阻塞,对于读多写少的场景并发性高。
其他方案(等待/通知)
等待/通知。在Java 5之前没有读写锁时候,当写操作开始时,所有晚于写操作的读操作均会进入等待状态,写操作完成并进行通知之后,所有等待的读操作才能继续执行。写操作需要通过synchronized进行同步。
继承结构
继承了ReadWriteLock接口,接口中只有两个方法
接口结构
- Lock readLock();
返回用于读取的锁。
- Lock writeLock();
返回用于写入的锁。
内部结构
按照惯例,先看一下内部结构,大概有个意识。
内部聚合了WriteLock,ReadLock。又都聚合了Sync(继承AQS)。
其中又有FairSync,NonfairSync 公平锁与非公平锁,都继承与Sync。
内部持有两把锁(读锁与写锁),并且根据Sync来实现功能,其中又分为公平锁方式与非公平锁方式。
/** 提供读锁的内部类 */
private final ReentrantReadWriteLock.ReadLock readerLock;
/** 提供写锁的内部类 */
private final ReentrantReadWriteLock.WriteLock writerLock;
/** 执行所有同步机制 */
final Sync sync;
Sync
获取写锁(独占锁)
AQS中被volatile修饰的代表锁状态的变量,在读写锁中被拆成了两半,高16位代表读锁的获取情况,低16位代表写锁的获取情况
1.获取写锁的状态
2.如果锁被其他线程获取,那么返回false
3.锁没有被获取,询问是否需要阻塞,如果需要阻塞,返回false,入队寻找时机休眠
4.不需要阻塞则尝试CAS获取锁
protected final boolean tryAcquire(int acquires) {
Thread current = Thread.currentThread();
int c = getState();
//获取写锁的状态(同步状态值的低16位)
int w = exclusiveCount(c);
//1.锁被获取的情况(可能读锁,也能写锁)
if (c != 0) {
//如果写锁的状态为0,那么直接返回false
if (w == 0
//判断一下持有锁的线程,是否是当前线程,(需要考虑锁的重入)
|| current != getExclusiveOwnerThread())
return false;
//重入次数过多,抛出异常,(因为状态state只有一半表示写锁的状态)
if (w + exclusiveCount(acquires) > MAX_COUNT)
throw new Error("Maximum lock count exceeded");
//锁重入
setState(c + acquires);
return true;
}
//2.锁没有被获取,询问是否需要阻塞(公平锁还是非公平锁处理方式不一致,也就是在这里体现出公平与非公平锁)。
//writerShouldBlock公平锁与非公平锁的判断情况不一致。后续分析具体差别
if (writerShouldBlock() ||
//假设writerShouldBlock返回false,不需要阻塞。就会去尝试CAS修改同步状态。这里的c必定是0,acquires是1
//CAS成功说明没有线程竞争,那么代表锁的状态c的值,从0->1,成功获取到锁
!compareAndSetState(c, c + acquires))
//线程需要进入同步队列,或者CAS失败,需要返回false,让线程进入同步队列(AQS模板方法)
return false;
//3.成功获取到锁,设置持有锁的线程为当前线程
setExclusiveOwnerThread(current);
return true;
}
**获取写锁的状态(同步状态值的低16位)**在读写锁中,将同步状态值一分为二,高十六位为读的状态,低16位为写锁的状态。
static int exclusiveCount(int c) {
return c & EXCLUSIVE_MASK;
}
static final int EXCLUSIVE_MASK = (1 << 16) - 1;
1左移动16位,再减1
1: 0000 0000 0000 0000 0000 0000 0000 0001 1
左移29位: 0000 0000 0000 0001 0000 0000 0000 0000
减1: 0000 0000 0000 0000 1111 1111 1111 1111
任何数&EXCLUSIVE_MASK,舍弃高16位,保留低16位,得到写锁被线程获取的状态
是否需要进入同步队列的判断(公平锁/非公平锁的体现)
队头节点等于当前线程允许直接CAS
队头节点是当前线程需要进入AQS的同步队列
final boolean writerShouldBlock() {
Node t = tail;
Node h = head;
Node s;
if(h != t){
//false: 头节点后继不为null,并且后继节点等于当前线程(队头)
//true: 头节点后继不为null或者后继节点不等于当前节点(不在队头)
return (s = h.next) == null || s.thread != Thread.currentThread()
} else {
//队列都空了,允许直接CAS
return false;
}
}
非公平锁永远都允许先直接CAS,而不用入同步队列
final boolean writerShouldBlock() {
return false;
}
不允许直接CAS,或者CAS失败进入同步队列等待
tryAcquire竞争锁失败了,线程构造成节点进入AQS的同步队列中。
后续情况在ReentrantLock中分析过了,不再赘述
public final void acquire(long arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
释放写锁
1.将同步状态值减1
2.返回锁是否全部释放完毕
protected final boolean tryRelease(int releases) {
//只有持有锁的线程才能释放锁,否则抛出错误
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
//状态减一
int nextc = getState() - releases;
//判断锁是否全部释放完毕(可重入锁可以多次获取锁)
//低16位表示写锁的状态
boolean free = exclusiveCount(nextc) == 0;
if (free)
//清除锁的被持有线程
setExclusiveOwnerThread(null);
setState(nextc);
//只有全部释放完才会返回true
return free;
}
获取读锁(共享锁)
tryAcquireShared在AQS也是未实现的方法,留给子类去实现。典型的模板设计模式
public final void acquireShared(int arg) {
if (tryAcquireShared(arg) < 0)
doAcquireShared(arg);
}
1.如果写锁被其他线程获取,直接返回-1,乖乖排队去。
2.判断是否需要阻塞,如果需要阻塞直接进入到fullTryAcquireShared方法,这个方法后面再看有对阻塞做出处理
3.不需要阻塞就一直尝试CAS,CAS成功做一些记录工作。失败进入入fullTryAcquireShared方法
protected final int tryAcquireShared(int unused) {
Thread current = Thread.currentThread();
int c = getState();
//1.写锁被其他线程获取了,直接返回-1,未获得锁
if (exclusiveCount(c) != 0 &&
//并且保证线程的拥有者不是自身,后续会讲解到锁降级的情况,要考虑到
getExclusiveOwnerThread() != current)
return -1;
//2.运行到这儿说明没有线程获取写锁,或者拥有锁的人是自身
//获取读锁的数量
int r = sharedCount(c);
//(公平锁还是非公平锁处理方式不一致,也就是在这里体现出公平与非公平锁),假设false执行CAS
if (!readerShouldBlock() &&
//获取读锁的线程数量是否超过最大值
r < MAX_COUNT &&
//CAS设置同步状态值,+ SHARED_UNIT是因为高16位才表示读锁的个数
compareAndSetState(c, c + SHARED_UNIT)) {
//设置同步状态值成功,做一些记录工作(暂时不用去看)
if (r == 0) {
// firstReader是把读锁状态从0变成1的那个线程(第一个获取读锁的线程)
firstReader = current;
firstReaderHoldCount = 1;
//当前线程是第一次获取读锁的线程重入时,直接使用firstReaderHoldCount进行++
} else if (firstReader == current) {
firstReaderHoldCount++;
} else { //从ThreadLocal中获取当前线程重入读锁的次数,然后自增下。
//缓存的上一个线程的HoldCounter
HoldCounter rh = cachedHoldCounter;
if (rh == null || rh.tid != getThreadId(current))
//不是同一个线程,从线程的ThreadLocal中取
cachedHoldCounter = rh = readHolds.get();
else if (rh.count == 0)
//同一个线程,将缓存中的HoldCounter设置到线程的ThreadLocal
readHolds.set(rh);
//获取到读锁+1(rh是保存在各自线程中的一份副本,其他线程不可访问,只有自身线程可以访问)
//这个else中做的东西其实就是讲各自线程获取锁的次数记录到ThreadLocal中
rh.count++;
}
//竞争到读锁,返回true,不需要进入AQS的同步队列
return 1;
}
//3.补偿循环尝试
return fullTryAcquireShared(current);
}
公平锁获取读锁
与获取写锁一致
final boolean readerShouldBlock() {
Node t = tail;
Node h = head;
Node s;
if(h != t){
//false: 头节点后继不为null,并且后继节点等于当前线程(队头)
//true: 头节点后继不为null或者后继节点不等于当前节点(不在队头)
return (s = h.next) == null || s.thread != Thread.currentThread()
} else {
//队列都空了,允许直接CAS
return false;
}
}
非公平锁获取读锁
如果同步队列队头节点为写线程的话,那么就需要进入同步队列。
为了避免写线程无限期地饿死
试想一下,写线程在同步队列中等待,被持有的是读锁,持续的有读线程都可以获取读锁,那么写线程将很难获得写多,最终造成饥饿
final boolean readerShouldBlock() {
Node h, s;
//true:需要入同步队列,头队列非空,队头节点为写节点
//false:不需要入同步队列 队列为空,或者队头节点为读节点
return
//头结点不等于null
(h = head) != null &&
//后继节点不为null
(s = h.next) != null &&
//后继节点不是读节点
!s.isShared() &&
//后继节点线程不为空
s.thread != null;
}
fullTryAcquireShared
在什么情况下会进入到fullTryAcquireShared
- 目前没有线程获取写锁(有的话直接进入同步队列排队了)
- 或者CAS失败了。其上情况并不能确定线程一定没有拥有获取锁的资格。(有可能是重入锁的情况或者有其他读线程在竞争CAS)
fullTryAcquireShared作为一个补偿方法
方法中代码中对需要阻塞的情况做了处理如果是CAS失败,则会通过循环进行CAS的尝试。
final int fullTryAcquireShared(Thread current) {
HoldCounter rh = null;
for (;;) {
int c = getState();
//写锁被其他线程获取了,直接返回-1,表示失败(代码一致)
if (exclusiveCount(c) != 0) {
if (getExclusiveOwnerThread() != current)
return -1;
//对于需要进入同步队列的处理(公平锁非公平锁处理不一致)
} else if (readerShouldBlock()) {
// 如果这个线程是第一个获取读锁的线程,线程一定属于重入,直接放行进入循环CAS
//与上述代码一致,不再赘述
if (firstReader == current) {
} else {
if (rh == null) {
rh = cachedHoldCounter;
//先看本地缓存是否为Null,或者与当前线程id不一致
//进行一个ThreadLocal初始化
if (rh == null || rh.tid != getThreadId(current)) {
rh = readHolds.get();
if (rh.count == 0)
readHolds.remove();
}
}
//线程并不是重入线程,并且已经判断需要进入同步队列
if (rh.count == 0)
return -1;
}
}
if (sharedCount(c) == MAX_COUNT)
throw new Error("Maximum lock count exceeded");
//运行到这一步说明不需要进入AQS的同步队列,或者是进行锁的重入
if (compareAndSetState(c, c + SHARED_UNIT)) {
//CAS成功做一些记录
if (sharedCount(c) == 0) {
firstReader = current;
firstReaderHoldCount = 1;
} else if (firstReader == current) {
firstReaderHoldCount++;
} else {
if (rh == null)
rh = cachedHoldCounter;
if (rh == null || rh.tid != getThreadId(current))
rh = readHolds.get();
else if (rh.count == 0)
readHolds.set(rh);
rh.count++;
cachedHoldCounter = rh; // cache for release
}
//获取锁成功
return 1;
}
}
}
读锁的获取相对会比较麻烦
- 先判断是否有线程获得写锁,如果有那么直接进入同步队列,寻找休眠的机会线程休眠
- 如果写锁没有被获取,根据锁的类型判断线程是否应该被阻塞
- 如果不需要阻塞的话,则自旋CAS设置读锁的状态,并且将线程获取读锁的情况记录在ThreadLocal中
释放读锁(共享锁)
protected final boolean tryReleaseShared(int unused) {
Thread current = Thread.currentThread();
//第一个获取读锁的线程
if (firstReader == current) {
if (firstReaderHoldCount == 1)
firstReader = null;
else
firstReaderHoldCount--;
} else {
//将线程中存在ThreadLocal减一处理
HoldCounter rh = cachedHoldCounter;
if (rh == null || rh.tid != getThreadId(current))
rh = readHolds.get();
int count = rh.count;
if (count <= 1) {
readHolds.remove();
if (count <= 0)
throw unmatchedUnlockException();
}
--rh.count;
}
//循环CAS修改同步值状态
for (;;) {
int c = getState();
//高16位代表读锁的状态
int nextc = c - SHARED_UNIT;
if (compareAndSetState(c, nextc))
//==0也没有线程获取写锁
return nextc == 0;
}
}
结构
- static final class HoldCounter
用来记录线程已经获取了多少次读锁的内部类
- static final class ThreadLocalHoldCounter extends ThreadLocal<HoldCounter>
ThreadLocal,并且泛型指定位HoldCounter,初始化值为0。
以上两个内部类,用来实现读锁的可重入功能,如果值为0那么说明线程在之前没有获取读锁,反之线程已经是获取了读锁的并且没有释放。
核心属性
- private transient ThreadLocalHoldCounter readHolds;
ThreadLocal。当前线程持有的重入读锁数目。每重入一次就会++。每释放一次–,当线程的hold count 变为0时被移除。
- private transient HoldCounter cachedHoldCounter;
相对于当前最后一个获取读锁的线程
- private transient Thread firstReader = null;
private transient int firstReaderHoldCount;
firstReader 是第一个获取了读锁的线程(没有释放锁)。firstReaderHoldCount 是 firstReader 的 hold count(即重入次数)
WriteLock 写锁
方法代理到Sync获取独占锁,释放独占锁。按照AQS的模板方法,如果获取锁失败,那么进入同步队列,等待唤醒。
AQS释放锁,唤醒同步队列中的后继线程。
- lock() 加锁
public void lock() {
sync.acquire(1);
}
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
- unlock() 释放锁
public void unlock() {
sync.release(1);
}
//AQS释放锁
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
ReadLock 读锁
其行为都是代理到Sync的,获取共享锁,释放独占锁。
- lock() 加锁
public void lock() {
sync.acquireShared(1);
}
public final void acquireShared(int arg) {
if (tryAcquireShared(arg) < 0)
doAcquireShared(arg);
}
//以共享的不间断模式进行获取
private void doAcquireShared(int arg) {
final Node node = addWaiter(Node.SHARED);
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();
if (p == head) {
//Sync实现的以为共享的方式获取锁
int r = tryAcquireShared(arg);
if (r >= 0) {
//设置队列的头部,并检查后继者是否为共享模式(读锁),是的话也进行唤醒
setHeadAndPropagate(node, r);
p.next = null;
if (interrupted)
selfInterrupt();
failed = false;
return;
}
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
- unlock() 释放锁
public void unlock() {
sync.releaseShared(1);
}
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
//唤醒后继线程,检查后继者是否为共享模式(读锁),是的话也进行唤醒
doReleaseShared();
return true;
}
return false;
}
锁降级
锁降级指的是写锁降级成为读锁。锁降级是指把持住(当前拥有的)写锁,再获取到读锁,随后释放(先前拥有的)写锁的过程。
public void processData() {
// 锁降级从写锁获取到开始
writeLock.lock();
try {
write();
readLock.lock();
} finally {
writeLock.unlock();
}
// 锁降级完成,写锁降级为读锁
try {
read();
} finally {
readLock.unlock();
}
}
锁降级中读锁的获取是否必要呢?答案是必要的。主要是为了保证数据的可见性,如果当前线程不获取读锁而是直接释放写锁,假设此刻另一个线程(记作线程T)获取了写锁并修改了数据,那么当前线程无法感知线程T的数据更新。