在上一篇文章中根据实例demo介绍了ReentrantReadWriteLock的共享锁、排它锁的锁获取和释放的源码,可以知道锁基本都是依赖AQS完成各项功能,所以在阅读源码前或者阅读锁相关源码比较吃力时,建议先去了解下AQS相关的理论知识与源码,该篇文章接着上一节ReentrantReadWriteLock的源码继续讲解,即ReentrantReadWriteLock小知识点比较多,因为上一节篇幅原因,所以这里重开一篇文章进行介绍
1)同步状态 state属性详解、锁名称讲解
state又称同步状态,是AQS中的一个属性,标识当前锁是否被占有,以及重入的次数(如果是可重入锁),注意这里一定要与同步队列中竞争锁的节点数量区分开。它是表示当前拿到锁的线程数或重入数,而同步队列中的节点是处于一种竞争锁的状态,除了首节点,其他节点是没有拿到锁的。在ReentrantReadWriteLock中state则是读锁(共享锁)、写锁(排它锁)的集合体,它是一个32位的整数,高16位表示当前占有共享锁的线程数,低15位表示当前占有排它锁的线程数,所以能通过该变量查看当前同步状态是否被某线程占有,以及线程占有的是写锁还是读锁。且读锁和写锁不能同时共存,即同步状态不能显示既有竞争读锁的线程持有锁,又有竞争写锁的线程持有锁。
源码中我们出现了读锁、写锁、共享锁、排它锁的名词,其实读锁和共享锁是同一种锁,只不过叫法不同而已,他们允许多个竞争读锁的线程共享锁。而写锁和排它锁类似,在同一时刻仅允许一个线程获取锁。
2)ReentrantReadWriteLock中的锁究竟是什么?为什么有时候说竞争锁、有时候说修改同步状态,竞争锁释放锁本质是什么?持有锁的线程数是什么意思,线程重入数又是什么?
在Lock的实现类中,如ReentrantReadWriteLock,锁功能的实现都是依赖AQS功能。这些实现类对应的锁并不是一个确切的事物,而是通过state这个属性的变化来实现锁的功能,所以主观上来讲,可以把state看做一把锁。不过这是片面的,但是可以先这么理解。
state是AQS中的一个属性,表示当前同步队列的同步状态,一般竞争锁或者释放锁都是通过修改state的值来表现出来,所以竞争锁也就是竞争修改同步状态的值。竞争释放锁的本质就是竞争修改该变量的值。
持有锁的线程则是指有权限修改state属性的线程,在排它锁中,该线程只能有一个,但是有的时候源码表现的意思则是可能有多个持有锁的线程。在排它锁中,多个持有锁的线程实际上是指同一个线程多次获取锁,线程重入数就是指这个行为。而在共享锁中,多个持有锁的线程则有两种情况,一个是确实有多个不同的线程共享同一把锁,另一个情况则是与排它锁类似,指同一个锁的多次重入。
3)公平锁和非公平锁在底层方法上的不同(writeShouldBlock、readerShouldBlock方法)
前面说了公平锁和非公平锁类似,仅在writeShouldBlock、readerShouldBlock方法的实现上不同,前面讲解了公平锁中这两个方法的实现,此处补充下非公平锁的实现源码:
final boolean writerShouldBlock() {
//1、排它锁在非公平状态下竞争不会阻塞,即使同步队列中还有节点等待竞争锁,当前竞争锁线程也不会阻塞
return false;
}
final boolean readerShouldBlock() {
return apparentlyFirstQueuedIsExclusive();
}
final boolean apparentlyFirstQueuedIsExclusive() {
Node h, s;
//1、判断同步队列头结点后唤醒的节点是否为竞争排它锁的节点,是返回true,不是则返回false
return (h = head) != null &&
(s = h.next) != null &&
!s.isShared() &&
s.thread != null;
}
可以看到在非公平锁状态下,这两个方法很简单,竞争写锁永远不会阻塞,直接参与竞争。竞争读锁只在首节点后继节点为竞争排它锁的节点时阻塞。为了加深记忆,下面对比列一下公平非公平锁这两个方法的区别:
writeShouldBlock:
公平锁:如果同步队列中首节点后有后继节点竞争锁,则当前竞争写锁的行为应该阻塞
非公平锁:任何时候都不阻塞
readerShouldBlock
公平锁:与writeShouldBlock相同,即如果同步队列中首节点后有后继节点竞争锁,则当前竞争写锁的行为应该阻塞
非公平锁:判断同步队列头结点后唤醒的节点是否为竞争排它锁的节点,是则阻塞
4)AQS中获取锁、获取中断锁的源码解析
/**
*1、获取锁源码
*/
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true; //不同之一,常规获取方法源码是设置中断位状态并根据返回的中断状态进行置位;获取中断方法源码则是检测到中断时抛异常。
}
} finally {
if (failed)
cancelAcquire(node);
}
}
/**
*2、获取中断源码
*/
public final void acquireInterruptibly(int arg)
throws InterruptedException {
if (Thread.interrupted()) //不同之一,获取中断方法会首先判断线程是否中断,如果设置了中断则抛异常
throw new InterruptedException();
if (!tryAcquire(arg))
doAcquireInterruptibly(arg);
}
private void doAcquireInterruptibly(int arg)
throws InterruptedException {
final Node node = addWaiter(Node.EXCLUSIVE);
boolean failed = true;
try {
for (;;) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return;
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
throw new InterruptedException(); //最大的不同,常规获取方法源码是设置中断位状态并根据返回的中断状态进行置位;获取中断方法源码则是检测到中断时抛异常。
} finally {
if (failed)
cancelAcquire(node);
}
}
注意这里获取中断锁的意思是获取锁的过程中响应中断,正如源码中显示的一样,获取锁和获取中断锁源码的不同有两处:
1)获取中断所进入正式获取锁前,先判断线程是否中断,如果中断则抛异常。
2)同步队列节点轮询竞争锁时,正常获取锁方法会返回当前线程的中断状态,随后再调用对应的设置中断位方法,而获取中断方法在轮询中一旦检测到中断,则直接抛异常。