JUC系列之AQS

1、AQS基本概念

AQS(AbstractQueuedSynchronizer) 抽象队列同步器,是Java并发包的核心基础组件,ReentrantLock、ReentrantReadWriteLock以及JUC同步四大神器(CountDownLatch、CyclicBarrier、Exchanger、Semaphore)等的实现都依赖于AQS。AQS是一种提供了原子式管理同步状态、阻塞和唤醒线程功能以及队列模型的简单框架。后面围绕AQS结构以及独占锁、共享锁的获取和释放逐步进入了解AQS。在这里插入图片描述

2、AQS结构

1.Node结构

Node是AQS的一个静态内部类,Node是CLH队列中的节点元素。

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;
        /**
         * 当前线程处于SHARED情况下,该字段才会使用
         */
        static final int PROPAGATE = -3;

        /**
         * 当前节点在队列中的状态,初始化时默认为0
         */
        volatile int waitStatus;

        /**
         * 前驱节点
         */
        volatile Node prev;

        /**
         * 后继节点
         */
        volatile Node next;

        /**
         * 处于该节点中的线程
         */
        volatile Thread thread;

        /**
         * 指向下一个处于Condition状态的节点
         */
        Node nextWaiter;

        /**
         * Returns true if node is waiting in shared mode.
         */
        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
        }

        Node(Thread thread, Node mode) {     // Used by addWaiter
            this.nextWaiter = mode;
            this.thread = thread;
        }

        Node(Thread thread, int waitStatus) { // Used by Condition
            this.waitStatus = waitStatus;
            this.thread = thread;
        }
    }

Node属性

Node主要属性有waitStatus、prev、next、thread、nextWaiter。

waitStatus:节点在队列中的状态,初始化时默认为0;
CANCELLED:1,当该节点的线程等待超时或者中断,需要从同步队列中取消。
SIGNAL:-1,后继节点的线程处于等待状态,如果当前节点释放同步状态或被取消会通知后继节点,使得后继节点的线程能够运行
CONDITION: -2,表示节点在等待队列中,节点线程等待唤醒,当其它线程对Condition调用了signal()方法后,该节点从等待队列转移到同步队列。
PROPAGATE:-3当前线程处于SHARED情况下,该字段才会使用。

prev和next用来表示当前节点的前驱和后继节点;thread表示当前节点的线程;

nextWaiter:指向下一个处于Condition状态的节点。

2.同步状态State

    /**
     * The synchronization state.
     */
    private volatile int state;

State字段是AQS的核心属性,由volatile关键字修饰,用来表示临界资源获取锁的情况。

3.CLH队列

CLH队列是一个单向链表,AQS采用的CLH变体的虚拟双向队列(FIFO),AQS通过将每条请求共享资源的线程封装成一个Node节点来实现锁的分配。CLH锁出列只设置更新头部节点,插入队列只需要原子更新尾部的节点。
在这里插入图片描述

入队

当尝试获取锁失败时,会调用addWaiter方法将节点加入到CLH同步队列中去。

    /**
     * 为当前线程创建节点并加入到队列中
     */
    private Node addWaiter(Node mode) {
        Node node = new Node(Thread.currentThread(), mode);
        // 快速入队,如果入队失败则调用enq入队
        Node pred = tail;
        if (pred != null) {
        	// 当前尾部节点设置为node的前驱节点
            node.prev = pred;
            // CAS 更新尾部节点
            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;
                }
            }
        }
    }

出队

