AQS(AbstractQueuedSynchronizer)是java同步的基本框架,依赖于先进先出(FIFO)等待队列的阻塞锁和相关同步器(信号量、事件等),我们常用的ReentrantLock,Semaphore,
CountDownLatch等内部均是实现AQS的同步机制,因此要深入理解java同步机制,先从理解AQS框架开始。
AQS是一个抽象类这个类继承AbstractOwnableSynchronizer, AbstractOwnableSynchronizer这个类用于设置当前线程的辅助类。
AQS内部维护了一个结点队列,队列上维护的是获取同步上锁的线程,了解AQS先从了解这些Node的定义开始。
AQS内部定义Node源码:
/**
* 等待队列节点类
*
* 等待队列是“CLH”(Craig、Landin和Hagersten)锁队列的变体。CLH锁通常用于自旋锁。相反,我们使用它们来阻塞同步器,
* 但是使用相同的基本策略,即在其节点的前身中保存关于线程的一些控制信息。每个节点中的“status”字段跟踪线程是否应该阻塞。
* 节点在其前任节点释放时发出信号。否则,队列的每个节点都充当一个特定的通知样式的监视器,其中包含一个等待线程。
* 状态字段不控制线程是否被授予锁等。一个线程可能试图获取队列中的第一个线程。
* 但领先并不能保证成功;它只给予竞争的权利。因此,当前发布的竞争者线程可能需要重新等待。
*
* 要加入到CLH锁中,您可以原子地将其拼接为new tail。要退出队列,只需设置head字段。
*
* +------+ prev +-----+ +-----+
* head | | <---- | | <---- | | tail
* +------+ +-----+ +-----+
*
* 插入CLH队列只需要在“tail”上执行一个原子操作,因此存在一个简单的原子点,用于从未排队到排队的划分。类似地,
* 退出队列只涉及更新“head”。然而,节点需要做更多的工作来确定谁是他们的继任者,部分原因是处理由于超时和中断而可能发生的取消。
*
* “prev”链接(在原来的CLH锁中没有使用)主要用于处理取消。如果一个节点被取消,它的继承节点(通常)会重新链接到
* 一个未取消的继承节点。有关自旋锁的类似机制的解释,请参阅Scott和Scherer的论文at:
* http://www.cs.rochester.edu/u/scott/synchronization/
*
* 我们还使用“next”链接来实现阻塞机制。每个节点的线程id都保存在它自己的节点中,因此,前一个节点通过遍历
* 下一个链接来通知下一个要唤醒的节点,以确定它是哪个线程。继任者的确定必须避免使用新排队的节点来设置其前任的“下一个”字段的竞争。
* 当节点的后继出现null时,
* 通过从原子更新的“tail”向后检查,可以在必要时解决这个问题。(或者,换句话说,next-links是一种优化,因此我们通常不需要反向扫描。)
*
* 对消在基本算法中引入了一些保守性。因为我们必须轮询其他节点的取消,所以我们可能会忽略被取消的节点是在前面还是在后面。处理
* 这一问题的方法是,总是在取消时取消后继者的泊车位,使他们能够稳定地依靠新的前任,除非我们能够确定将承担这一责任的未取消的前任。
*
* CLH队列需要一个虚拟头节点来启动。但是,我们不会在构建过程中创建它们,因为如果从来没有争用,
* 就会浪费精力。相反,将构造节点,并在第一次争用时设置head和tail指针。
*
* 等待条件的线程使用相同的节点,但使用额外的链接。条件只需要在简单(非并发)链接队列中链接节点,因为它们只在独占时才被访问。
* 在等待时,将节点插入到条件队列中。信号一发出,节点就被转移到主队列。状态字段的一个特殊值用于标记节点所在的队列。
*
* 感谢Dave Dice、Mark Moir、Victor Luchangco、Bill Scherer和Michael Scott,以及JSR-166专家组的成员,
* 为本课程的设计提供了有用的想法、讨论和批评。
*/
static final class Node {
/** 指示节点在共享模式下等待的标记 */
static final Node SHARED = new Node();
/** 标记指示节点在独占模式下等待 */
static final Node EXCLUSIVE = null;
/** waitStatus的值表示线程已被取消 */
static final int CANCELLED = 1;
/** waitStatus的值指示唤醒等待中的后续线程 */
static final int SIGNAL = -1;
/** waitStatus的值表示条件等待 */
static final int CONDITION = -2;
/** waitStatus的值表示下一个acquireShared()应该无条件传播 */
static final int PROPAGATE = -3;
/**
* 状态值,只接受以下值:
* SIGNAL: 此节点的后继节点(或将很快)被阻塞(通过park),因此当前节点在释放或取消时必须取消对其后继节点的存储。
* 为了避免争用,获取方法必须首先表明它们需要一个信号,然后重试原子获取,然后在失败时阻塞
* CANCELLED: 由于超时或中断,此节点被取消。节点永远不会离开此状态。特别是,具有已取消节点的线程再也不会阻塞。
* CONDITION: 此节点当前位于条件队列中。在传输之前,它不会被用作同步队列节点,此时状态将被设置为0。
* (这里这个值的使用与场的其他用途无关,而是简化了力学。)
* PROPAGATE: 已发布的节点应该传播到其他节点。这在doReleaseShared中设置(仅针对head节点),以确保传播继续进行,
* 即使其他操作已经介入。
* 0: 以上皆非
*
* 数值的排列是为了简化使用。非负值表示节点不需要发出信号。因此,大多数代码不需要检查特定的值,只需要检查符号。
*
* 对于普通同步节点,字段初始化为0,对于条件节点,字段初始化为条件。使用CAS(或者在可能的情况下,使用无条件的volatile写)修改它。
*/
volatile int waitStatus;
/**
* 链接到当前节点/线程所依赖的用于检查等待状态的前辈节点。在排队时分配,只有在退出排队时才为空(为了GC)。
* 此外,在取消前一个节点时,我们会在找到一个未取消的节点时短路,因为头节点从来没有被取消过:一个节点只有在成功获取后才成为头节点。
* 被取消的线程永远不会成功获取,并且线程只取消自己,而不取消任何其他节点。
*/
volatile Node prev;
/**
* 链接到当前节点/线程在发布时解压的后续节点。在排队过程中分配,在绕过已取消的前辈时进行调整,在退出队列时为空(为了GC)。
* enq操作直到附件之后才分配前任的下一个字段,所以看到一个空的next字段并不一定意味着节点在队列的末尾。然而,
* 如果下一个字段显示为空,我们可以从尾部扫描prev以进行双重检查。
* 取消节点的下一个字段被设置为指向节点本身,而不是null,以简化isOnSyncQueue的工作。
*/
volatile Node next;
/**
* 进入此节点队列的线程。在构造时初始化,使用后为空。
*/
volatile Thread thread;
/**
* 链接到正在等待状态的下一个节点,或共享的特殊值。因为条件队列只有在独占模式下才能访问,
* 所以我们只需要一个简单的链接队列来在节点等待条件时保存节点。
* 然后将它们转移到队列中重新获取。由于条件只能是独占的,所以我们使用特殊值来表示共享模式来保存字段。
*/
Node nextWaiter;
/**
* 如果节点在共享模式下等待,则返回true。
*/
final boolean isShared() {
return nextWaiter == SHARED;
}
/**
* 返回以前的节点,如果为空则抛出NullPointerException。当前任不能为空时使用。可以省略null检查,但它是用来帮助VM的。
*
* @return 这个结点的前驱结点
*/
final Node predecessor() throws NullPointerException {
Node p = prev;
if (p == null)
throw new NullPointerException();
else
return p;
}
Node() { // 用于建立初始头结点或共享标记
}
Node(Thread thread, Node mode) { // 用于 addWaiter
this.nextWaiter = mode;
this.thread = thread;
}
Node(Thread thread, int waitStatus) { // 用于 Condition
this.waitStatus = waitStatus;
this.thread = thread;
}
}
这个类Node被AbstractQueuedSynchronizer使用,用于在获取锁时对当前等待线程的排队机制。