本文以公平锁的角度切入AQS
Synchronized | ReentrantLock | |
---|---|---|
锁实现机制 | 对象头监视器模式 | 依赖 AQS |
灵活性 | 不灵活 | 支持响应中断、超时、尝试获取锁 |
释放锁形式 | 自动释放锁 | 显示调用 unlock() |
支持锁类型 | 非公平锁 | 公平锁 & 非公平锁 |
条件队列 | 单条件队列 | 多个条件队列 |
是否支持可重入 | 支持 | 支持 |
写在前面:
分清楚:锁是对外界开放的API,而AQS同步器是对线程进行管理的容器,任务区分开来,研究的是AQS。
(Lock:lock、unlock;AQS:acquire、acquireQueue……)。
抽象同步队列最终的实现是:公平锁或者非公平锁
加锁过程:
state为锁的状态:0~n state:volatile 、CAS 确保该变量对其他线程也是可见的。 无锁机制改变state的值 1)当state=0时,表示无锁状态 2)当state>0时,表示已经有线程获得了锁,也就是 state=1,但是因为 ReentrantLock 允许重入,所以同一个线程多次获得同步锁的时候,state 会递增,比如重入5次,那么state=5。 而在释放锁的时候,同样需要释放 5 次直到 state=0 其他线程才有资格获得锁
AOS:指定当前的独占锁的线程。
特点:
1、先进先出的双端队列
2、通过 Head、Tail 头尾两个节点来组成队列结构,通过 volatile 修饰保证可见性
3、Head 指向节点为已获得锁的节点,head指向的时得到锁的Node节点,是一个虚拟节点,节点本身不持有具体线程;更可以看作是 【有锁节点 + 阻塞队列】
4、获取不到同步状态,会将节点进行自旋获取锁,自旋一定次数失败后会将线程阻塞,相对于 CLH 队列性能较好【抢锁方式:自适应自旋锁】
关键方法与属性 | 对应含义 |
---|---|
waitStatus | 当前节点在队列中处于什么状态 |
thread | 表示节点对应的线程 |
prev | 前驱指针,指向本节点的上一个节点 |
next | 后继指针,指向本节点的下一个节点 |
predecessor | 返回前驱节点,没有的话抛出 NPE 异常 |
属性值 | 值含义 |
---|---|
0 | Node 被初始化后的默认值 |
CANCELLED | 值为1,由于中断或超时,节点被取消 |
SIGNAL | 值为-1,表示节点的后继节点即将被阻塞;当前节点需要唤醒他的后继节点 |
CONDITION | 值为-2,表示节点在等待队列中,节点线程等待唤醒 |
尝试拿锁,只有两种情况能够成功:
1)无锁
2)自己人(涉及到锁的重入)
protected final boolean tryAcquire(int acquires) { // 判断锁状态 和 线程能否重入 final Thread current = Thread.currentThread(); int c = getState(); if (c == 0) { //因为fairSync是公平锁,任何时候都需要检查一下 队列中是否在当前线程之前有等待者.. if (!hasQueuedPredecessors() && compareAndSetState(0, acquires)) { setExclusiveOwnerThread(current); return true; } } else if (current == getExclusiveOwnerThread()) { int nextc = c + acquires; if (nextc < 0) throw new Error("Maximum lock count exceeded"); setState(nextc); return true; } return false; }
addWaiter:① 将当前线程封装为Node节点,② 加入上面锁的双向链表(等待队列)中(enq)
获取锁(tryAcquire)->构造节点(addWaiter)->加入队列(addWaiter)->自旋获取锁(acquireQueued)
final boolean acquireQueued(final Node node, int arg) { boolean failed = true; //当前线程是否被中断 = 标记等待过程中是否中断过 boolean interrupted = false; try { //自旋..前置节点..Node 要么获取锁,要么中断 for (;;) { final Node p = node.predecessor(); if (p == head && tryAcquire(arg)) { setHead(node); p.next = null; // help GC failed = false; //当前线程 获取锁 过程中..没有异常 return interrupted; // 这里才是出口,唯一的出口 } // 挂起当前线程,并且唤醒之后 返回 当前线程的 中断标记 // 唤醒:1.正常唤醒 其它线程 unpark 2.其它线程给当前挂起的线程 一个中断信号.. if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) //interrupted == true 表示当前node对应的线程是被 【中断信号唤醒的...】 interrupted = true; } } finally { //true 表示当前线程抢占锁成功,普通情况下【lock】 当前线程早晚会拿到锁.. //false 表示失败,需要执行出队的逻辑... (响应中断的lock方法时。) if (failed) cancelAcquire(node); } }
1、pred(waitState)为0,当前的结点不需要阻塞。;
2、pred(waitState)为1,跳过;
3、pred(waitState)为-1,park当前节点。
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) { int ws = pred.waitStatus; // 1、前方节点是处于阻塞,挂起当前线程(申锁节点进行阻塞) if (ws == Node.SIGNAL) return true; // 2、队列包含了被解约的节点 if (ws > 0) { do { // 跳过所有的取消的Node == 将所有的CANCELLED节点出队 node.prev = pred = pred.prev; } while (pred.waitStatus > 0); pred.next = node; // 3、初始化的Node,默认状态为0。 } else { // 将前置节点强制设置为 SIGNAl,表示前置节点释放锁之后需要 喊醒我.. compareAndSetWaitStatus(pred, ws, Node.SIGNAL); } return false; }
public void unlock() { sync.release(1); }
这里主要就是对资源的释放,更新同步状态,然后唤醒下一个等待线程。
//AQS#release方法 //ReentrantLock.unlock() -> sync.release()【AQS提供的release】 public final boolean release(int arg) { //尝试释放锁,tryRelease 返回true 表示当前线程已经完全释放锁 //返回false,说明当前线程尚未完全释放锁.. if (tryRelease(arg)) { //head什么情况下会被创建出来? //当持锁线程未释放线程时,且持锁期间 有其它线程想要获取锁时,其它线程发现获取不了锁,而且队列是空队列,此时后续线程会为当前持锁中的 //线程 构建出来一个head节点,然后后续线程 会追加到 head 节点后面。 Node h = head; //条件一:成立,说明队列中的head节点已经初始化过了,ReentrantLock 在使用期间 发生过 多线程竞争了... //条件二:条件成立,说明当前head后面一定插入过node节点。 if (h != null && h.waitStatus != 0) //唤醒后继节点.. unparkSuccessor(h); return true; } return false; }
//Sync#tryRelease() protected final boolean tryRelease(int releases) { //减去释放的值.. int c = getState() - releases; //条件成立:说明当前线程并未持锁..直接异常.,. if (Thread.currentThread() != getExclusiveOwnerThread()) throw new IllegalMonitorStateException(); //当前线程持有锁.. //是否已经完全释放锁..默认false boolean free = false; //条件成立:说明当前线程已经达到完全释放锁的条件。 c == 0 if (c == 0) { free = true; setExclusiveOwnerThread(null); } //更新AQS.state值 setState(c); return free; }
1)如果其下一个节点为空,或者其等待状态是取消状态,那么就从后往前找,找到一个等待状态 <=0 的,然后将其唤醒;
2)如果下一个节点不为空,且等待状态 <=0,将其唤醒。
private void unparkSuccessor(Node node) { int ws = node.waitStatus; if (ws < 0) compareAndSetWaitStatus(node, ws, 0); Node s = node.next; //条件一: //s 什么时候等于null? //1.当前节点就是tail节点时 s == null。 //2.当新节点入队未完成时(1.设置新节点的prev 指向pred 2.cas设置新节点为tail 3.(未完成)pred.next -> 新节点 ) //需要找到可以被唤醒的节点.. //条件二:s.waitStatus > 0 前提:s != null //成立:说明 当前node节点的后继节点是 取消状态... 需要找一个合适的可以被唤醒的节点.. if (s == null || s.waitStatus > 0) { s = null; for (Node t = tail; t != null && t != node; t = t.prev) if (t.waitStatus <= 0) s = t; } //如果找到合适的可以被唤醒的node,则唤醒.. 找不到 啥也不做。 if (s != null) LockSupport.unpark(s.thread); }
/** * 当前取消排队的node所在 队列的位置不同,执行的出队策略是不一样的,一共分为三种情况: * 1.当前node是队尾 tail -> node * 2.当前node 不是 head.next 节点,也不是 tail * 3.当前node 是 head.next节点。 */ private void cancelAcquire(Node node) { if (node == null) return; node.thread = null; Node pred = node.prev; //滤除所有的无效节点pred.waitStatus > 0 即:1 cancelled while (pred.waitStatus > 0) node.prev = pred = pred.prev; Node predNext = pred.next; node.waitStatus = Node.CANCELLED; //条件一:node == tail 成立:当前node是队尾 tail -> node //条件二:compareAndSetTail(node, pred) 成功的话,说明修改tail完成。 if (node == tail && compareAndSetTail(node, pred)) { //修改pred.next -> null. 完成node出队。 compareAndSetNext(pred, predNext, null); } else { int ws; //条件一:pred != head 成立, 说明当前node 不是 head.next 节点,也不是 tail【中间的节点】 if (pred != head && //条件2.1:成立:说明node的前驱状态是 Signal 状态 不成立:前驱状态可能是0, ((ws = pred.waitStatus) == Node.SIGNAL || // 假设前驱状态是 <= 0 则设置前驱状态为 Signal状态..表示要唤醒后继节点。 (ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) &&pred.thread != null) { Node next = node.next; //让pred.next -> node.next ,所以需要保证pred节点状态为 Signal状态。 if (next != null && next.waitStatus <= 0) compareAndSetNext(pred, predNext, next); } else { //head.next节点,直接unpark后继(相当于忽略现在的节点) unparkSuccessor(node); } node.next = node; // help GC,修改了 node 的next 指针,但是保证了 prev 指针的不变 } }
为什么所有的变化都是对 Next 指针进行了操作,而没有对 Prev 指针进行操作呢?
执行 cancelAcquire 的时候,当前节点的前置节点可能已经从队列中出去了(已经执行过 Try 代码块中的 shouldParkAfterFailedAcquire 方法了),如果此时修改 Prev指针,有可能会导致 Prev 指向另一个已经移除队列的 Node,因此这块变化 Prev 指针不安全。
什么情况下会对 Prev 指针进行操作?
shouldParkAfterFailedAcquire 方法中,会执行下面的代码,其实就是在处理 Prev 指针。shouldParkAfterFailedAcquire 是获取锁失败的情况下才会执行,进入该方法后,说明共享资源已被获取,当前节点之前的节点都不会出现变化,因此这个时候变更 Prev 指针比较安全。
do { node.prev = pred = pred.prev; } while (pred.waitStatus > 0);
参考资料:
《Java并发编程实战》
《小刘源码课程》
https://tech.meituan.com/2019/12/05/aqs-theory-and-apply.html