AQS 队列同步器是用来构建锁或其他同步组件的基础框架,它使用一个 volatile int state 变量作为共享资源,如果线程获取资源失败,则进入同步队列等待;如果获取成功就执行临界区代码,释放资源时会通知同步队列中的等待线程。
同步器的主要使用方式是继承,子类通过继承同步器并实现它的抽象方法来管理同步状态,对同步状态进行更改需要使用同步器提供的 3个方法 getState、setState 和 compareAndSetState ,它们保证状态改变是安全的。子类推荐被定义为自定义同步组件的静态内部类,同步器自身没有实现任何同步接口,它仅仅定义若干同步状态获取和释放的方法,同步器既支持独占式也支持共享式。
同步器是实现锁的关键,在锁的实现中聚合同步器,利用同步器实现锁的语义。锁面向使用者,定义了使用者与锁交互的接口,隐藏实现细节;同步器面向锁的实现者,简化了锁的实现方式,屏蔽了同步状态管理、线程排队、等待与唤醒等底层操作。
每当有新线程请求资源时都会进入一个等待队列,只有当持有锁的线程释放锁资源后该线程才能持有资源。等待队列通过双向链表实现,线程被封装在链表的 Node 节点中,Node 的等待状态包括:CANCELLED(线程已取消)、SIGNAL(线程需要唤醒)、CONDITION (线程正在等待)、PROPAGATE(后继节点会传播唤醒操作,只在共享模式下起作用)。
1、AQS 有哪两种模式?
独占模式表示锁只会被一个线程占用,其他线程必须等到持有锁的线程释放锁后才能获取锁,同一时间只能有一个线程获取到锁。
共享模式表示多个线程获取同一个锁有可能成功,ReadLock 就采用共享模式。
独占模式通过 acquire 和 release 方法获取和释放锁,共享模式通过 acquireShared 和 releaseShared 方法获取和释放锁。
2、AQS 独占式获取/释放锁的原理?
获取同步状态时,调用 acquire 方法,维护一个同步队列,使用 tryAcquire 方法安全地获取线程同步状态,获取失败的线程会被构造同步节点并通过 addWaiter 方法加入到同步队列的尾部,在队列中自旋。之后调用 acquireQueued 方法使得该节点以死循环的方式获取同步状态,如果获取不到则阻塞,被阻塞线程的唤醒主要依靠前驱节点的出队或被中断实现,移出队列或停止自旋的条件是前驱节点是头结点且成功获取了同步状态。
释放同步状态时,同步器调用 tryRelease 方法释放同步状态,然后调用 unparkSuccessor 方法唤醒头节点的后继节点,使后继节点重新尝试获取同步状态。
3、为什么只有前驱节点是头节点时才能尝试获取同步状态?
头节点是成功获取到同步状态的节点,后继节点的线程被唤醒后需要检查自己的前驱节点是否是头节点。
目的是维护同步队列的 FIFO 原则,节点和节点在循环检查的过程中基本不通信,而是简单判断自己的前驱是否为头节点,这样就使节点的释放规则符合 FIFO,并且也便于对过早通知的处理,过早通知指前驱节点不是头节点的线程由于中断被唤醒。
4、AQS 共享式式获取/释放锁的原理?
获取同步状态时,调用 acquireShared 方法,该方法调用 tryAcquireShared 方法尝试获取同步状态,返回值为 int 类型,返回值不小于于 0 表示能获取同步状态。因此在共享式获取锁的自旋过程中,成功获取同步状态并退出自旋的条件就是该方法的返回值不小于0。
释放同步状态时,调用 releaseShared 方法,释放后会唤醒后续处于等待状态的节点。它和独占式的区别在于 tryReleaseShared 方法必须确保同步状态安全释放,通过循环 CAS 保证,因为释放同步状态的操作会同时来自多个线程。
5、阻塞队列有哪些选择?
阻塞队列支持阻塞插入和移除,当队列满时,阻塞插入元素的线程直到队列不满。当队列为空时,获取元素的线程会被阻塞直到队列非空。阻塞队列常用于生产者和消费者的场景,阻塞队列就是生产者用来存放元素,消费者用来获取元素的容器。
Java 中的阻塞队列
ArrayBlockingQueue,由数组组成的有界阻塞队列,默认情况下不保证线程公平,有可能先阻塞的线程最后才访问队列。
LinkedBlockingQueue,由链表结构组成的有界阻塞队列,队列的默认和最大长度为 Integer 最大值。
PriorityBlockingQueue,支持优先级的无界阻塞队列,默认情况下元素按照升序排序。可自定义 compareTo 方法指定排序规则,或者初始化时指定 Comparator 排序,不能保证同优先级元素的顺序。
DelayQueue,支持延时获取元素的无界阻塞队列,使用优先级队列实现。创建元素时可以指定多久才能从队列中获取当前元素,只有延迟期满时才能从队列中获取元素,适用于缓存和定时调度。
SynchronousQueue,不存储元素的阻塞队列,每一个 put 必须等待一个 take。默认使用非公平策略,也支持公平策略,适用于传递性场景,吞吐量高。
LinkedTransferQueue,链表组成的无界阻塞队列,相对于其他阻塞队列多了 tryTransfer 和 transfer 方法。transfer方法:如果当前有消费者正等待接收元素,可以把生产者传入的元素立刻传输给消费者,否则会将元素放在队列的尾节点并等到该元素被消费者消费才返回。tryTransfer 方法用来试探生产者传入的元素能否直接传给消费者,如果没有消费者等待接收元素则返回 false,和 transfer 的区别是无论消费者是否消费都会立即返回。
LinkedBlockingDeque,链表组成的双向阻塞队列,可从队列的两端插入和移出元素,多线程同时入队时减少了竞争。
实现原理
使用通知模式实现,生产者往满的队列里添加元素时会阻塞,当消费者消费后,会通知生产者当前队列可用。当往队列里插入一个元素,如果队列不可用,阻塞生产者主要通过 LockSupport 的 park 方法实现,不同操作系统中实现方式不同,在 Linux 下使用的是系统方法 pthread_cond_wait 实现。