JUC基础(三)—— Lock锁 及 AQS(2)

简介: JUC基础(三)—— Lock锁 及 AQS

2.4 创建条件队列

AQS的条件队列其实源自AQS的一个内部类ConditionObject,这其实就是一种标准的内部类的使用场景,当A仅存在于B里面时,就可以把类A定义在类B内部。

dbd9861bc2224d5fb196158e5cb9e1e0.png



另外,这个内部类实现了Condition接口,我们先稍微看下其定义的方法,都是符合模型的方法,具体的使用我们将在后面的小结中述说

7c936bf354fa4fb18dadeaa0af9a228f.png


显而易见地,如果我们需要使用条件队列时,仅需要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()
  • 剥离条件队列的所有节点,将其放入同步队列中

1ce63990fe1f418ea41c76c4531f3caa.png

从图中不难看出这一逻辑,把条件队列头部的Node节点 转移 到同步队列尾部。当然,我们照例会给出两个注意事项,算作本小结的重点


  • 持有锁
  • 只有持有排他锁的线程才能调用该方法,否则会报IllegalMonitorStateException异常

  • 队列竞争
  • 条件队列只有锁的持有线程才能操作,所以条件队列剥离节点不用考虑并发。但是同步队列是外部线程亦可操作的,所以在2.3里我们也提到同步队列的头尾节点是volatile的,将节点放进同步队列时,要采用CAS

  • 是否唤醒
  • 将节点放在同步队列之后,是否会唤醒这个节点呢?这是不一定的,在放入同步队列后,本节点的前一个节点如果是”已取消“状态,那就唤醒。如果不是”已取消“,那就使用CAS将其节点变为”信号“,如果CAS失败,就直接唤醒本节点。关于节点的状态变化,我会在第3小节中详谈


2.7 释放锁

同获取锁一样,真正的释放锁的方法 tryRelease 在AQS里并没有实现,需要继承的类自己去实现2dd8e7bd5ade4466aa02c2cfc6c9e2e4.png



AQS里的操作就是在释放锁之后,帮你唤醒同步队列的第一个节点


ea840a537ccb4e5fafe71a8cca18758a.png


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。

5624b9e198be47fb9ad992c61a22f5c6.png


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 同时支持两种模式

fc305c139abb488ca23c081fc49839e3.png


ReentrantLock 需要实现的代码就是为这两种同步器,分别写上申请和释放锁的具体代码(我们在第二章说过,AQS并没有写这两块的具体代码)


3. 两种构造方法

上文我们提到,ReentrantLock 同时支持两种模式,这是因为ReentrantLock的构造方法有两个,能够决定创建的锁是“公平锁”或”非公平锁“,默认情况下ReentrantLock使用的是非公平锁,亦可以通过传入参true 来使用公平锁,这里我们又讲到了锁的一个性质——是否公平,我们简单解释一下


18de15e6f8624da891669bc8fa82e844.png


  • 公平锁
  • 多个线程申请锁时,按申请顺序来获取锁,即先进先出队列(FIFO)
  • 非公平锁
  • 多个线程间申请锁时,谁能先获得锁是不确定的,各线程都会参与竞争,可能会出现线程饥饿

4. ReentrantLock 锁的模型

其实看过我的【全网最细系列】synchronized锁详解,偏向锁与锁膨胀全流程的人应该还记得,我说到过synchronized的重量级锁的模型 和 ReentrantLock 是高度相似的,我们可以借鉴一张模型图

165b0aada6da469e99e1bdca9e4a3668.png


进入同步区域前要竞争锁,竞争失败的线程在入口等待队列休眠。竞争到锁的线程则可以进入同步区域(上图大方框区域)执行代码,此时它可以使用 await 选择放弃本锁并休眠,且可以选择休眠到哪个队列里(条件队列)

同样的,他也可以使用 signal (或signalAll) 唤醒某个条件队列的单个(或所有)线程。需要注意的是,signal操作并没有放弃锁,所以即使唤醒别的线程,其他线程其实也是竞争不到锁的。因此大部分代码上 signal 后都会立即释放锁

总结

以上就是对Lock的初步解析,后续还会继续完善。目前缺漏的点,主要在

1. Node 节点的变化,以及是否存在并发问题的分析,在这块的内容中,有多个方法都能更改两个队列的节点,但是却没有使用CAS,而是采用了精巧涉及避免并发问题

2.只简单介绍了AQS的一种内置应用——ReentrantLock,实际上AQS还有共享锁,读写锁等多种内置的应用,只有全部研读一遍,才能对AQS的功能理解更深

目录
相关文章
|
1月前
|
安全 Java
JUC锁: ReentrantReadWriteLock详解
`ReentrantReadWriteLock` 主要用于实现高性能的并发读取,而在写操作相对较少的场景中表现尤为突出。它保证了数据的一致性和线程安全,在合适的场合合理使用 `ReentrantReadWriteLock`,可以实现更加细粒度的控制,并显著提升应用性能。然而,需要注意它的复杂度较一般的互斥锁高,因此在选择使用时要仔细考虑其适用场景。
31 1
|
3月前
|
安全 Java
Java多线程中的锁机制:深入解析synchronized与ReentrantLock
Java多线程中的锁机制:深入解析synchronized与ReentrantLock
83 0
|
4月前
|
监控 安全 Java
Java中的锁(Lock、重入锁、读写锁、队列同步器、Condition)
Java中的锁(Lock、重入锁、读写锁、队列同步器、Condition)
26 0
|
算法 Java
JUC--锁
简单介绍锁
|
安全 Java
synchronized 锁与 ReentrantLock 锁的区别
synchronized 锁与 ReentrantLock 锁的区别
110 0
|
算法 调度
JUC基础(三)—— Lock锁 及 AQS(1)
JUC基础(三)—— Lock锁 及 AQS
120 0
|
安全 Java
并发编程-19AQS同步组件之重入锁ReentrantLock、 读写锁ReentrantReadWriteLock、Condition
并发编程-19AQS同步组件之重入锁ReentrantLock、 读写锁ReentrantReadWriteLock、Condition
93 0
并发编程-19AQS同步组件之重入锁ReentrantLock、 读写锁ReentrantReadWriteLock、Condition
【JUC基础】04. Lock锁
java.util.concurrent.locks为锁定和等待条件提供一个框架的接口和类,说白了就是锁所在的包。
5515 0
AQS(abstractQueuedSynchronizer)锁实现原理详解
AQS(abstractQueuedSynchronizer)抽象队列同步器。其本身是一个抽象类,提供lock锁的实现。聚合大量的锁机制实现的共用方法。
149 0