AbstractQueuedSynchronizer源码解析

AQS源码解析

简介

AbstractQueuedSynchronizer从名字可以看到三个信息:抽象的、同步的队列。它提供了同步状态、阻塞与唤醒线程以及先进先出队列的基础框架。JDK中许多并发工具类的实现都基于AQS,如ReentrantLock。

1、重要的成员变量

和所有的队列一样,FIFO队列也有头节点head和尾节点tail。
state是同步器的状态,什么是同步器的状态呢?可以理解为锁冲入的次数,开始state值为0,没有线程获取锁。当某个线程尝试获取锁并且成功的时候,state的值变为1。其它线程想要获取锁时,发现state = 1,则进入同步队列阻塞。那么为什么不用boolean表示呢?因为AQS底层设计为可重入锁,如果一个线程获取锁的时候发现state > 0,但是currentThread是自己,则state加1,获取锁成功。当然释放锁的时候,state是多少就要释放多少次。

private transient volatile Node head;
private transient volatile Node tail;
// The synchronization state.
private volatile int state;

2、构造方法

不重要

3、核心方法

3.1 内部类 Node()

我们发现,只要涉及到队列,基本上都有一个内部类Node()。但AQS的node类比较复杂,主要是有很多的变量,我们来一个个看它们具体代表什么含义。
SHARED: 表明该锁是共享锁。

EXCLUSIVE : 表明该锁是排他锁。

CANCELLED : 1;表明该节点被取消了。

SIGNAL : -1;表示该节点的后继节点处于阻塞状态,当该节点释放锁(releases)时,需要唤醒他的后继节点。正常情况下,同步队列中的大部分节点的状态都为SIGNAL。

CONDITION :-2;表明该节点是在条件队列(condition queue)上等待,而不是同步队列。由于AQS不仅可以用来当锁(替代synchronized关键字),还具有替代Object的wait/notify的功能,如果一个线程调用了Condition.await(),该线程会加入到条件队列中(而不是同步队列),并将状态设置为CONDITION。

PROPAGATE : -3;共享模式下,由于可以有不只一个线程享用锁,前置节点不仅会唤醒他的后继节点,还可能会将唤醒操作进行“传播”,也就是会唤醒后继节点的后继节点。

waitStatus: 节点状态

nextWaiter: 1. 当该节点处于同步队列时(AQS自己本身的CLH队列),表明该节点是在竞争锁。如果是独占锁,那么nextWaiter=null,如果是共享锁,那么nextWaiter=new Node()。也就是说,nextWaiter此时仅仅相当于一个flag,用来表明该锁是独占锁还是共享锁。2. 当该节点处于条件队列时,nextWaiter指向他在队列中的后继节点。

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;
	volatile int waitStatus;
	volatile Node prev;
	volatile Node next;
	volatile Thread thread;
	Node nextWaiter;

	Node() {
 	}

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

3.2 内部类 ConditionObject()

// 暂不讲解

3.3 获取锁 acquire(int arg)

acquire()一共做了四件事:

  • 尝试获取锁。
  • 获取锁失败,则调用addWaiter()将当前线程加入同步队列。
  • acquireQueued(),死循环,一直尝试获取锁或直接阻塞。
  • selfInterrupt(),中断。

下面详细介绍下acquire()在这期间具体做了些什么。

public final void acquire(int arg) {
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}

protected boolean tryAcquire(int arg) {
    throw new UnsupportedOperationException();
}

3.3.1 tryAcquire(int arg)
这个方法用于子类重写,实现自己需要的获取锁的逻辑。

// AQS
protected boolean tryAcquire(int arg) {
    throw new UnsupportedOperationException();
}

下面是ReentractLock中非公平锁的对此方法的重写。简单解释下:

  • 首先获取当前锁状态,如果state=0,则直接尝试CAS获取锁(突出一个非公平),获取成功则将exclusiveOwnerThread设为当前线程。
  • 如果state != 0,则看持有锁的线程是否为当前线程,如果是的话,直接重入,setState(c + acquires) 即重入次数。
// ReentractLock
protected final boolean tryAcquire(int acquires) {
    return nonfairTryAcquire(acquires);
}

