数据结构
AQS内部是一个双向链表,其中head是一个虚节点。
// +------+ prev +-----+ +-----+
// head | | <---- | | <---- | | tail
// +------+ +-----+ +-----+
static final class Node {
static final Node SHARED = new Node(); //共享模式的标记
static final Node EXCLUSIVE = null; //独占模式标记
static final int CANCELLED = 1; //线程已取消
static final int SIGNAL = -1; //后继节点需要唤醒
static final int CONDITION = -2; //处于条件队列
static final int PROPAGATE = -3; //共享模式才会使用的,无条件传播acquireShared
//node状态,上面的几个状态值和0,同步队列中初始化为 0,条件队列中初始化为 CONDITION。
//使用 CAS 进行修改,大于0意味着节点不需要唤醒。
volatile int waitStatus;
//链接到当前节点线程的前驱节点(依赖前驱节点的waitStatus进行后续操作)。
//在取消前驱时,找到未取消的前驱并链接,因为【head永远不会被取消】。
//只有在成功得到锁后才成为head。被取消的线程永远不会得到锁,且线程只会取消自己所属节点。
volatile Node prev;
//链接到当前节点线程解除阻塞被释放后的后继节点。
//在入队期间分配,在绕过取消的前驱时进行调整,并在出队时清空为null(为了 GC)。
//enq 操作直到prev分配后才分配前驱的next字段,因此next为null并不意味着该节点位于队列末尾。
//如果next字段为null,我们可以从尾部扫描 prev 进行双重检查。CANCELLED节点的next字段设置为指向节点本身。
volatile Node next;
volatile Thread thread; //使该节点入队的线程。使用后为null
//独占模式时,如果在条件队列(只有在独占模式下才能访问)是一个单向链表,nextWaiter是单向链表的next指针。
//非独占模式时,指向SHARED代表共享,指向EXCLUSIVE代表独占
Node nextWaiter;
final boolean isShared() {
return nextWaiter == SHARED;
}
//当前驱不能为空时使用,返回前一个节点。
final Node predecessor() throws NullPointerException {
Node p = prev;
if (p == null)
throw new NullPointerException();
else
return p;
}
Node() { // Used to establish initial head or SHARED marker
}//用于建立初始头部或SHARED标记
Node(Thread thread, Node mode) { // 给addWaiter方法使用
this.nextWaiter = mode;
this.thread = thread;
}
Node(Thread thread, int waitStatus) { // 给Condition对象使用
this.waitStatus = waitStatus;
this.thread = thread;
}
}
//条件队列Condition数据结构
public class ConditionObject implements Condition, java.io.Serializable {
/** First node of condition queue. */
private transient Node firstWaiter;
/** Last node of condition queue. */
private transient Node lastWaiter;
}
Node注释翻译
-
等待队列是“CLH”(Craig、Landin和Hagersten)锁定队列的变体。CLH锁通常用于自旋锁。我们将它们用于阻塞同步器,但使用相同的基本策略,即在其节点的前身中保存线程的一些控制信息。每个节点中的“status”字段跟踪线程是否应该阻塞。当节点的上一个节点释放锁时,就唤醒节点。否则,队列的每个节点充当一个特定通知样式的监视器,它持有单个等待线程。状态字段不控制线程是否被授予锁等等。
-
如果线程是队列中的第一个线程,它可能会尝试获取锁。但第一个线程并不能保证成功;它只会给你竞争的权利。因此,当前已释放锁的竞争者线程可能需要重新等待。
-
要排队到CLH锁中,只需将其作为新尾巴进行原子拼接。要退出队列,只需设置head字段。
-
插入到CLH队列中只需要一个“tail”字段上的原子操作,因此有一个简单的原子操作来区分【未入队】和【已入队】。类似地,退出队列只涉及更新“head”字段。
-
但是,节点需要做更多的工作来确定谁是它们的后继节点,部分原因是为了处理由于超时和中断而可能出现的取消。
-
“prev”字段的链表(在原始CLH锁中不使用)主要用于处理取消。如果一个节点被取消,它的后继节点(通常)会重新链接到一个【未取消】的前驱节点。
-
我们还使用“next”链表来实现阻塞机制。每个节点的线程id保存在其自己的节点中,因此,一个前驱节点通过遍历“next”链表来确定它是哪个线程,从而通知唤醒下一个节点。
-
后继节点必须避免与新入队的节点竞争设置它们的前驱节点的“next”字段。当一个节点的后继节点显示为空时,在必要时可以通过从“tail”向后检查来解决这个问题。(或者,换句话说,“next”链表是一个优化,所以我们通常不需要向后扫描。)
-
对消在基本算法中引入了一些保守性。由于我们必须轮询其他节点的取消,我们可能会忽略一个被取消的节点不管是在我们前面还是后面。处理这个问题的方法是,在被取消的时候总是唤醒后继节点,允许他们链接在一个新的前驱节点上,直到我们能够确定一个未被取消的前驱节点。
-
CLH队列需要一个虚拟头节点来启动。但我们不是在构造器中创建的,因为如果没有竞争,那就是浪费精力。相反,在第一次争用时构造节点并设置头指针和尾指针。
条件队列相关
-
等待条件的线程使用相同的node,但使用了一个额外的链表。条件队列只需要链接单(非并发)链表中的节点,因为它们只有在独占时才被访问。
-
在await时,节点被插入到条件队列中。在signal后,节点就被转移到主队列。status字段的一个特殊值CONDITION 用于标记节点所在的队列。
AQS属性
//等待队列的头指针,延迟初始化。除初始化外,只能通过 setHead 方法进行修改。
//注意:如果head存在,它的waitStatus一定不是CANCELLED。
private transient volatile Node head;
//等待队列的尾部指针,延迟初始化。仅通过方法 enq 修改以添加新的等待节点。
private transient volatile Node tail;
//同步器状态值
private volatile int state;
AQS架构图

网友评论