前面讲了什么是AQS.并且基于Reentrantlock已经看了一部分AQS方法. 建议看完下面这篇再来阅读该文.
AQS是什么?基于ReentrantLock解密!
但是其实真实项目中一般不使用ReetrantLock,一般都是synchronized. 因为在jdk1.6之后,synchronized底层实现里面,里面做了一些计数器的维护,加锁释放锁,CAS啊.都能实现ReentrantLock一样的效果.而且使用起来相对简单. 另外读写锁ReentrantReadWriteLock其实也挺常用的
下面就接着看对于ReentrantReadWriteLock底层是如何基于aqs和Lock API进行读写锁的实现的.
前言
- 如果想要几分钟了解AQS是不现实的,你要这样想,人家大神花了几个月甚至几年写的代码怎么会那么容易几分钟让你就看懂呢?
- 本文会根据一步一图的形式,将代码中的各种指针变化全部画出来.便于理解.因为不画图是根本无法了解流程是怎么走的.
- 再讲一遍,该文每个场景都要跟着一步一图走,单看文章可能无法理解,需要结合源码.可以边看边打开idea跟着方法一块儿走.顺便看着我画的图在process on等画图工具里面跟着一起画.理解流程.这样才能更快的理解里面及其复杂的各种指针变化.
- 本文阅读完要20几分钟,但是如果你跟着一起画图,一起看源码,直到看明白,其实是远远不止的.所以做好心理准备,开始吧.
实际场景解读AQS读写锁ReentrantReadWriteLock
LockAPI的读写锁简单介绍
首先明确一点,读锁是共享锁,而写锁是独占锁.
-
Lock API,读写锁,可以加读锁,也可以加写锁
-
但是,读锁和写锁是互斥的,也就是说,你加了读锁之后,就不能加写锁;如果加了写锁,就不能加读锁
-
如果有人在读数据,就不能有人写数据,读锁 -> 写锁 -> 互斥
-
如果有人在写数据,别人不能写数据,写锁 -> 写锁 -> 互斥;
-
如果有人在写数据,别人也不能读数据,写锁 -> 读锁 > 互斥
-
但是如果有人加了读锁之后,别人可以同时加读锁
-
如果你有一份数据,有人读,有人写,如果你全部都是用synchronized的话,会导致如果多个人读,也是要串行化,一个接一个的读
-
我们希望的效果是多个人可以同时来读,如果使用读锁和写锁分开的方式,就可以让多个人来读数据,多个人可以同时加读锁
-
读写锁的好处
- 读写锁维护了一对锁,一个读锁和一个写锁,通过分离读写锁,使得并发性比一般的排它锁有了很大提升。
- synchronzied特性以及功能,读写锁都拥有
- 但是因为大多数应用场景都是读多于写的,因此在这样的情况下,读写锁可以提高吞吐量。这是相对于synchronized好的地方.
线程1写锁如何基于AQS的state完成加锁的?
- 我们走进writeLock的lock方法中.可以看到下面方法.首先调用tryAcquire方法.获取到了当前线程1.会判断是否有其他线程抢到过锁
acquire方法如下
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
protected final boolean tryAcquire(int acquires) {
Thread current = Thread.currentThread();
// 获取到一个state = 0
int c = getState();
// 二进制值里面的高低16位分别代表了读锁和写锁,AQS就一个,state
// state二进制值的高16位代表了读锁,低16位代表了写锁
// 可以认为下面的w就是从c(二进制值)通过位运算获取到了state的低16位,代表了写锁的状态
int w = exclusiveCount(c);
// 如果c != 0,说明有人加过锁,但是此时c = 0
if (c != 0) {
// (Note: if c != 0 and w == 0 then shared count != 0)
if (w == 0 || current != getExclusiveOwnerThread())
return false;
if (w + exclusiveCount(acquires) > MAX_COUNT)
throw new Error("Maximum lock count exceeded");
// Reentrant acquire
setState(c + acquires);
return true;
}
// 非公平锁,此时一定会去尝试加锁
// 如果是公平锁,此时会判断如果队列中有等待线程,就不加锁
if (writerShouldBlock() ||
!compareAndSetState(c, c + acquires))
return false;
setExclusiveOwnerThread(current);
return true;
}
- 上面的c也就是state第一次进去是为0的.证明没有其他线程抢到锁,所以会走第二个if语句.进行writerShouldBlock判断是否公平,
- 如果是非公平策略.writerShouldBlock会返回false,此时一定会去执行compareAndSetState(c, c + acquires) 尝试加锁,state+1
- 如果是公平策略.writerShouldBlock则会判断是否阻塞队列里面有元素在排队,没有返回false,执行cas,state+1,有就返回true 直接跳过if
- 然后会设置当前加锁线程为线程1.
总结
当第一个线程过来尝试加锁的时候,会cas成功后对state加1,将当前加锁线程存进变量ExclusiveOwnerThread中
线程1state二进制高低16位判断写锁可重入
其实说白了就是将state的值在加1,比如下面线程1的state本来加过一次锁.再加一次就会变为2.
代码分析
下面看代码,其实还是tryAcquire方法
protected final boolean tryAcquire(int acquires) {
Thread current = Thread.currentThread();
int c = getState(); //获取到state此时为1,
int w = exclusiveCount(c);//w变量代表write写锁的第十六位二进制数,exclusiveCount获取的低十六位的值此时也是不为0的.
if (c != 0) {
//虽然c!=0,w!=0,但是因为当前加锁的线程还是线程1.所以if中的第二个条件不满足
if (w == 0 || current != getExclusiveOwnerThread())
return false;
//这里会判断加写锁数量是否大于最大数量65535.如果超过就抛异常. 不满足条件.
if (w + exclusiveCount(acquires) > MAX_COUNT)
throw new Error("Maximum lock count exceeded");
//这里会重新设置state的值为2.然后返回true
setState(c + acquires);
return true;
}
if (writerShouldBlock() ||
!compareAndSetState(c, c + acquires))
return false;
setExclusiveOwnerThread(current);
return true;
}
总结
- 如果是int类型的state不是0的话,那么他的二进制数值,32位,低16位一定不是0,如果低16位不是0的话,就代表他是加过写锁的
c != 0,w == 0,c肯定不是0,但是低16位是0,说明高16位不为0, 此时有线程加了读锁,没有线程加写锁,此时线程1要加写锁,而且线程1还不是之前加锁的那个线程
c != 0,w != 0,有线程加过锁,之前加的是写锁,但是当前线程不是之前加锁的线程,此时也不让线程1加写锁,同一个时间,只能有一个线程加写锁,如果线程1比如加了写锁,线程2也要加写锁,是互斥的
c != 0,w != 0,之前有人加过写锁,而且加写锁的还是线程1
如果加写锁的人是线程1,说明线程1就是在可重入的加写锁,将state += 1
线程2写锁加锁失败如何基于CAS队列阻塞等待?
当tryAcquire返回false的时候证明此时加锁的不是线程1,而是线程2了.此时就要进行阻塞线程2了.
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
addWaiter方法如下
- 这里流程其实就跟ReentrantLock中分析的时候一样.就是各种指针变化.
*/
private Node addWaiter(Node mode) {
Node node = new Node(Thread.currentThread(), mode);
Node pred = tail;
if (pred != null) {
node.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
enq(node);
return node;
}
- 我们一步步分析,首先基于当前抢锁失败线程创建一个node.然后pred指针指向tail.此时tail为null,所以pred也为null
enq方法如下
- 然后进入enq方法
private Node enq(final Node node) {
for (;;) {
Node t = tail;
if (t == null) {
// Must initialize
if (compareAndSetHead(new Node()))
tail = head;
} else {
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
- 首先t也指向tail.然后tail为null,那么t也为null,然后就进行初始化虚拟Node设置为一个Head节点,并且将tail指针指向nodeHead节点.
- 然后二次循环,此时t重新指向tail,然后判断不为null,就进入else,将线程2node的prev指向NodeHead,然后tail节点设置成线程2node,nodeHead的next指针指向线程2node,形成回路,最后返回nodeHead如下图.
acquireQueued方法
- enq执行完之后addWaiter方法返回线程2node
acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) 也就是acquireQueued(线程2node)
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 {
//这里tryAcquire不报错就不会执行.暂时不管,想了解cancelAcquire的话建议看ReentrantLock,那个文档有写
if (failed)
cancelAcquire(node);
}
}
- 首先获取node前驱节点,这里线程2node的前驱节点就是nodeHead,所以第一个if中虽然p==head,但是我们这里假设再次执行tryAcquire还是返回false,那么就会继续走shouldParkAfterFailedAcquire(nodeHead,线程2Node)方法了.
shouldParkAfterFailedAcquire方法
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;
if (ws == Node.SIGNAL)
return true;
if (ws > 0