AQS独占锁的获取

aqs提供了两种锁,独占锁和共享锁。独占锁只有一把锁,同一时间只允许一个线程获得锁;而共享锁则有多把锁,同一时间允许多个线程获得锁。我们本文主要讲独占锁。
一. 独占锁的获取aqs中对独占锁的获取一共有三个方法:
acquire:不响应中断获取独占锁acquireinterruptibly:响应中断获取独占锁tryacquirenanos:响应中断+超时获取独占锁由于篇幅,我们主要着眼于acquire方法,当然,只要你理解了acquire,acquireinterruptibly和tryacquirenanos自然不在话下了,因为这两个方法只是在acquire的基础上增加了一些判断逻辑来处理中断和超时情况而已。
我们上源码
public final void acquire(int arg) { if (!tryacquire(arg) && acquirequeued(addwaiter(node.exclusive), arg)) selfinterrupt();}其acquire方法中一共有四个方法,其逻辑也分为4步:
tryacquire :尝试获取锁,成功即acquire方法结束,否则调用addwaiteraddwaiter :获取锁失败即调用此方法入队,即将获取锁失败的线程包装成node放入同步队列的队尾acquirequeued :入队成功后即调用此方法,如果node在队首则再次抢锁,否则挂起等待唤醒(唤醒后再去获取锁)selfinterrupt :如果是被中断唤醒,则再次执行中断粗略介绍完后,我们现在一个一个方法看。
1.1 tryacquireprotected boolean tryacquire(int arg) { throw new unsupportedoperationexception();}tryacquire是钩子方法,是我们根据需要重写的。其功能就是在独占模式下去获取锁,获取成功则返回true,acquire方法直接结束;如果获取失败返回false,则后续会调用后面要讲的addwaiter方法将线程入队。
因为aqs是模板类,不同的子类只需要重写不同的钩子方法,因此,tryacquire不能设置成抽象方法,不然一些不需要此钩子方法的子类也要实现这个方法。所以作者对tryacquire的默认实现是抛了一个异常(当然我认为直接写个return也是ok的)。
1.2 addwaiter如果tryacquire获取锁失败后,我们就会调用addwaiter将线程包装成node入队挂起。addwaiter的大致逻辑是:先将线程包装成node,然后入队,如果队列未初始化或者入队失败,则会调用子方法enq,enq来进行初始化队列和自旋入队,我们看下具体代码:
private node addwaiter(node mode) { // 将此线程包装成node node node = new node(thread.currentthread(), mode); // 将pred指向尾结点 node pred = tail; // 如果pred 即尾结点不为null,说明同步队列初始化完成了。 if (pred != null) { // 尾插法 // 步骤一:将node的前驱指针指向当前尾结点 node.prev = pred; // 步骤二:通过cas将尾结点指向当前节点 if (compareandsettail(pred, node)) { pred.next = node; return node; } } // 走到这一步有两个原因 // 1是队列未初始化,2是尾结点插入失败 enq(node); return node;}下面是enq方法,当执行到这个方法时,说明线程获取锁已经失败了,然后入队过程又失败了,入队过程失败有两个原因:
同步队列未初始化入队过程中cas操作失败private node enq(final node node) { for (;;) { node t = tail; // 队列为空, 初始化队列操作,即将head和tail指向一个空节点 if (t == null) { if (compareandsethead(new node())) tail = head; } else { // 队列不为空 // 并发下,cas操作可能会失败,所以通过for循环不断进行入队,直到成功为止 node.prev = t; if (compareandsettail(t, node)) { t.next = node; return t; } } }}cas节点入队失败的原因,我们看到enq源码中执行完尾插法的步骤一,即将node的前驱指针指向当前尾结点,如果是并发情况下,应该是如下图所示(紫色节点代表我们关注的node):
此时,可能有多个node都准备入队,所以此时可能有多个node的前驱节点都指向尾结点,所以我们在执行步骤二将尾结点指向node时,采用的是cas,即只有一个node能成功,假设我们关注的node入队成功了,如下图:
则另外两个cas操作肯定会失败,即它们将要进入enq方法重新自旋入队。
1.3 acquirequeued执行完addwaiter方法后,说明我们已经入队成功了,此时我们需要将node中的线程挂起,等待下次被唤醒。
但在挂起之前,我们需要再次检查下我们此时的node是否是在队首,如果在队首,我们又会再次去抢锁。否则我们会通过shouldparkafterfailedacquire判断是否要挂起(shouldparkafterfailedacquire不仅仅是判断此线程是否可以被挂起,还会将同步队列中属性为cancelled的node移除队列),如果需要挂起,则调用parkandcheckinterrupt将线程挂起。具体源码如下:
final boolean acquirequeued(final node node, int arg) { // 获取失败标签,默认ture,如果获取到锁了后则会置为false boolean failed = true; try { // 中断标签,默认false boolean interrupted = false; for (;;) { // 获取此节点的前驱节点 final node p = node.predecessor(); // 如果前驱节点是头结点,则会再次调用tryacquire抢锁 // 如果抢锁成功了,则进入if语句,然后return if (p == head && tryacquire(arg)) { // 将此节点设置为头结点 sethead(node); p.next = null; // help gc // 获取失败标志置为false,因为拿到锁了 failed = false; // 返回中断标志 return interrupted; } // shouldparkafterfailedacquire判断是否要挂起 // 如果要挂起,则调用parkandcheckinterrupt将线程挂起 if (shouldparkafterfailedacquire(p, node) && parkandcheckinterrupt()) interrupted = true; } } finally { if (failed) cancelacquire(node); }}shouldparkafterfailedacquire源码如下。其主要作用有2:
决定获取锁失败后,是否将线程挂起清除同步队列中所有状态为cancelled的节点private static boolean shouldparkafterfailedacquire(node pred, node node) { int ws = pred.waitstatus; // 如果此节点的前驱节点为signal,则说明此节点需要挂起,返回true if (ws == node.signal) return true; // 如果此节点的前驱节点状态大于0,即状态为cancelled则移除前驱节点,然后再往前遍历,直到清除完所有cancelled的节点 if (ws > 0) { do { node.prev = pred = pred.prev; } while (pred.waitstatus > 0); pred.next = node; } else { // 将前驱节点置为signal compareandsetwaitstatus(pred, ws, node.signal); } return false;}这是acquirequeued中的最后一步,即将线程挂起,然后静静的等待被唤醒。除非该线程被其他线程unpark或者被中断,否则该线程的程序将一直停止在这。
private final boolean parkandcheckinterrupt() { // 通过locksupport挂起线程 locksupport.park(this); // 返回线程的标志位,true表示此线程被中断过 return thread.interrupted();}1.4 selfinterrupt通过我们前面的分析可以知道,当线程被中断过,则会进入到此方法。
而interrupte这个方法也只是将当前线程的中断标志置为true,至于会不会被中断,这个是由系统决定的。
static void selfinterrupt() { thread.currentthread().interrupt();}二. 独占锁的的释放相比独占锁的获取,独占锁的释放逻辑就简单多了。独占锁释放只做了两件事情:
释放锁唤醒head结点后最近需要被唤醒的节点。其释放逻辑的实现是通过release方法,而做的两件事分别对应了其子方法tryrelease和unparksuccessor:
public final boolean release(int arg) { // 如果释放锁成功,则进入if去唤醒同步队列中的线程 if (tryrelease(arg)) { node h = head; // head节点不为空(即同步队列不为空) 且 状态不为0(初始化队列时,head结点waitstatus为0,此时等待队列中是没有节点的) // 则唤醒head结点后继节点 if (h != null && h.waitstatus != 0) // 唤醒离head最近需要被唤醒的节点 unparksuccessor(h); return true; } return false;}2.1 tryrelease这个方法和tryacquire一样,也是钩子方法,是留给子类重写的,作用是用来释放锁,如果释放成功则返回true,失败返回false,这个具体的实现我们也放在后续aqs的子类中讲解,这里就不过多阐述了。
2.2 unparksuccessor此方法的作用是唤醒后继node,我们看代码:
private void unparksuccessor(node node) { int ws = node.waitstatus; // waitstatus< 0,说明此时waitstatus为signal if (ws 0) { s = null; for (node t = tail; t != null && t != node; t = t.prev) if (t.waitstatus <= 0) s = t; } if (s != null) // 唤醒node中的线程 locksupport.unpark(s.thread);}这里需要注意的是,我们在找需要被唤醒的节点时,为什么是从后往前遍历呢?
其实这和获取锁时的尾结点入队有关,我们再看下入队方法addwaiter中插入尾结点的相关代码:
node.prev = pred; //step1if (compareandsettail(pred, node)) // step2 pred.next = node; // step3假设我们此时有个node正在入队,执行完step2,还未执行step3,unparksuccessor中如果采用从head往后遍历,是找不到这个新插入的node的;但如果是采用从后往前遍历,则不会出现这个问题。
三. 总结对于独占锁的获取与释放,就分析完了,这里我再总结一下:
获取独占锁是通过acquire来实现的,首先通过tryacquire获取锁,如果获取成功,则直接返回,如果失败,则会调用addwaiter方法进行入队,如果入队过程中发现队列未初始化,则会初始化队列再进行入队,入队不成功则会一直自旋直到成功;入队成功后就会挂起,直到被其他线程或者中断唤醒;唤醒后会检查线程的中断标志位,如果被中断过,会再次调用中断方法,告诉系统自己需要被中断。
释放独占锁是通过release方法实现的,其首先通过tryrelease释放锁,如果失败则直接返回false,如果成功则会调用unparksuccessor唤醒后继节点。

安科瑞Acrel-EIoT能源物联网平台的应用
索尼KP-EF61MG型(背投影彩电)三无
ST和Exagan加速GaN的大规模采用
TDK针对动态应用推出高稳定性的GYPRO®4300数字式MEMS陀螺仪
检波电路中的非线性器件是什么_典型检波应用电路
AQS独占锁的获取
沁恒股份 CH9145蓝牙以太网网关方案概述
英威腾光伏助力徐州中兴纸业实现绿色发展
低能耗LED提供更高效率照明
传感器在电动汽车控制系统中的作用介绍
一加5最新消息:一加5成全球十佳智能手机,2999你觉得值吗?
e络盟社区发布第二期《创客宝典》电子书
PCMCIA的英文全称及介绍
业余条件下制作电路板方法总览,some ways to make PCB
电池厂几种常见的浆料制备方式
Achieving Standardized HS-CAN
iPhone 14系列将在8月开始量产,不会全部搭载A16处理器
iPhone SE出现在苹果的清货页面上后三天即已售空
万物智联大势下,安信可Ai-ThinkerAi-WB1系列模组如何赋能智能家居无线连接?
多次利用视觉AI在医疗领域实现突破,李飞飞入选美国国家医学院