acquireQueued会把放入队列中的线程不断去获取锁,直到获取成功或者不再需要获取(中断)。

    final boolean acquireQueued(final Node node, int arg) {
    	// 标记是否成功拿到资源
        boolean failed = true;
        try {
        	// 标记是否中断
            boolean interrupted = false;
            // 自旋直到获取锁或者中断
            for (;;) {
            	// 获取当前节点的前驱节点
                final Node p = node.predecessor();
                // 如果p是头节点,则当前节点是真实数据队列的第一个节点(头节点是虚节点),则尝试获取锁
                if (p == head && tryAcquire(arg)) {
                	// 获取锁成功,则将当前节点设为头节点
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    return interrupted;
                }
                // 如果p为头节点且当前节点没有获取到锁或者p不是头节点,需要判断当前节点是不是需要被阻塞,避免无限循环浪费资源
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

3、独占锁模式

1.独占锁的获取

ReentrantLock是一种基于AQS实现的独占锁,下面结合ReentrantLock锁的获取和释放来分析独占锁的获取。ReentrantLock调用lock方法时最终会调用AQS的acquire方法。

	// 独占式获取锁
    public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }
  1. tryAcquire:这个方法需要子类去实现实际获取锁的逻辑,尝试获取锁,如果获取成功,则不再进入同步队列;
  2. addWaiter:尝试获取锁失败后则执行addWaiter方法将当前线程封装成一个Node节点加入到CLH队列尾部;
  3. acquireQueued:线程在队列中自旋等待获取锁,直到获取到锁或者中断;如果当前线程在等待过程被中断则返回true,否则返回false。
  4. selfInterrupt:产生一个中断。

addWaiter、acquireQueued代码已经在上面分析过。

2.独占锁的释放

ReentrantLock释放锁:

    public void unlock() {
        sync.release(1);
    }

调用AQS的release方法释放锁:

    public final boolean release(int arg) {
        if (tryRelease(arg)) {
            Node h = head;  //获取头节点
            // 如果头节点不为空,且头节点的状态不是初始化状态则解除后继线程的挂起状态
            if (h != null && h.waitStatus != 0)
                unparkSuccessor(h);
            return true;
        }
        return false;
    }

ReentrantLock实现tryRelease方法:

        protected final boolean tryRelease(int releases) {
            int c = getState() - releases;
            // 如果当前线程没有持有锁,抛出异常
            if (Thread.currentThread() != getExclusiveOwnerThread())
                throw new IllegalMonitorStateException();
            boolean free = false;
            // 持有线程全部释放,将当前独占锁的线程设置为null,然后更新state
            if (c == 0) {
                free = true;
                setExclusiveOwnerThread(null);
            }
            setState(c);
            return free;
        }

unparkSuccessor方法:将头节点的后继节点唤醒,如果后继节点不符合唤醒条件,则从队尾一直往前遍历直到找到符合条件(非Cancelled状态)的节点为止,

    private void unparkSuccessor(Node node) {
        /*
         * 获取头节点waitStatus
         */
        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);
    }
  1. tryRelease:尝试释放锁,具体实现由子类实现,释放成功直接返回false;
  2. unparkSuccessor:释放锁成功,则调用unparkSuccessor唤醒后继节点。

AQS唤醒节点时为什么要从后往前找第一个非Cancelled的节点

  • 使用节点入队的时候1、2操作可以保证原子性,2、3操作并没有保证原子性,某一时刻存在node的prev指针存在而next为null的情况,因此prev指针是可靠的而next指针并不可靠。如果执行完2而3未执行时恰好又执行了unparkSuccessor方法,从前往后遍历无法遍历完全部的Node。
  • CANCELLED节点产生过程中断开Next指针的操作也可能导致无法遍历所有的节点。

在这里插入图片描述
为什么AQS保证前驱节点的原子性,而不是保证后继节点的原子性

  • next域:需要修改两个地方来保证原子性,一个是tail.next;二是tail指针。需要两次原子操作
  • prev域:只需要修改一处来保证原子性,tail指针。只需要一次原子操作
    在这里插入图片描述
    prev只需要一次原子操作是因为node.prev指针并不存在锁竞争的问题,不会有多个线程同时设置node.prev,相反tail.next指针可能会有多个线程竞争。

4、共享锁模式

独占锁和共享锁的最大区别在于独占锁同一时刻只能有一个线程持有锁,而共享锁同一时刻可以有多个线程持有锁。

1.共享锁的获取

    public final void acquireShared(int arg) {
        if (tryAcquireShared(arg) < 0)
            doAcquireShared(arg);
    }

流程如下

  1. 调用tryAcquireShared方法尝试获取共享锁,tryAcquireShared方法同样由子类去实现,成功则直接返回。
  2. 获取失败则调用doAcquireShared进入同步队列中自旋获取锁。

tryAcquireShared()方法返回值意义
当返回值大于0时,表示获取同步状态成功,其它线程仍可以继续获得同步状态;
当返回值等于0时,表示获取同步状态成功,但是没有可用剩余同步状态;
当返回值小于0时,表示同步获取失败。

private void doAcquireShared(int arg) {
		// 节点加入到队列尾部
        final Node node = addWaiter(Node.SHARED);
        boolean failed = true;
        try {
            boolean interrupted = false;
            // 自旋获取锁
            for (;;) {
                final Node p = node.predecessor();
                // 如果p是头节点
                if (p == head) {
                    int r = tryAcquireShared(arg);
                    // 当前节点的前驱节点是头节点且成功获取同步状态
                    if (r >= 0) {
                    	// 将当前节点设置为头节点,如果还有可用资源则唤醒后继节点
                        setHeadAndPropagate(node, r);
                        p.next = null; // help GC
                        if (interrupted)
                            selfInterrupt();
                        failed = false;
                        return;
                    }
                }
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

2.共享锁的释放

释放同步状态:

    public final boolean releaseShared(int arg) {
        if (tryReleaseShared(arg)) {
            doReleaseShared();
            return true;
        }
        return false;
    }
    private void doReleaseShared() {
        /*
         * Ensure that a release propagates, even if there are other
         * in-progress acquires/releases.  This proceeds in the usual
         * way of trying to unparkSuccessor of head if it needs
         * signal. But if it does not, status is set to PROPAGATE to
         * ensure that upon release, propagation continues.
         * Additionally, we must loop in case a new node is added
         * while we are doing this. Also, unlike other uses of
         * unparkSuccessor, we need to know if CAS to reset status
         * fails, if so rechecking.
         */
        for (;;) {
            Node h = head;
            if (h != null && h != tail) {
                int ws = h.waitStatus;
                if (ws == Node.SIGNAL) {
                	// 共享模式持有同步状态的线程可能有多个,所以使用Cas保证线程安全
                    if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                        continue;            // loop to recheck cases
                    // 唤醒后继节点
                    unparkSuccessor(h);
                }
                else if (ws == 0 &&
                         !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                    continue;                // loop on failed CAS
            }
            if (h == head)                   // loop if head changed
                break;
        }
    }

流程如下

  1. 调用tryReleaseShared方法尝试释放同步状态,具体实现由子类完成,释放失败直接返回false
  2. 释放同步状态成功则调用doReleaseShared方法唤醒后继节点。

5、Condition

并发编程中需要解决两个问题:互斥和同步。互斥即同一时刻只允许一个线程访问资源,同步即线程之间的等待—通知(wait-notify) 机制。Lock解决了互斥问题,同步问题则使用了Condition来解决

1.等待队列

Condition等待队列的实现要比CLH中的同步队列实现更加简单,这是因为等待队列的操作都是在获取锁的线程中进行,所以不需要对线程安全做一些特殊处理。

2.await方法

public final void await() throws InterruptedException {
    if (Thread.interrupted())
        throw new InterruptedException();
    // 节点加入到等待队列中
    Node node = addConditionWaiter();
    // 释放锁
    int savedState = fullyRelease(node);
    int interruptMode = 0;
    // 判断节点是不是在同步队列中
    while (!isOnSyncQueue(node)) {
    	// 挂起当前线程
        LockSupport.park(this);
        if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
            break;
    }
    if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
        interruptMode = REINTERRUPT;
    if (node.nextWaiter != null) // clean up if cancelled
        unlinkCancelledWaiters();
    if (interruptMode != 0)
        reportInterruptAfterWait(interruptMode);
}

3.signal方法

加入到条件等待队列中的线程如何再重新获取锁,需要依赖signal和signalAll的通知机制。

    public final void signal() {
        if (!isHeldExclusively())
            throw new IllegalMonitorStateException();
        Node first = firstWaiter;
        if (first != null)
            doSignal(first);
    }
    // 唤醒条件等待队列中的第一个节点将其加入到同步队列中
    private void doSignal(Node first) {
        do {
            if ( (firstWaiter = first.nextWaiter) == null)
                lastWaiter = null;
            first.nextWaiter = null;
        } while (!transferForSignal(first) &&
                 (first = firstWaiter) != null);
    }

参考链接:

美团技术团队AQS分享
锁原理
aqs原理

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值