JUC--AQS设计

尝试设计

CAS能够原子的对一个值进行写操作,那么可以将这个值(称为status)作为竞争资源的标记位。在多个线程想要去修改共享资源时,先来读取status,看能不能获取到写status的权限。
拒绝其它线程的调用怎么设计呢?有两种业务场景:有的业务可能只是快速尝试获取一下共享资源,获取不到也没关系,会进行其它处理,有的业务线程一定要获取共享资源才能进行下一步处理,如果没有获取到,愿意等待。第一种场景直接返回共享资源当前的状态就可以了,第二种场景不适合自旋,我们设计一个队列让这些线程排队。队列头部的线程自旋访问status,其它线程挂起,避免了大量资源的消耗。当头部线程成功占用了共享资源,那么它在唤醒后续一个被挂起的线程,让它开始自旋的访问status。

AQS,一个抽象的(可被继承和复用),内部存在排队(竞争资源的线程排队)的同步器(对共享资源和线程进行同步管理)

JUC--AQS设计_第1张图片

AQS属性

    private volatile int state;

state就是之前说的判断共享资源是否被占用的标记位,volatile保证了线程之间的可见性,简单来说就是一个线程修改了state的值,其它线程下一次能读取到最新值。
为什么不是boolean而是int?
锁有独占和共享,共享模式下可能有多个线程正在共享资源,所以state需要表示线程占用数量,因此是int值。

 private transient volatile Node head;
 private transient volatile Node tail;

AQS用一个队列用于对等待的线程进行管理,这个队列通过一个FIFO的双向链表来实现。

内部类

 /** Marker to indicate a node is waiting in shared mode */
        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;
        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主要存储了线程对象(thread),节点在队列里的等待状态(waitStatus),前后指针等信息。重点关注waitStatus这个属性,它是一个枚举值,AQS工作时必然伴随Node的waitStatus值的变化。

Node中的方法也很简单,prodecessor就是获取前置Node。

内容非常简单,后面我们要重点关注的则是如何利用state和FIFO的队列来管理多线程的同步状态,这些操作被封装成了方法。

方法

一开始提供了两种使用场景:

  • 尝试获取锁,不管有没有获取到,立即返回。
  • 必须获取锁,如果当前时刻锁被占用,则进行等待。

AQS最上层应该会有这两个方法。

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

truAcquire是一个被protected修饰的方法,参数是一个int值,代表对int state的增加操作,返回boolean代表是否成功获得锁。
很明显,AQS规定继承类鄙视重写tryAcquire方法,否则就直接抛出。
为什么要上层自己实现?
因为尝试获取锁这个操作中可能包含某些业务自定义的逻辑,比如是否可重入等。

若上层调用tryacquire返回true,可对共享资源进行操作。返回false,如果不想等待,可以进行自己的处理;如果上层选择等待锁,那么直接调用acquire方法,acquire方法内部封装了复杂的排队处理逻辑。

下面来看acquire方法

if条件包含两部分:

  • 1 !tryAcquire(arg)
  • 2 acquireQueued(addWaiter(Node.EXCLUSIVE), arg)

1如果为false代表已经获取到锁,不需要排队。
假如tryAcquire返回false,说明需要排队,执行2判断,2里面嵌套了addWaiter方法。

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

顾名思义,方法的作用就是将当前线程封装成一个Node,然后假如等待队列,返回值即为该Node。逻辑非常简单,首先是新建一个Node对象,将其插入队尾。范式需要考虑多线程场景,假设存在多个线程正在同时调用addWaiter方法。

新建pred节点引用,指向当前的尾节点,如果尾节点不为空,那么下面将进行三步操作:

  1. 将当前节点的pre节点指向pred节点(尾节点)
  2. 尝试通过CAS操作将当前节点置为尾节点
  3. 如果返回false,说明pred节点已经不是尾节点,退出判断,调用enq方法,准备重新进入队列。如果返回true,

看一下enq方法

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

这个方法里的逻辑,在最外层加上一层死循环,如果队列未初始化(tail==null),那么就尝试初始化,如果尾插节点失败,呢么就不断重试,知道插入成功为止。

