关于AQS源码(AbstractQueuedSynchronizer)的解读记录。

本文解析了Java并发库AQS中的Node结构,介绍了Node的构造、状态以及AQS如何通过Node实现独占和共享锁的获取、队列操作。重点讲解了enqueue和addWaiter方法的工作原理。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

我是在学习ReentrantLock的时候看到了AQS这个东西的,于是就去看了他的源码

Node结点介绍

在AQS里面有一个内部类为Node

static final Node SHARED = new Node();

static final Node EXCLUSIVE = null;

SHARED为结点的模式意思为共享模式,EXCLUSIVE表示为结点的模式为独占模式。

static final int CANCELLED =  1;

static final int SIGNAL    = -1;

static final int CONDITION = -2;
        
static final int PROPAGATE = -3;

上述似乎是Node对象的状态描述的几个参数。

volatile int waitStatus;

volatile Node prev;

volatile Node next;

volatile Thread thread;

Node nextWaiter;

waitStatus表示的是等待状态也是模式的一种,后面三个prev、next、thread则是Node对象存储的主要内容,应该说的是在AQS中Node和Node之间是以双向链表的形式存在的。如下图所示。

prev指向前置节点,next指向后置节点。这里的前置和后置是相对于本身节点而言。 在AQS中head指向头节点,tail指向尾巴节点。thread存储的是当前节点的线程任务。

Node() {}

Node(Thread thread, Node mode) {
    this.nextWaiter = mode;
    this.thread = thread;
}

Node(Thread thread, int waitStatus) {
    this.waitStatus = waitStatus;
    this.thread = thread;
}

Node具有的三种构造器

1.无参构造器,没有做任何操作

2.有参构造器带thread和node对象的将他们赋值给nextWaiter和thread

3.有参构造器带thread和waitStatus等待状态的将他们赋值给waitStatus和thread

final Node predecessor() throws NullPointerException {
    Node p = prev;
    if (p == null)
        throw new NullPointerException();
    else
        return p;
}

Node的内部方法predecessor,将prev属性的值(指向的是该节点的前置节点)赋值给一个实例的Node对象p,如果p为空则没有前置节点,本节点即为头结点。否则返回的是当前节点的前置结点。总结:如果本节点是头结点,则抛异常,反之则将前置节点返回。

final boolean isShared() {
    return nextWaiter == SHARED;
}

Node的内部方法isShared,该方法将字段SHARED和nextWaiter两个Node对象做比较,是否一直的boolean判断返回。

接下来看AQS自己的内容。

private transient volatile Node head;

private transient volatile Node tail;

private volatile int state;

protected final int getState() {
    return state;
}

protected final void setState(int newState) {
    state = newState;
}

由前面可以知道head和tail分别是指向AQS链表的头和尾结点。state为状态,暂不知道是什么状态,有两个方法分别是简单的get和set方法,简单理解就是取值和赋值的两个方法。

AQS骨架介绍

AQS大概分为以下几个部分:

1.获取独占锁
2.获取共享锁
3.获取锁失败后将线程丢进等待队列
4.释放锁:独占锁和共享锁

入队操作,将线程丢进等待队列

private Node enq(final Node node) {
    for (;;) {
        Node t = tail;
        if (t == null) { // Must initialize
            if (compareAndSetHead(new Node()))
                tail = head;
        } else {
            node.prev = t;
            if (compareAndSetTail(t, node)) {
                t.next = node;
                return t;
            }
        }
    }
}
private final boolean compareAndSetHead(Node update) {
    return unsafe.compareAndSwapObject(this, headOffset, null, update);
}
private final boolean compareAndSetTail(Node expect, Node update) {
    return unsafe.compareAndSwapObject(this, tailOffset, expect, update);
}

AQS的enq方法(入队操作),入参为一个Node对象,使用for(;;)类似于while(true),只要没有return,则重复执行,将tail对象的值赋给新的Node实例t,健壮性判断 如果t为null则表示tail节点为null,那么就是之前并不存在尾节点,此时初始化使用一个CAS原子性操作,比较this对象的headoffset偏移量处的值是否为 null,如果值为 null,则将 headOffset处的值替换为新的节点 update当前线程返回 true。如果值不为 null,则说明其他线程已经在执行该操作,当前线程返回false就行。当为true时,将tail尾结点的指向也设置为head。此时初始化完成,效果如下图所示。

