java并发编程AbstractQueuedSynchronizer原理和作用

作用

如图aqs会在很多地方用到,如之前介绍的CountDownLatch,信号量Semaphore,可重入锁ReentrantLock,线程池中的worker等,都用到了aqs,因此该数据结构在并发编程中十分重要
在这里插入图片描述
CountDownLatch,Semaphore,ReentrantLock都用内部声明了Sync类来继承aqs,实现逻辑按照不同的类实现不同的作用
在这里插入图片描述
关键词:让线程协作
在这里插入图片描述
aqs实现的作用如下:

  • 线程状态的原子性管理
  • 线程的阻塞与解除阻塞
  • 线程队列的管理

比喻:HR 和面试官
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

数据结构

AQS内部实现了两个队列,一个同步队列,一个条件队列。
在这里插入图片描述

  • 同步队列:当线程获取资源失败之后,就进入同步队列的尾部保持自旋等待,不断判断自己是否是链表的头节点,如果是头节点,就不断参试获取资源,获取成功后则退出同步队列。
  • 条件队列:为Lock实现的一个基础同步器,并且一个线程可能会有多个条件队列,只有在使用了Condition才会存在条件队列。

同步队列和条件队列都是由一个个Node组成的。AQS内部有一个静态内部类Node。

static final class Node {
            static final Node EXCLUSIVE = null;
            //当前节点由于超时或中断被取消
            static final int CANCELLED =  1;

            //表示当前节点的前节点被阻塞
            static final int SIGNAL    = -1;

            //当前节点在等待condition
            static final int CONDITION = -2;

            //状态需要向后传播
            static final int PROPAGATE = -3;

            volatile int waitStatus;

            volatile Node prev;
            volatile Node next;
            volatile Thread thread;

            Node nextWaiter;

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

关键是这几个
在这里插入图片描述

前后两个节点,节点上的thread类,还有nextWaiter节点和waitStatus状态
在这里插入图片描述
就是如图右下角那几个参数
在这里插入图片描述
ConditionObject则实现了条件队列,这个队列可以创建多个
在这里插入图片描述

重要方法的源码

获取独占锁

  //独占模式下获取资源
    public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }

这里的tryAcquire(arg)是抽象方法,有AQS的子类来实现,因为每个子类实现的锁是不一样的。一共有四个方法在AQS中没有具体实现:

  • tryAcquire(int arg):获取独占锁
  • tryRelease(int arg):释放独占锁
  • tryAcquireShared(int arg):获取共享锁
  • tryReleaseShared(int arg):释放共享锁

最重要的是acquireQueued(addWaiter(Node.EXCLUSIVE), arg),源码如下

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
        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;//打断循环
                }
            }
        }
    }
 final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;
        try {
            boolean interrupted = false;
            for (;;) {
                final Node p = node.predecessor();//拿到node的上一个节点
                //前置节点为head,说明可以尝试获取资源。排队成功后,尝试拿锁
                if (p == head && tryAcquire(arg)) {
                    setHead(node);//获取成功,更新head节点
                    p.next = null; // help GC
                    failed = false;
                    return interrupted;
                }
                //尝试拿锁失败后,根据条件进行park
                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)
            /*
             * This node has already set status asking a release
             * to signal it, so it can safely park.
             */
            return true;
        if (ws > 0) {
            /*
             * Predecessor was cancelled. Skip over predecessors and
             * indicate retry.
             */
            do {
            //如果前节点取消了,那就往前找到一个等待状态的接待你,并排在它的后面
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
            pred.next = node;
        } else {
            /*
             * waitStatus must be 0 or PROPAGATE.  Indicate that we
             * need a signal, but don't park yet.  Caller will need to
             * retry to make sure it cannot acquire before parking.
             */
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        return false;
    }
    //阻塞当前线程,返回中断状态
    private final boolean parkAndCheckInterrupt() {
        LockSupport.park(this);
        return Thread.interrupted();
    }

park
在AQS的实现中有一个出现了一个park的概念。park即LockSupport.park().它的作用是阻塞当前线程,并且可以调用LockSupport.unpark(Thread)去停止阻塞。它们的实质都是通过UnSafe类使用了CPU的原语。

在AQS中使用park的主要作用是,让排队的线程阻塞掉(停止其自旋,自旋会消耗CPU资源),并在需要的时候,可以方便的唤醒阻塞掉的线程。

入队

上面的代码可以看到,获取锁失败后,会先执行addWaiter方法加入队列,然后执行acquireQueued方法自旋地获取锁直到成功。

addWaiter代码逻辑如下图,简单说就是把node入队,入队后返回node参数给acquireQueued方法:

在这里插入图片描述
这里有一个点需要注意,如果队列为空,则新建一个Node作为队头。

入队后获取锁

acquireQueued自旋获取锁逻辑如下图:

在这里插入图片描述
这里有几个细节:

1.waitStatus

  • CANCELLED(1):当前节点取消获取锁。当等待超时或被中断(响应中断),会触发变更为此状态,进入该状态后节点状态不再变化。
  • SIGNAL(-1):后面节点等待当前节点唤醒。
  • CONDITION(-2):Condition中使用,当前线程阻塞在Condition,如果其他线程调用了Condition的signal方法,这个结点将从等待队列转移到同步队列队尾,等待获取同步锁。
  • PROPAGATE(-3):共享模式,前置节点唤醒后面节点后,唤醒操作无条件传播下去。
  • 0:中间状态,当前节点后面的节点已经唤醒,但是当前节点线程还没有执行完成。
    在这里插入图片描述

2.获取锁失败后挂起

如果前置节点不是头节点,或者前置节点是头节点但当前节点获取锁失败,这时当前节点需要挂起,分三种情况,

前置节点waitStatus=-1,如下图:
在这里插入图片描述
前置节点waitStatus > 0,如下图:
在这里插入图片描述
前置节点waitStatus < 0 但不等于 -1,如下图:
在这里插入图片描述
3.取消获取锁

如果获取锁抛出异常,则取消获取锁,如果当前节点是tail节点,分两种情况如下图:
在这里插入图片描述

如果当前节点不是tail节点,也分两种情况,如下图:
在这里插入图片描述

4.对中断状态忽略

5.如果前置节点的状态是 0 或 PROPAGATE,会被当前节点自旋过程中更新成-1,以便之后通知当前节点。

参考

AQS 的作用和重要性是什么?
https://blog.csdn.net/vincent_wen0766/article/details/108718349
AQS 模型(源码内的其他方法可以看这一篇)
https://www.yisu.com/zixun/457731.html
AQS 数据结构和原理(获取独占锁自定义公平和非公平锁)
https://www.cnblogs.com/zofun/p/12206759.html
AQS 同步队列和条件队列
https://www.cnblogs.com/liqiangchn/p/12081838.html

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值