目录
前言
前面我们说了AQS
对独占锁的实现。接下来说一下AQS
对共享锁的实现。既然是共享锁那么也就意味着有多个线程是可以进入到临界区的。
上面形象的描述了共享锁和独占锁的区别。同一时刻能有多个线程进入临界区的锁为共享锁,只有一个线程能进入临界区的锁为独占锁。ReentrantLock
是对独占锁的典型的实现,而ReentrantReadWriteLock
也是对共享锁的经典应用。
什么是读写锁
说到共享锁不得不提到JUC
对共享锁的典型实现读写锁-ReentrantReadWriteLock
。读写锁的基本原则:
- 允许多个线程同时读共享变量
- 只允许一个线程写共享变量
- 如果一个写线程正在执行写操作 那么禁止读线程读共享变量
读写锁与互斥锁的一个重要区别就是读写锁允许多个线程同时读共享变量,而互斥锁是不允许的,这是读写锁在读多写少场景下性能优于互斥锁的关键。但读写锁的写操作是互斥的,当一个线程在写共享变量的时候,是不允许其他线程执行写操作和读操作。
所以不难看出ReentrantReadWriteLock
持有一把互斥锁和一把共享锁来解决读写同步的问题。
ReentrantReadWriteLock共享锁的获取
AQS
作为JUC包下面最重要的存在 即实现了独占锁的获取和释放,也实现了共享锁的获取和释放。例如我们文提到过的ReentrantReadWriteLock
同时拥有 独占锁 和共享锁 的功能。readLock
为共享锁 writeLock
为独占锁。我们以ReentrantReadWriteLock
获取共享锁为例来探讨 共享锁的获取。
public final void acquireShared(int arg) {
//尝试获取同步状态
if (tryAcquireShared(arg) < 0)
doAcquireShared(arg);
}
tryAcquireShared()
方法是AQS
留给子类实现的,我们以ReentrantReadWriteLock
为例。
由于ReentrantReadWriteLock
同时拥有了独占锁和共享锁 所以对共享锁的获取的实现比较复杂。
protected final int tryAcquireShared(int unused) {
Thread current = Thread.currentThread();
//获取共享状态
int c = getState();
//exclusiveCount这个是获取独占锁的重入次数 这里需要说明的是独占锁的重入次数是通过 共享状态的高16位来存储的。
//如果独占锁的重入次数不等于0 并且当前的独占线程不是自己 那么返回-1 表示无法获取
if (exclusiveCount(c) != 0 &&
getExclusiveOwnerThread() != current)
return -1;
//如果没有独占锁 获取共享锁的个数
int r = sharedCount(c);
//如果读线程被阻塞 并且共享锁个数小于最大个数 65535 并且CAS设置共享状态成功
//注意同步状态的高16位来保存 共享锁的次数 所以SHARED_UNIT是 1 << 16
if (!readerShouldBlock() &&
r < MAX_COUNT &&
compareAndSetState(c, c + SHARED_UNIT)) {
//如果共享锁为0 将当前线程赋值给 firstReader 并赋值重入数为1
if (r == 0) {
firstReader = current;
firstReaderHoldCount = 1;
} else if (firstReader == current) {
//如果r不为0 并且当前线程是第一个获取共享锁的线程 增加重入值
firstReaderHoldCount++;
} else {
//以上两种情况都不满足 当前线程不是第一次获取锁
//下面的代码是对HoldCounter做++ 操作 每一个线程都有一个计数器 用来保存可重入的次数
//但是第一次获取共享锁的线程没有 它是被缓存在共享变量firstReaderHoldCount中的
HoldCounter rh = cachedHoldCounter;
if (rh == null || rh.tid != getThreadId(current))
cachedHoldCounter = rh = readHolds.get();
else if (rh.count == 0)
readHolds.set(rh);
rh.count++;
}
return 1;
}
//进入完全重试循环
return fullTryAcquireShared(current);
}
以上代码为ReentrantReadWriteLock
对AQS
的tryAcquireShared
方法的实现。
exclusiveCount© != 0 &&
首先检查是否有线程获取独占锁,如果有并且获取独占锁的线程不是自己 那么直接返回获取同步状态失败。可以思考一下 这里为什么会判断当前线程是不是独占线程?其实这就是支持锁降级的过程。说白了就是从写锁降级到读锁的过程,在这个过程需要当前线程hold主写锁 等到当前线程获取到读锁之后再释放写锁
getExclusiveOwnerThread() != current- 接下来有三个条件表达式 判断读线程是否应该阻塞、获取共享锁的线程小于65535(这里也会统计重入次数)、CAS设置共享状态操作成功。
- 读线程是否应该被阻断
- 非公平锁的实现:
apparentlyFirstQueuedIsExclusive
队列中第一个节点是独占线程在等待。 - 公平锁的实现:
hasQueuedPredecessors
是否有比自己等待更久的线程在队列中。如果有获取共享锁的线程在等待那么为了公平就必须等待。如果有独占线程在等待那么和上面非公平锁一样也需要等待。
- 非公平锁的实现:
r < MAX_COUNT
ReentrantReadWriteLock
使用高16位来表示共享锁的获取次数这个值是65535
大于这个值获取共享锁的线程将等待。
- 读线程是否应该被阻断
- 满足上述三个条件 才会进入分支修改自己的可重入数。
ReentrantReadWriteLock
使用HoldCounter
来保存自己的可重入数。 - 多个线程抢夺共享锁失败就进入
fullTryAcquireShared()
方法
下面的fullTryAcquireShared()
方法任然是ReentrantReadWriteLock
的方法。
final int fullTryAcquireShared(Thread current) {
HoldCounter rh = null;
for (;;) {
//下面这个流程和上述的流程一样 判断是否独占线程获取了锁
int c = getState();
if (exclusiveCount(c) != 0) {
if (getExclusiveOwnerThread() != current)
return -1;
} else if (readerShouldBlock()) {
//这个分支 如果读线程需要阻塞的情况 上面我们说了 读线程需要阻塞 在非公平锁的情况下
//条件是 队列中有独占线程在等待 独占线程等待是因为有线程获取了共享锁
if (firstReader == current) {
//如果当前线程是第一个获取共享锁的线程 并且继续重入 那么继续进行竞争获取锁
} else {
//这个分支很好理解 如果线程的重入次数为0 从readHolds中移除 并且进入到等待队列
//重入次数不是0的线程 说明是在重入 那么直接让其去和别的线程竞争 进行自旋
if (rh == null) {
rh = cachedHoldCounter;
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");
//进行CAS竞争锁
if (compareAndSetState(c, c + SHARED_UNIT)) {
//竞争成功的去更改自己的计数器。
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;
}
}
}
上述方法处理竞争共享锁失败的线程 让其进行自旋获取的过程。
如果竞争失败 并且线程需要阻塞就会进入AQS的阻塞队列doAcquireShared(arg)
。
private void doAcquireShared(int arg) {
//addWaiter在上一篇文章已经介绍过了 唯一的区别这里传入的是一个共享节点的标识Node.SHARED
final Node node = addWaiter(Node.SHARED);
boolean failed = true;
//try代码块的逻辑就是将 构造的共享节点放入到等待队列中
try {
boolean interrupted = false;
for (;;) {
//获取当前节点的前置节点
final Node p = node.predecessor();
//如果当前节点的前置节点是头节点 就再次尝试获取共享同步状态
if (p == head) {
int r = tryAcquireShared(arg);
if (r >= 0) {
//设置头节点 如果下一个节点也是共享节点 则叫醒它
setHeadAndPropagate(node, r);
p.next = null; // help GC
//在自旋的过程中如果线程被中断了 只是设置一下线程被中断了 并不响应
if (interrupted)
selfInterrupt();
failed = false;
return;
}
}
//如果前一个节点不是头节点或者竞争资源失败 那么阻塞当前线程
//下面这个方法再上一篇文章解读过 这里就不略过 阻塞线程的操作
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
共享锁的获取比较简单 ReentrantReadWriteLock
的获取比较复杂。
ReentrantReadWriteLock共享锁的释放
ReentrantReadWriteLock
的tryReleaseShared
protected final boolean tryReleaseShared(int unused) {
//上半部分 将重入次数减1
Thread current = Thread.currentThread();
if (firstReader == current) {
if (firstReaderHoldCount == 1)
firstReader = null;
else
firstReaderHoldCount--;
} else {
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;
}
//下半部分使用 自旋来包装共享状态释放成功
for (;;) {
int c = getState();
int nextc = c - SHARED_UNIT;
if (compareAndSetState(c, nextc))
return nextc == 0;
}
}
上面ReentrantReadWriteLock
对tryReleaseShared
方法的实现主要分为两个部分,
- 将重入次数减1,第一次获取共享锁的线程的同步次数保存在变量
firstReaderHoldCount
中的 所以这里需要做一个判断 - 自旋来保证共享状态的释放 如果当前线程重入次数大于1 此处返回false
尝试释放同步状态成功就进入下面这个方法doReleaseShared()
private void doReleaseShared() {
for (;;) {
Node h = head;
if (h != null && h != tail) {
int ws = h.waitStatus;
if (ws == Node.SIGNAL) {
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
continue;
unparkSuccessor(h);
}
else if (ws == 0 &&
!compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
continue;
}
if (h == head)
break;
}
}
又是一个自旋操作,上述方法最主要的作用就是叫醒后续正在排队的线程。
- 如果当前线程的等待状态为
SIGNAL
表示后续节点处于等待状态,当前节点如果释放了同步状态需要叫醒后续的节点。所以如果CAS成功执行unparkSuccessor
操作,该操作是一个unpack
的过程. - 如果同步状态为0 需要将同步状态设置成
PROPAGATE
状态。而前面的获取的过程说到的setHeadAndPropagate
就和此处相关。有兴趣可以自行阅读。
ReentrantReadWriteLock独占锁的获取
通过之前的文章不难知道 独占锁的获取是通过AQS的acquire()
方法。
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
上面的代码和上一篇文章说到的独占锁的获取是一样的(可以参考【并发编程】三:深入理解AbstractQueuedSynchronizer-共享锁的获取和释放源码分析ReentrantReadWriteLock) 唯一区别在于ReentrantReadWriteLock
对tryAcquire
的实现。我们就主要来看其非公平锁对tryAcquire
的实现。
protected final boolean tryAcquire(int acquires) {
Thread current = Thread.currentThread();
int c = getState();
int w = exclusiveCount(c);
if (c != 0) {
//条件1:共享状态不等于0 而独占状态等于0 说明有共享锁被获取 直接返回获取失败 从这里我们也
//可以看出 ReentrantReadWriteLock是不支持锁升级的。下面会分析
//条件2:current != getExclusiveOwnerThread() 独占锁被获取 如果不是自己直接返回失败
if (w == 0 || current != getExclusiveOwnerThread())
return false;
// 独占锁的获取次数大于 65535 直接抛异常 Maximum lock count exceeded
if (w + exclusiveCount(acquires) > MAX_COUNT)
throw new Error("Maximum lock count exceeded");
//成功直接设置状态 然后返回 这里为什么不设置setExclusiveOwnerThread
//代码能执行这里 current == getExclusiveOwnerThread()一定成立
setState(c + acquires);
return true;
}
//写阻塞 或者设置同步状态失败 返回失败 否则设置独占线程返回成功
if (writerShouldBlock() ||
!compareAndSetState(c, c + acquires))
return false;
setExclusiveOwnerThread(current);
return true;
}
c != 0 && w == 0
如果成立 说明有共享锁 这里如果有共享锁 一律返回false了。没有去管自身是否获取共享锁的情况,既然没考虑那就是不支持锁升级。为什么不支持 因为没必要。writerShouldBlock
非公平锁永远返回false 也就是这个条件永远不成立。公平锁会判断有没有前置等待节点 如果有 则应该阻塞。
接下来的流程就和上一篇文章一毛一样了。这里不做赘述了。
ReentrantReadWriteLock独占锁的释放
和上面一样 我们只对ReentrantReadWriteLock
的tryRelease
做说明,其他AQS的内容和上一篇一样。
protected final boolean tryRelease(int releases) {
//如果当前线程不是独占线程 抛出异常
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
//计算释放同步状态后的值
int nextc = getState() - releases;
//判断独占锁是否没有重入 有重入还需要调用tryRelease继续释放
boolean free = exclusiveCount(nextc) == 0;
if (free)
setExclusiveOwnerThread(null);
setState(nextc);
return free;
}
- 上面代码
getState() - releases
能获取释放后的同步状态值吗?说好的高16位存储的是共享锁的同步状态 低16位存储的是独占锁的同步状态吗 这样直接减不会有问题?如果是共享锁的同步状态这样减确实有问题 ,但是独占锁的同步状态是没有问题的,因为它是低16位。
锁升级
上面也说了 锁的升级没必要 那么我们通过一个demo来展示确实无法升级。
readWriteLock.readLock().lock(); //1
try {
System.out.println("获取了共享锁"); //2
readWriteLock.writeLock().lock(); //3
System.out.println("获取了写锁");//4
}finally {
System.out.println("释放读锁");
readWriteLock.readLock().unlock();
}
System.out.println("锁升级为写锁了");
readWriteLock.writeLock().unlock();
程序走到代码 3处就进入了阻塞队列变成了死锁。
锁降级
readWriteLock.writeLock().lock();
try {
System.out.println("获取了写锁");
readWriteLock.readLock().lock();
System.out.println("获取了读锁");
}finally {
System.out.println("释放写锁");
readWriteLock.readLock().unlock();
}
System.out.println("写锁降级为读锁了");
readWriteLock.writeLock().unlock();
上面演示了锁降级的过程。输出语句全部都会输出。在没有释放写锁的情况下去获取读锁这个过程被称为锁降级。那降级的目的是什么?很多人都说是为了保证可见性。
我们看一下<Java并发编程的艺术> 这本书上的解释:
/**
* 当数据发生变更后,update变量被设置成false 此时所有访问processData()
* 方法的线程都能够感知到变化,但只有一个线程能够获取到写锁,其他线程会被阻塞在
* 读锁和写锁的lock()方法上。当前线程获取写锁完成对数据准备之后,再去获取读锁,随后释放写锁,完成锁降级
*
* 锁降级中读锁的获取是否有必要呢?答案是必要的。主要是为了保证数据的可见性,如果当前线程不获取
* 读锁 而是释放写锁,假设此刻另一个线程T获取了写锁并修改了数据,那么当前线程无法感知到T的数据更新。
* 如果当前线程获取读锁,即遵循锁降级的步骤,则线程T将会被阻塞,直到当前线程使用数据并释放读锁之后,线程T
* 才能获取写锁进行数据更新
*/
volatile boolean update = false;
public void processData(){
readWriteLock.readLock().lock();
if (!update){
readWriteLock.readLock().unlock();
readWriteLock.writeLock().lock();
try{
if (!update){
System.out.println("准备数据");
update = true;
}
readWriteLock.readLock().lock();
}finally {
readWriteLock.writeLock().unlock();
}
System.out.println("使用数据");
readWriteLock.readLock().unlock();
}
}
对于这段解释我是有疑问的:
它把这个问题描述为可见性问题 我认为是不合适的。这里不存在可见性问题 也就是线程A更改完数据 线程B是立马可见的。Lock本身是不存在可见性问题的, 我们前面文章已经分析它是通过volatile
关键字做到的。但是这个demo表述的问题应该是:线程A对数据更改完之后 立马把锁变成读锁来访问 能访问到线程A更改之后新鲜的数据。如果读锁不在写锁中获取 那么可能会出现线程B占有写锁 更改了数据 那么这时线程A再获取读锁访问数据 就不是之前线程A更改过的数据,而是线程B更改过的数据。 所以这取决你的业务是否能接受,并不存在可见性问题。
总结
ReentrantReadWriteLock
的实现要比ReentrantLock
实现复杂的多。需要自己多次实验。