AQS - 抽象同步队列:独占锁的实现

参考链接:https://www.bilibili.com/video/BV12K411G7Fg

通过 CAS ,我们可以实现乐观锁操作,从而使得线程进行同步,但是通过 CAS 的源码,我们发现 CAS 仅仅能修改内存中的一个值,而不是对对象进行同步,那么该如何对对象进行同步呢?同时,在多线程对统一资源进行竞争的情况下,如何能管理到所有需要该资源的线程呢?于是,AQS应运而生。

参考:《深入 Java 虚拟机》

AbstractQueuedSynchronizer抽象同步队列简称AQS,它是实现同步器的基础组件,并发包中锁的底层就是通过AQS实现。结构如下

属性

int state

在共享模式下,需要表示共享锁的持有线程数量。

共享锁 和 独占锁(排他锁)

共享锁:该锁允许被多个线程持有,共享锁仅支持读数据,如果一个线程对数据加了共享锁后,其他数据只能对该数据加共享锁。

独占锁(排他锁):只有一个线程能获得锁。

共享锁 和 独占锁是 AQS 的不同实现方式

Node head & Node tail

用于维护一个 FIFO 的双向链表,两个 Node 节点分别指向头节点和尾节点

Node

队列中的节点,结构见上图

方法(以独占模式为例)

tryAcquire(int arg)

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

尝试获取锁,获取锁失败直接返回。

该方法仅仅抛出一个异常,AQS 继承类需要继承该方法,用于给上层开放空间,使用户能编写业务逻辑。

acquire(int arg)

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

tryAcquire方法失败后,会进入等待队列

addWaiter(Node.EXCLUSIVE), arg)

主要作用为新建一个 Node 节点,并将节点插入等待队列。

private Node addWaiter(Node mode) {
        Node node = new Node(Thread.currentThread(), mode);
    	// 获取当前尾节点,tail 是 AQS 的属性
        Node pred = tail;
        if (pred != null) {
            node.prev = pred;
            // 尝试通过原子操作将当前节点置为尾节点
            // 其实获取 pred 后,其他线程也可能会对 tail 进行修改
            // compareAndSetTail(Node expect, Node update) 会读取 tail 的偏移
            // 判断当前的 pred 是不是还是队尾(期间可能被其他线程修改),若是,则更新队尾为当前 node
            if (compareAndSetTail(pred, node)) {
                pred.next = node;
                return node;
            }
        }
    
    	// 调用完整的入队方法,上面尝试快速入队失败时会进入该方法
    	// 例如 tail 被修改的情况
        enq(node);
        return node;
}

acquireQueued(final Node node, int arg)

加入队列后,在队列中自旋对锁进行获取。

经过代码可以看出,head 节点后的节点组成了等待队列

当 head 后第一个 node 获得锁时,node 会成为新的头节点

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

boolean tryRelease(int arg)

tryAcquire ,作为开放给上层的方法

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

boolean release(int arg)

释放锁,并通知队列,改变等待队列中的线程状态

public final boolean release(int arg) {
        if (tryRelease(arg)) {
            Node h = head;
            if (h != null && h.waitStatus != 0)
            	// 传入 head 并唤醒等待队列中的 node
                unparkSuccessor(h);
            return true;
        }
        return false;
}

unparkSuccessor(Node node)

头节点操作完资源后,通知等待队列中的节点

下方代码的操作中,为什么唤醒不从头节点开始呢

该处搜索并不是原子性的,从后往前搜索,可能会因为队列构建顺序未

  1. 后节点 pre 指向前节点
  2. 前节点 next 才会指向后节点

从前往后可能会因第2步还未完成而造成搜索中断

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;
            // 从尾节点开始搜索,head 后最靠前的节点并且 waitStatus <= 0 的节点
            for (Node t = tail; t != null && t != node; t = t.prev)
                if (t.waitStatus <= 0)
                    s = t;
        }
    	// 对找到的节点进行唤醒操作,唤醒后会自旋执行 acquire 方法获取锁
        if (s != null)
            LockSupport.unpark(s.thread);
}

共享模式

共享模式下,锁可以被多个线程获取,表现为 state 值的增加。

线程使用锁操作完成后,对锁进行释放,同时 state 减少。

锁的获取

使用锁资源的锁释放后

  • 独占模式:仅会唤醒最靠前的节点
  • 共享模式:唤醒队列所有处于共享模式下挂起状态的节点
posted @ 2021-10-29 17:24  Dozeer  阅读(76)  评论(0编辑  收藏  举报
Live2D