多提一嘴:将tail=head;是将head的引用赋给tail,所以他们指向同一个东西,那就是在if判断条件中CAS中new出来的Node结点对象。

如果t不为null,说明队列不为空,此时只需要将当前结点插入队列的最后。代码实现是将t(尾巴结点)赋值给当前结点的前置节点即node.prev。在if判断中又是一个CAS原子性操作,将尾巴结点设置为当前待插入的结点。只有一个线程可以设置成功,成功之后返回true,进入if内部,同时将之前的尾结点的下一个结点设置为当前结点。最后将尾结点返回。设置尾结点的流程可以看下图。

总结一下:这个enq的入队操作就是先看是否有尾结点,没有则说明需要初始化,初始化之后因为for(;;),重新判断此时将当前结点插入到队末,返回当前队列的尾巴结点。

private Node addWaiter(Node mode) {
    Node node = new Node(Thread.currentThread(), mode);
    // Try the fast path of enq; backup to full enq on failure
    Node pred = tail;
    if (pred != null) {
        node.prev = pred;
        if (compareAndSetTail(pred, node)) {
            pred.next = node;
            return node;
        }
    }
    enq(node);
    return node;
}

这个是addWaiter,顾名思义就是将线程添加到等待队列中,创建一个node对象,将当前工作线程和mode模式放入,接来下获取队列的尾结点tail并赋值给pred,如果pred不为空,则表示队列中已经有节点存在,在这种情况下将新结点的prev指向pred,然后通过CAS将新结点设置为队列的尾结点,将pred的next指针指向新节点,最后返回新节点。(此处的处理其实同enq的处理相同,但是目的是不同的,在addWaiter中的空判断是查看是否可以使用快速路径添加,将结点添加到队列当中,而在enq中的判断则是验证队列是否初始化,是一种健壮性的判断),至此为AQS同步队列的入队操作。

获取独占式锁

public final void acquire(int arg) {
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}
protected boolean tryAcquire(int arg) {
    throw new UnsupportedOperationException();
}
//这里只展示两行代码就可以理解了。
private Node addWaiter(Node mode) {
    Node node = new Node(Thread.currentThread(), mode);
final boolean acquireQueued(final Node node, int arg) {
    boolean failed = true;
    try {
        boolean interrupted = false;
        for (;;) {
            final Node p = node.predecessor();
            if (p == head && tryAcquire(arg)) {
                setHead(node);
                p.next = null; // help GC
                failed = false;
                return interrupted;
            }
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    int ws = pred.waitStatus;
    if (ws == Node.SIGNAL)
        return true;
    if (ws > 0) {
        do {
            node.prev = pred = pred.prev;
        } while (pred.waitStatus > 0);
        pred.next = node;
    } else {
        compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
    }
    return false;
}

这个过程为获取独占式锁的过程,主要为以下几个方法构成

1.tryAcquire
2.addWaiter
3.acquireQueued
4.selfInterrupt

tryAcquire方法需要子类对象重写,否则直接抛出异常UnsupportedOperationException,主要作用为获取同步状态。
addWaiter会包装一个新的Node结点对象,其中将模式通过传参注入,由之前可见EXCLUSIVE = null,此时状态是独占锁
acquireQueued首先获取了当前节点的前置节点,后续做了两个if的判断,首先第一个,如果前置节点是头结点且当前节点获取到了同步状态,那么直接将当前节点设置为头结点,并且结点的引用设置为null方便GC的回收,(可能有人有疑问了,这个线程执行了吗,怎么刚设置为头结点就全部设置为null了,当然是执行了具体的线程执行是在tryAcquire(arg)方法中进行的,所以此时的node结点已经完成了任务,他的作用是为了存储线程对象的引用,既然已经执行完,那么就不需要继续存储了,设置为null,等新的头结点诞生之后,新的头结点的node.prev是指向旧的头结点,也会被取消,此时旧的头结点就是等待被GC回收),会不会讲的有点啰嗦。第二个if判断中,将前置节点和当前节点统统传入shouldParkAfterFailedAcquire方法中,获取前置节点的等待状态,如果等待状态为SIGNAL,文章最开始有提到,这个是-1,这是一个释放信号

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值