final boolean nonfairTryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    if (c == 0) {
        if (compareAndSetState(0, acquires)) {
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    else if (current == getExclusiveOwnerThread()) {
        int nextc = c + acquires;
        if (nextc < 0) 
        	// 这里小于0是因为最高位为符号位。
            throw new Error("Maximum lock count exceeded");
        setState(nextc);
        return true;
    }
    return false;
}

3.3.2 addWaiter(Node mode)
为当前线程创建一个节点mode并插入到同步队列的末尾。当然tail尾节点不能为null,为null则说明队列还未初始化,需要enq(node)初始化。enq(node)里面是一个死循环,先校验是否初始化,然后和addWaiter()前半部分一样将mode节点插入末尾。
那么为什么不直接调用enq(node),反而在外面先尝试插入呢?源码中有相关注释,主要是在锁竞争不激烈的情况下,一次CAS基本就能成功插入,提升性能。

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;
}

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;
            }
        }
    }
}

3.3.3 acquireQueued(final Node node, int arg)
这个方法主要分为两个部分:
1、死循环,然后判断当前节点的上一个节点是否为头结点,如果是,则尝试获取锁,成功则返回。如果不是,则执行shouldParkAfterFailedAcquire(p, node)先检查并且判断该节点是否应该阻塞,如果为true,再执行parkAndCheckInterrupt()检查是否应该中断,返回中断标记。
2、如果获取锁失败,则取消获取锁(逻辑复杂,后续讲解)。

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);
    }
}

接下来我们详细看看 shouldParkAfterFailedAcquire() 方法中具体做了什么:
1、if (ws == Node.SIGNAL) 事实上,队列中绝大多数的节点的状态都为SIGNAL,表明当该节点释放锁(releases)时,需要唤醒他的后继节点。此时直接返回true,表明已经准备好阻塞了。
2、if (ws > 0) 如果前一个节点pre状态大于0(CANCLED状态),则该节点已被取消,此时pre不具备有唤醒当前节点的能力,则需要往前直到找到一个ws <= 0 的节点。
3、将该节点状态设为SIGNAL,返回false。