一旦addWaiter成功之后,那么就不能这么不管了,AQS在各个线程中为花了当前Node的waitStatus,根据不同的状态,程序来做出不同的操作。通过调用acquireQueued方法,开始对Node的waitStatus进行跟踪维护。

看acquireQueued源码。

final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;
        try {
            boolean interrupted = false;
            for (;;) {
            //7
                final Node p = node.predecessor();
                if (p == head && tryAcquire(arg)) {
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    return interrupted;
                    //11
                }
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

7-11 当前置节点为head,说明当前节点有权限去尝试拿锁,这是一种约定。如果tryAcquire返回true,代表拿到了锁,顺利撑场,函数返回。

13-15。if中包含两个方法,判断当前线程是否需要挂起等待。如果需要,那么就挂起,并判断外部是否调用线程中断。如果不需要,那么继续尝试拿锁。

18-19。如果try块中抛出非预期异常,那么取消当前线程获取锁的行为

有两点需要关注一下

  1. 一个约定:head节点代表当前正在持有锁的节点。若当前节点的前置节点是head,那么该节点就开始自旋的获取锁。一旦head节点释放,当前节点就能第一时间获取。
  2. interrupt变量最终被返回出去后,上层acquire方法判断该值,来选择是否调用当前线程中断,属于一种延迟中断机制。
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;
    }

若当前节点没有拿锁的权限或者拿锁失败,会进入方法看是否需要挂起(park),方法的参数是pred Node和当前Node的引用。

首先获取pred Node和waitStatus,若waitStatus为SIGNAL,说明前置节点也在等待拿锁,并且之后会唤醒当前节点。所以当前线程可以挂起休息,返回true。

如果shouldParkAfterFailedAcquire返回false,呢么再进行一轮重试。如果返回true,代表当前节点需要被挂起,则执行parkAndCheckInterrupt

	private final boolean parkAndCheckInterrupt() {
        LockSupport.park(this);
        return Thread.interrupted();
    }

是对当前线程进行挂起的操作。这里LockSupport.park(this);本质是通过UNSAFE下的native方法调用操作系统原语来讲当前线程挂起。

此时当前Node的线程将阻塞再此处,直到持有锁的线程调用release方法,release方法会唤醒后续节点、

通过对acquireQueued这个方法的分析,如果当前线程所在的系欸但那处于头节点的后一个,呢么它将不断去尝试拿锁,直到获取成功。否则需要挂起。这样就能保证head之后的一个节点在自旋CAS获取锁,其它线程都已经被挂起或正在挂起。这样就能够最大限度的避免无用的自旋消耗CPU。

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

和tryAcquire一样,tryRelease也是AQS开放给上层自由实现的抽象方法。

在release中,假如尝试释放锁成功,下一步就要唤醒等待队列里的其它节点,这里主要看unparkSuccessor这个方法,参数是head Node。

private void unparkSuccessor(Node node) {
        /*
         * If status is negative (i.e., possibly needing signal) try
         * to clear in anticipation of signalling.  It is OK if this
         * fails or if status is changed by waiting thread.
         */
        int ws = node.waitStatus;
        if (ws < 0)
            compareAndSetWaitStatus(node, ws, 0);

        /*
         * Thread to unpark is held in successor, which is normally
         * just the next node.  But if cancelled or apparently null,
         * traverse backwards from tail to find the actual
         * non-cancelled successor.
         */
        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);
    }

获取head的waitStatus,如果不为0,那么将其置为0,表示锁以释放。接下来获取后续节点,如果后续节点为null或处于CANCELED状态,那么从后往前搜索,找到除了head以外最靠前且非CANCELLED状态的Node,对其进行唤醒,让它起来尝试拿锁。

这时拿锁,挂起,释放,唤醒都能够有条不紊且高效的执行。

为什么不直接从头开始搜索?
和addWaiter方法中,前后两个节点建立连接的顺序有关。
1 后节点的pre指向前节点。
2 前节点的next才会指向后节点。
这两步操作在多线程环境下并不是原子的,也就是说,如果唤醒是从前往后搜索,那么可能前节点的next还未建立好,那么搜索可能会中断.
自此AQS告一段落。

你可能感兴趣的:(JUC,java,开发语言,java)