Java的AQS原理以及等待唤醒机制步骤

原理

当抢锁失败时,第一个进入AQS队列的线程节点,会进行初始化,创建一个虚拟头节点,同时将头和尾指针同时指向这个头节点,当后续线程节点进入时,都会连接在头节点之后,形成一个双向链表,其中头指针指向头节点,尾指针指向队列最后一个插入的线程节点。

步骤

当A、B、C三个线程争抢锁时,Java的AQS(AbstractQueuedSynchronizer,抽象队列同步器)内部的实现步骤如下(以非公平锁为例):

1. A线程抢到锁

A线程首先尝试获取锁,调用非公平锁的 lock方法进行加锁

        final void lock() {
	//尝试加锁,如果没成功,则调用acquire进行后续抢锁
            if (compareAndSetState(0, 1))
                setExclusiveOwnerThread(Thread.currentThread());
            else
                acquire(1);
        }

2. B、C线程试图抢锁

B,C进来也是先调用 lock方法,进行加锁操作,但是由于A已经加锁,此时会调用acquire方法

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

//tryAcquire底层调用此方法
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) // overflow
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
	//否则表示抢占失败
            return false;
        }

3. 抢锁失败,B、C线程进入队列,看是否挂起还是让线程就绪

当B、C线程尝试获取锁时,由于A线程已经持有锁,所以tryAcquire方法会失败。B、C线程在获取锁失败后,会调用addWaiter方法,将自己包装成一个Node节点,并加入到AQS的FIFO队列中。

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)) {
	//将之前的最后一个节点的next指向当前线程节点,形成双向链表
            pred.next = node;
	//返回当前线程节点
            return node;
        }
    }
//如果当前队列没有任何节点,则初始化,并放入当前线程节点,具体逻辑看最前面
    enq(node);
    return node;
}

4. B、C线程等待唤醒

B、C线程在加入到队列后,会调用acquireQueued方法,此时线程会被阻塞,并等待唤醒。AQS使用LockSupport.park方法来阻塞线程。当一个线程获取锁失败时,它会被加入到AQS队列的末尾,并且调用LockSupport.park方法来阻塞自己。

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

5. A线程释放锁,B、C线程被唤醒

当A线程执行完毕,会调用release方法释放锁,并唤醒队列中的下一个节点对应的线程。AQS会调用LockSupport.unpark方法来唤醒队列中的第一个节点对应的线程。

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