这里有个疑点,为什么最后将前一个节点状态设为SIGNAL后,不返回true返回false,在下次循环进入再返回true呢?
这里和释放节点release有关,先卖个关子,后面将释放的时候再说明。

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    int ws = pred.waitStatus;
    if (ws == Node.SIGNAL)
        /*
         * 如果前一个节点为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;
}

3.3.4 selfInterrupt()
如果3.3.3中的返回值为true,则中断当前线程(安心睡觉去了,等待唤醒 )。

static void selfInterrupt() {
    Thread.currentThread().interrupt();
}

3.4 释放锁 release(int arg)

首先这里有个tryRelease(arg),用于子类重写释放锁逻辑。如果释放锁成功,则开始尝试唤醒头节点下一个节点
唤醒有个前提,h != null && h.waitStatus != 0 所以如果 头节点的状态为0,则不唤醒下一个节点

public final boolean release(int arg) {
    if (tryRelease(arg)) {
        Node h = head;
        if (h != null && h.waitStatus != 0)
            unparkSuccessor(h);
        return true;
    }
    return false;
}

unparkSuccessor(Node node)
首先要清楚node节点其实就是head节点。
if (ws < 0) 即为SIGNAL,表明接下来要唤醒下一个节点了,那么head节点状态设为0。
if (s == null || s.waitStatus > 0) 说明下一个节点为CANCLED或者已经跑了,则从尾结点开始向前遍历,找到最前面的 waitStatus <= 0 的节点,用于唤醒。
最后找到则唤醒该节点。

private void unparkSuccessor(Node node) {
    int ws = node.waitStatus;
    if (ws < 0)
        compareAndSetWaitStatus(node, ws, 0);
    Node s = node.next;
    if (s == null || s.waitStatus > 0) {
        s = null;
        for (Node t = tail; t != null && t != node; t = t.prev)
            if (t.waitStatus <= 0)
                s = t;
    }
    if (s != null)
        LockSupport.unpark(s.thread);
}

OK我们捋完了release的逻辑,再回过头来看看为什么shouldParkAfterFailedAcquire()方法最后要返回false?
我们可以看到,在release() 方法中,释放锁的先决条件是 h.waitStatus != 0 ,那么什么时候它会等于0呢?要么是初始状态,要么是已经进入到 unparkSuccessor() 中通过CAS将值改为0。显然这里属于后者。

先回顾一下节点node的状态:
DEFAULT = 0,初始状态;啥也不知道。
CANCLED = 1,取消;不抢锁了。
SIGNAL = -1,释放锁时唤醒下一个节点。

假设我们有3个节点 node0(head)、node1和node2(tail)。

  • node0节点释放锁 —> 唤醒node1
  • node3来了,如果我们的锁恰好为非公平锁,并且node3抢到了锁,那么node1则会执行shouldParkAfterFailedAcquire() 方法期望再次中断。
  • node1在执行到compareAndSetWaitStatus(pred, ws, Node.SIGNAL) 前交出时间片,同时node3释放了锁,执行到if (h != null && h.waitStatus != 0) 时发现状态为0,不唤醒下一个节点。
  • node1再拿回时间片,将头节点状态设为-1,如果此时直接返回true,node1中断了,将再也没有节点尝试获取锁,也不会有任何节点被唤醒。
  • 而如果返回false,node1则还会在死循环中尝试获取锁(此时没有线程持有锁),这样我们整个流程才是闭环的。

3.5 排队检验 hasQueuedPredecessors()

这个方法一般被用在公平锁中来判断当前线程节点是否需要排队。
逻辑上很简单:

  • h != t ,头结点和尾结点不同。
  • (s = h.next) == null, 头结点的后继结点不为空。
  • s.thread != Thread.currentThread() , 头结点的后继结点所属线程不为当前线程。
public final boolean hasQueuedPredecessors() {
    Node t = tail; 
    Node h = head;
    Node s;
    return h != t &&
        ((s = h.next) == null || s.thread != Thread.currentThread());
}

接下来我们详细分析下这三个条件分别代表什么含义:
1、h != t 不成立即h == t,返回false,不需要排队。两种情况:

  • 队列未初始化时 h == t == null
  • 队列已经初始化了,但是只有一个节点,此时头节点和尾节点都指向它

2、h != t 成立
说明队列至少有两个节点或者同步队列正在初始化enq()但还未完成时。

private Node enq(final Node node) {
    for (;;) {
        Node t = tail;
        if (t == null) { // Must initialize
            if (compareAndSetHead(new Node()))
            
//----------如果刚执行到此处,此时 h == new Node(), t == null ---------

                tail = head;
        } else {
            node.prev = t;
            if (compareAndSetTail(t, node)) {
                t.next = node;
                return t;
            }
        }
    }
}

2.1、h != t 成立,但是 (s = h.next) == null || s.thread != Thread.currentThread() 不成立。
说明head节点的后继节点不为null,并且节点线程为当前线程,可以不用排队直接尝试获取锁。

2.2、h != t 成立,并且 (s = h.next) == null 也成立。两种情况:

  • 同上面的未初始化完成情况,已经有线程在入队只是还未完成,所以当前线程需要去排队。
  • 同步队列已经完成了初始化,但是由于释放锁等操作,导致此时同步队列只有一个头节点,head和tail都指向这个头节点。此时有一个线程执行入队操作了,这个线程刚执行完compareAndSetTail(pred,node)把tail指针指向了自己新建的节点,导致了h!= t成立,但是还没有执行完pred.next= node,导致head节点的next为nul,满足了(s=h.next)==nul成立,此时也是说明有线程在执行入队操作了,只是还没有入队完而已,所以当前线程还是需要排队。
private Node addWaiter(Node mode) {
    Node node = new Node(Thread.currentThread(), mode);
    Node pred = tail;
    if (pred != null) {
        node.prev = pred;
        if (compareAndSetTail(pred, node)) {
//----------如果刚执行到此处,此时 h == pred, t == node, 但 pred.next = node 未执行---------
            pred.next = node;
            return node;
        }
    }
    enq(node);
    return node;
}

2.3、h != t 成立,(s=h.next) == null不成立,但 s.thread != Thread.currentThread() 成立。
这属于是标准情况,已经有其他线程在你前面排队了,所以你也得老老实实去排队。

  • 28
    点赞
  • 46
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值