2.4 创建条件队列
AQS的条件队列其实源自AQS的一个内部类ConditionObject,这其实就是一种标准的内部类的使用场景,当A仅存在于B里面时,就可以把类A定义在类B内部。
另外,这个内部类实现了Condition接口,我们先稍微看下其定义的方法,都是符合模型的方法,具体的使用我们将在后面的小结中述说
显而易见地,如果我们需要使用条件队列时,仅需要new一下这个ConditionObject就可以,AQS没有直接给我们生成条件队列的方法,这一块需要实现类自己去完成,如ReentrantLock里的newCondition()方法。
2.5 进入条件队列
其实在2.4里我们已经看到了Condition接口的几个await族方法,那么AQS 的 ConditionObject 里只要实现这几个方法就可以了。我们以await()为例
public final void await() throws InterruptedException { if (Thread.interrupted()) throw new InterruptedException(); Node node = addConditionWaiter(); int savedState = fullyRelease(node); int interruptMode = 0; while (!isOnSyncQueue(node)) { LockSupport.park(this); if ((interruptMode = checkInterruptWhileWaiting(node)) != 0) break; } if (acquireQueued(node, savedState) && interruptMode != THROW_IE) interruptMode = REINTERRUPT; if (node.nextWaiter != null) // clean up if cancelled unlinkCancelledWaiters(); if (interruptMode != 0) reportInterruptAfterWait(interruptMode); }
这里面有几个点需要注意的:
- 中断处理:await方法都是会抛出中断异常的,当线程是中断状态后进入条件队列,或者在条件队列中被中断时,都i会抛出异常
- 释放锁:调用await方法会先进入条件队列,然后释放锁
- 队列竞争:细心的人看到2.4以及2.5,应该发现了,条件队列的头尾节点没有使用volatile,增删节点也没有使用CAS原子操作。这是因为能使用await() / signal() 方法增删节点的,必定是排他锁的持有者,所以条件队列的处理不需要考虑并发
2.6 唤醒条件队列
相比于上文加入条件队列,唤醒条件队列会稍微复杂一点,唤醒有两种方法
- signal()
- 剥离条件队列的第一个节点,将其放入同步队列中
- signalAll()
- 剥离条件队列的所有节点,将其放入同步队列中
从图中不难看出这一逻辑,把条件队列头部的Node节点 转移 到同步队列尾部。当然,我们照例会给出两个注意事项,算作本小结的重点
- 持有锁
- 只有持有排他锁的线程才能调用该方法,否则会报IllegalMonitorStateException异常
- 队列竞争
- 条件队列只有锁的持有线程才能操作,所以条件队列剥离节点不用考虑并发。但是同步队列是外部线程亦可操作的,所以在2.3里我们也提到同步队列的头尾节点是volatile的,将节点放进同步队列时,要采用CAS
- 是否唤醒
- 将节点放在同步队列之后,是否会唤醒这个节点呢?这是不一定的,在放入同步队列后,本节点的前一个节点如果是”已取消“状态,那就唤醒。如果不是”已取消“,那就使用CAS将其节点变为”信号“,如果CAS失败,就直接唤醒本节点。关于节点的状态变化,我会在第3小节中详谈
2.7 释放锁
同获取锁一样,真正的释放锁的方法 tryRelease 在AQS里并没有实现,需要继承的类自己去实现
AQS里的操作就是在释放锁之后,帮你唤醒同步队列的第一个节点
3. 节点Node的属性
如果你看了上面第二个小节,应该可以意识到AQS的重点在于两种队列的处理,即同步队列和条件队列,而两种队列都采用的双向链表的结构,并且用的也是同一种Node。那么Node 有几种属性,它们代表了什么意思,又是怎么互相转换的呢?
我们先看 Node状态 waitStatus 官方的定义及注释:
- 已取消——CANCELLED = 1
- 由于超时或中断,此节点被取消。这将是节点的终态,不再改变。需要注意的是,处于“已取消”节点的线程再也不会阻塞。
- 信号——SIGNAL = -1
- 此节点的后续节点已经被(或将被)阻塞(通过park),因此当前节点在释放或取消时必须唤醒其后续节点。为了避免竞争,获取锁的方法必须首先指示它们需要一个信号,然后重试原子获取,然后在失败时被阻塞。
- 条件——CONDITION = -2
- 此节点当前在条件队列中,如果节点被转换到同步队列,将不会再使用该值,此时状态将设置为0。
- 传播——PROPAGATE = -3
- 执行释放共享锁时,需要把这个消息传播到其他节点。所以这种状态仅适用于共享锁的头节点,以确保传播继续进行,即使其他操作已经介入。
- 其他状态 = 0
- 除了上述状态,其他状态都为0。
那么上述这些状态该怎么转换呢?其实从状态的名字就可以看出来了。
1. 一个Node被放到同步队列时,状态是0;被放到条件队列时,状态是2——”条件“;
2. 当使用 signal 从条件队列剥离,放到同步队列时,该节点状态会从2 变为 0
3. 当一个节点被中断,或者超过等待时间,它的状态会变为 1——”已取消“
当然具体的情况会更复杂些,比如获取锁失败,放入同步队列时,会顺便清除掉同步队列的”已取消“节点;条件队列被线程中断后,还是会被先放进同步队列;
三、ReentrantLock
学习了上一节的AQS,我们来看看它的实用类ReentrantLock
1. ReentrantLock是什么
我们在第一个例子中,用到的Lock实例 就是 ReentrantLock。
ReentrantLock 是JUC包自带的一个Lock接口的实现类,能在大部分场景胜任“锁”的功能,从名字看,这是一个可重入锁,是否允许重入是锁的一个特性,从这个特性,我们可以把锁分为两类:
- 可重入锁
- 当一个线程获取到锁之后,当他再次进入由该锁控制的同步区域时,还是能直接进入,称为可重入。相当于你有一张工卡,只要你不扔掉工卡,你就能一直进入有权限的区域,比如刷卡进入公司大楼后,还可以继续刷卡进入办公室
- 不可重入锁
- 当一个线程获取到锁之后,当他再次进入由该锁控制的同步区域时,会被拒绝,称为不可重入。相当于你有一张一次性工卡,刷卡进入公司大楼后,当继续刷卡进入办公室时会被拒绝
try { // 第一次获得锁 lock.lock(); // do something try { // 在获得锁的情况下再次申请获得锁,可重入锁能获得,不可重入锁将无法获得 lock.lock(); } catch (Exception e) {} // do otherthing lock.unlock(); } catch (Exception e) { } finally { lock.unlock(); }
我们常见的几乎所有锁都是可重入锁,比如这里的ReentrantLock,比如synchronized锁。但细心的你应该能发现,Lock接口本身并没有规定锁的种类,因此实现类们可以根绝自己的需要,在进行lock()操作时,做到即使线程已经是本锁的持有者,但仍拒绝其锁申请。
一个典型不可重入的例子是线程池在停止闲置线程时,只能中断其他线程,不允许中断自己。但不可重入锁的建立和使用必须十分谨慎,因为非常容易导致死锁
2. ReentrantLock与AQS
我们说了lock的核心类是AQS,那么ReentrantLock 与 AQS有什么关系呢?首先,它并不是AQS的实现类,但是它里面有一个属性sync,这个sync是AQS的实现类,而ReentrantLock 又为 sync 编了两种继承类,看名字就知道,一个是公平同步器,一个是非公平同步器。所以ReentrantLock 同时支持两种模式
ReentrantLock 需要实现的代码就是为这两种同步器,分别写上申请和释放锁的具体代码(我们在第二章说过,AQS并没有写这两块的具体代码)
3. 两种构造方法
上文我们提到,ReentrantLock 同时支持两种模式,这是因为ReentrantLock的构造方法有两个,能够决定创建的锁是“公平锁”或”非公平锁“,默认情况下ReentrantLock使用的是非公平锁,亦可以通过传入参true 来使用公平锁,这里我们又讲到了锁的一个性质——是否公平,我们简单解释一下
- 公平锁
- 多个线程申请锁时,按申请顺序来获取锁,即先进先出队列(FIFO)
- 非公平锁
- 多个线程间申请锁时,谁能先获得锁是不确定的,各线程都会参与竞争,可能会出现线程饥饿
4. ReentrantLock 锁的模型
其实看过我的【全网最细系列】synchronized锁详解,偏向锁与锁膨胀全流程的人应该还记得,我说到过synchronized的重量级锁的模型 和 ReentrantLock 是高度相似的,我们可以借鉴一张模型图
进入同步区域前要竞争锁,竞争失败的线程在入口等待队列休眠。竞争到锁的线程则可以进入同步区域(上图大方框区域)执行代码,此时它可以使用 await 选择放弃本锁并休眠,且可以选择休眠到哪个队列里(条件队列)
同样的,他也可以使用 signal (或signalAll) 唤醒某个条件队列的单个(或所有)线程。需要注意的是,signal操作并没有放弃锁,所以即使唤醒别的线程,其他线程其实也是竞争不到锁的。因此大部分代码上 signal 后都会立即释放锁
总结
以上就是对Lock的初步解析,后续还会继续完善。目前缺漏的点,主要在
1. Node 节点的变化,以及是否存在并发问题的分析,在这块的内容中,有多个方法都能更改两个队列的节点,但是却没有使用CAS,而是采用了精巧涉及避免并发问题
2.只简单介绍了AQS的一种内置应用——ReentrantLock,实际上AQS还有共享锁,读写锁等多种内置的应用,只有全部研读一遍,才能对AQS的功能理解更深