AbstractQueuedSynchronizer源码解析(上)
2022/1/2 14:09:25
本文主要是介绍AbstractQueuedSynchronizer源码解析(上),对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!
目录
- 一、摘要
- 二、AQS
- 2.1 类定义
- 2.2 基本属性
- 2.2.1 简单属性
- 2.1.2 共享锁和排它锁的区别
- 2.1.3 同步队列属性
- 2.1.4 条件队列的属性
- 2.1.5 Node
- 2.3 Condition
- 三、同步器的状态
一、摘要
队列同步器AbstractQueuedSynchronizer,是用来构建锁或者其他同步组件的基础框架,它使用了一个 int 成员变量表示同步状态,通过内置的 FIFO 队列来完成资源获取线程的排队工作。同步器的设计是基于模板方法模式的,也就是说,使用者需要继承同步器并重写指定的方法,随后将同步器组合在自定义同步组件的实现中,并调用同步器提供的模板方法,而这些模板方法将会调用使用者重写的方法。
简称 AQS,是各种各样锁的基础,比如说 ReentrantLock、CountDownLatch 等等,这些我们经常用的锁底层实现都依赖于 AQS,由此可见这个类的重要性,所以学好 AQS 对于后面理解锁的实现是非常重要的。
二、AQS
2.1 类定义
AQS 类定义代码如下:
public abstract class AbstractQueuedSynchronizer extends AbstractOwnableSynchronizer implements java.io.Serializable {
可以看出两点:
- AQS 是个抽象类,就是给各种锁子类继承用的,AQS 定义了很多如何获得锁,如何释放锁的抽象方法,目的就是为了让子类去实现;
- 继承了 AbstractOwnableSynchronizer,AbstractOwnableSynchronizer 的作用就是为了知道当前是那个线程获得了锁,方便监控用的,代码如下:
2.2 基本属性
AQS 的属性可简单分为四类:同步器简单属性、同步队列属性、条件队列属性、公用 Node。
2.2.1 简单属性
让我们来看一下简单属性都有哪些:
最重要的就是 state 属性,是 int 类型的,所有继承 AQS 的锁都是通过这个字段来判断能不能获得锁,能不能释放锁。
// 同步器的状态,子类会根据状态字段进行判断是否可以获得锁 // 比如 CAS 成功给 state 赋值 1 算得到锁,赋值失败为得不到锁, CAS 成功给 state 赋值 0 算释放锁,赋值失败为释放失败 // 可重入锁,每次获得锁 +1,每次释放锁 -1 private volatile long state; // 自旋超时阀值,单位纳秒 // 当设置等待时间时才会用到这个属性 static final long spinForTimeoutThreshold = 1000L;
2.1.2 共享锁和排它锁的区别
排它锁:同一时刻,只能有一个线程可以获得锁,也只能有一个线程可以释放锁。
共享锁:可以允许多个线程获得同一个锁,并且可以设置获取锁的线程数量。
2.1.3 同步队列属性
同步队列:当多个线程都来请求锁时,某一时刻有且只有一个线程能够获得锁(排它锁),那么剩余获取不到锁的线程,都会到同步队列中去排队并阻塞自己,当有线程主动释放锁时,就会从同步队列头开始释放一个排队的线程,让线程重新去竞争锁。
所以同步队列的主要作用阻塞获取不到锁的线程,并在适当时机释放这些线程。
同步队列底层数据结构是个双向链表:
// 同步队列的头。 private transient volatile Node head; // 同步队列的尾 private transient volatile Node tail;
源码中的 Node 是同步队列中的元素,但 Node 被同步队列和条件队列公用,我们在后面再说 Node。
2.1.4 条件队列的属性
条件队列和同步队列的功能一样,管理获取不到锁的线程,底层数据结构也是链表队列,但条件队列不直接和锁打交道,但常常和锁配合使用,是一定的场景下,对锁功能的一种补充。
条件队列的属性如下:
public class ConditionObject implements Condition, java.io.Serializable { private static final long serialVersionUID = 1173984872572414699L; /** First node of condition queue. */ // 条件队列中第一个 node private transient Node firstWaiter; /** Last node of condition queue. */ // 条件队列中最后一个 node private transient Node lastWaiter; //...// }
ConditionObject 称为条件队列,我们需要使用时,直接 new ConditionObject () 即可。
2.1.5 Node
Node 非常重要,即是同步队列的节点,又是条件队列的节点,在入队的时候,我们用 Node 把线程包装一下,然后把 Node 放入两个队列中:
static final class Node { /** * 同步队列单独的属性 */ //node 指示节点在共享模式下等待的标记 static final Node SHARED = new Node(); //node 指示节点正在以独占模式等待的标记 static final Node EXCLUSIVE = null; // 当前节点的前节点 // 节点获得成功后就会变成head // head 节点不能被取消 volatile Node prev; // 当前节点的下一个节点 volatile Node next; /** * 两个队列共享的属性 */ // 表示当前节点的状态,通过节点的状态来控制节点的行为 // 普通同步节点,就是 0 ,条件节点是 CONDITION -2 volatile int waitStatus; /** * waitStatus 的状态有以下几种 */ // 被取消 static final int CANCELLED = 1; // SIGNAL 状态的意义:同步队列中的节点在自旋获取锁的时候 // 如果前一个节点的状态是 SIGNAL,那么自己就可以阻塞休息了,否则自己一直自旋尝试获得锁 static final int SIGNAL = -1; // 表示当前 node 正在条件队列中 // 当有节点从同步队列转移到条件队列时,状态就会被更改成 CONDITION static final int CONDITION = -2; // 无条件传播,共享模式下,该状态的进程处于可运行状态 static final int PROPAGATE = -3; // 当前节点的线程 volatile Thread thread; // 在同步队列中,nextWaiter 并不真的是指向其下一个节点 // 我们用 next 表示同步队列的下一个节点,nextWaiter 只是表示当前 Node 是排它模式还是共享模式 // 但在条件队列中,nextWaiter 就是表示下一个节点元素 Node nextWaiter; //...// }
2.3 Condition
刚才我们看条件队列 ConditionObject 时,发现其是实现 Condition 接口的,现在我们一起来看下 Condition 接口,其类注释上是这么写的:
- 当 lock 代替 synchronized 来加锁时,Condition 就可以用来代替 Object 中相应的监控方法了,比如 Object wait ()、Object notify、Object notifyAll 这些方法;
- 提供了一种线程协作方式:一个线程被暂停执行,直到被其它线程唤醒;
- Condition 实例是绑定在锁上的,通过 Lock中的newCondition 方法可以产生该实例;
- 除了特殊说明外,任意空值作为方法的入参,都会抛出空指针;
- Condition 提供了明确的语义和行为,这点和 Object 监控方法不同。
类注释上甚至还给我们举了一个例子:
例如,假设我们有一个支持 put 和 take 方法的有界缓冲区。如果在空缓冲区上尝试获取,则线程将阻塞,直到项目可用;如果在一个完整的缓冲区上尝试放置,则线程将阻塞,直到有可用空间为止。我们希望继续等待 put 线程并将线程放在单独的等待集中,以便我们可以使用优化,当缓冲区中的项目或空间变得可用时,一次只通知单个线程。这可以使用两个 Condition 实例来实现。
假设我们有一个有界边界的队列,支持 put 和 take 方法,需要满足:
- 如果试图往空队列上执行 take,线程将会阻塞,直到队列中有可用的元素为止;
- 如果试图往满的队列上执行 put,线程将会阻塞,直到队列中有空闲的位置为止。
1、2 中线程阻塞都会到条件队列中去阻塞。
take 和 put 两种操作如果依靠一个条件队列,那么每次只能执行一种操作,所以我们可以新建两个条件队列,这样就可以分别执行操作了。
除了类注释,Condition 还定义出一些方法,这些方法奠定了条件队列的基础,方法主要有:
void await() throws InterruptedException;
这个方法的主要作用是:使当前线程一直等待,直到被 signalled 或被打断。
当以下四种情况发生时,条件队列中的线程将被唤醒
- 有线程使用了 signal 方法,正好唤醒了条件队列中的当前线程;
- 有线程使用了 signalAll 方法;
- 其它线程打断了当前线程,并且当前线程支持被打断;
- 被虚假唤醒 (即使没有满足以上 3 个条件,wait 也是可能被偶尔唤醒。
被唤醒时,有一点需要注意的是:线程从条件队列中苏醒时,必须重新获得锁,才能真正被唤醒,这个我们在说源码的时候,也会强调这个。
await 方法还有带等待超时时间的,如下:
// 返回的 long 值表示剩余的给定等待时间,如果返回的时间小于等于 0 ,说明等待时间过了 // 选择纳秒是为了避免计算剩余等待时间时的截断误差 long awaitNanos(long nanosTimeout) throws InterruptedException; // 虽然入参可以是任意单位的时间,但底层仍然转化成纳秒 boolean await(long time, TimeUnit unit) throws InterruptedException;
除了等待方法,还是唤醒线程的两个方法,如下:
// 唤醒条件队列中的一个线程,在被唤醒前必须先获得锁 void signal(); // 唤醒条件队列中的所有线程 void signalAll();
至此,AQS 基本的属性就已经介绍完了,接着让我们来看一看 AQS 的重要方法。
三、同步器的状态
在同步器中,我们有两个状态,一个叫做 state,一个叫做 waitStatus,两者是完全不同的概念:
- state 是锁的状态,是 int 类型,子类继承 AQS 时,都是要根据 state 字段来判断有无得到锁,比如当前同步器状态是 0,表示可以获得锁,当前同步器状态是 1,表示锁已经被其他线程持有,当前线程无法获得锁;
- waitStatus 是节点(Node)的状态,种类很多,一共有初始化 (0)、CANCELLED (1)、SIGNAL (-1)、CONDITION (-2)、PROPAGATE (-3),各个状态的含义可以见上文。
这两个状态我们需要牢记,不要混淆了。
这篇关于AbstractQueuedSynchronizer源码解析(上)的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!
- 2024-11-23增量更新怎么做?-icode9专业技术文章分享
- 2024-11-23压缩包加密方案有哪些?-icode9专业技术文章分享
- 2024-11-23用shell怎么写一个开机时自动同步远程仓库的代码?-icode9专业技术文章分享
- 2024-11-23webman可以同步自己的仓库吗?-icode9专业技术文章分享
- 2024-11-23在 Webman 中怎么判断是否有某命令进程正在运行?-icode9专业技术文章分享
- 2024-11-23如何重置new Swiper?-icode9专业技术文章分享
- 2024-11-23oss直传有什么好处?-icode9专业技术文章分享
- 2024-11-23如何将oss直传封装成一个组件在其他页面调用时都可以使用?-icode9专业技术文章分享
- 2024-11-23怎么使用laravel 11在代码里获取路由列表?-icode9专业技术文章分享
- 2024-11-22怎么实现ansible playbook 备份代码中命名包含时间戳功能?-icode9专业技术文章分享