并发之aqs

1、什么是aqs呢?
aqs是java并发包中的队列同步器,他是构建锁的基本框架,比如ReentrantLock就是基于其实现的。
2、为什么使用aqs?
java 5.0之前的synchronized的没有做过锁优化的,其效率较低,后来就出来ReentrantLock显示锁采用aqs的方式进行了优化,当然java 6的时候synchronized关键字做了优化了,下面看一张图:
该图来源于java并发编程实战一书
当然ReentrantLock还有其他优势是synchronized所没有的,比如中断阻塞等,这个以后会写一篇博客专门讲ReentrantLock,以下aqs的实现大部分是基于ReentrantLock来说的!
3、aqs原理
3.1、同步状态state
该状态默认为0表示没有线程获取锁,一旦线程调用(ReentrantLock)的lock方法,这时候state将被+1,该状态是被线程共享的,所以用cas来保证状态的同步,ReentrantLock是可重入的,所以每次获取到锁时state都会+1,每次释放锁时state-1,直至0表示没有锁,其他线程才能真正获取锁!
3.2、同步队列
上面说了state表示是否已经有线程获取锁,但是线程怎么获取锁,以及释放锁呢?这就是同步队列的重要性!aqs中维护了一个双端同步队列(基于双向链表实现),其基本结构如下图所示:

aqs中维护了两个节点,head和tail,head节点表示获取锁的节点,tail节点一般指向的是尝试获取锁的线程节点,插入的时候用到的主要是tail节点,释放锁的时候主要用到的是head节点。下面是插入和删除的图:(后面根据代码分析图):
获取锁失败时,插入节点到同步队列
释放锁
4、代码流程(基于ReentrantLock的非公平锁)
4、1获取锁。

// An highlighted block
final void lock() {
            if (compareAndSetState(0, 1))
                setExclusiveOwnerThread(Thread.currentThread());
            else
                acquire(1);
        }

上面代码很简单,compareAndSetState操作将state从0变为1,这里使用cas是可能会有多个线程来竞争锁,直接赋值为1是不安全的,cas操作成功的线程才是真正获取到锁的,如果cas操作失败则调用acquire(1)方法,acquire(1)方法中的参数为什么是1呢?因为每次有线程获取到锁state就+1的原因。compareAndSetState操作失败的原因可能有两种,一是sate被其他线程操作了,表示其他线程抢先获取到了锁,二是该线程之前已经获取到了锁,已经重入了。
下面是acquire方法`。

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

首先是tryAcquire方法(该方法是个模板方法,由具体子类实现的),ReentrantLock是这样实现的:

// An highlighted block
protected final boolean tryAcquire(int acquires) {
            return nonfairTryAcquire(acquires);
        }
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;
        }

分析上述代码:当compareAndSetState失败时要么是锁被别人抢占,要么自己重入,无论如何线程将再次尝试获取锁(此时可能锁被释放,线程获取锁的过程是一直进行的,直到阻塞或者成功,这个后面的acquireQueue方法具体解释)。1、如果此时state是0,表示刚才的cas失败是因为其他线程抢占锁,此时释放了锁才成为0的,这时候再次cas尝试获取锁将state变为1,成功则返回。2、如果state不为0而getExclusiveOwnerThread方法就是当前线程则表示重入,此时直接让state+1就行了,注意这里设置state不需要cas了,因为当前是同一个线程,其他线程虽然竞争state但是无法成功操作state,也就是说冲入的时候相当于单线程,返回成功。(如果这里成功退出了,其实相当于实现了偏向锁,偏向锁中的“偏”,就是偏心的“偏”、偏袒的“偏”。它的意思是这个锁会偏向于第一个获得它的线程,如果在接下来的执行过程中,该锁一直没有被其他的线程获取,则持有偏向锁的线程将永远不需
要再进行同步,不需要cas了)3、否则就是获取失败。获取失败的线程将调用addWaiter方法将自己加入到队列中。
下面展示一些 内联代码片

// An highlighted block
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;
    }
     /**
     * Inserts node into queue, initializing if necessary. See picture above.
     * @param node the node to insert
     * @return node's predecessor
     */
    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;
                }
            }
        }
    }

addWaiter方法首先将线程封装到node中,然后判断是否有尾节点如果有则通过cas将该节点插入,并将该节点设置为新的尾节点。如果尾节点为空(为什么为空呢?因为只有当有线程竞争的时候才会初始化一个尾节点同时也是头节点,compareAndSetHead(new Node())就是这个代码,代表之前获得锁的节点)或者cas失败(其他线程抢先添加成尾节点)则调用enq方法,初始化头节点和尾节点,并把当前节点插入成为新的尾节点,此时,队列中就有两个节点,头节点代表已经获取锁的节点,尾节点正在竞争锁的节点。这里有必要说明一个点,无论头节点还是尾节点初始的节点中的waitStatus为0,关于这点有如下解释:
下面是Node类中的一些信息。

// An highlighted block
 /** waitStatus value to indicate thread has cancelled */
        static final int CANCELLED =  1;
        /** waitStatus value to indicate successor's thread needs unparking */
        static final int SIGNAL    = -1;
        /** waitStatus value to indicate thread is waiting on condition */
        static final int CONDITION = -2;
        /**
         * waitStatus value to indicate the next acquireShared should
         * unconditionally propagate
         */
        static final int PROPAGATE = -3;
        volatile int waitStatus;

这里我们只要了解SINGAL就行了,SINGLE表示后继节点正在阻塞,需要唤醒,初始0表示不需要唤醒。
回到上面队列中有两个节点的情况,waitStatus都是初始0,因为尾节点加入到队列中还未阻塞,所以都是0。这时候调用acquireQueued方法,继续获取锁(获取锁的过程是不断的,线程不停的竞争,直到阻塞或者成功获取!)
下面展示一些 内联代码片

// An highlighted block
 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);
        }
    }

这段代码相当于自旋,不断尝试获取锁,而获取锁的前提是当前节点的前驱节点是头节点!也就是第二个节点。也就是说每个线程刚进来的时候是有一次机会获取锁的(前面的tryAcquire方法,这就是所谓的非公平锁),如果没有获取到锁,随后在acquireQueued方法中不断尝试获取,直至成功或者阻塞!不过获取锁的前提不仅仅是第二个节点,因为可能阻塞!上面代码中尝试获取锁失败,或者前驱节点不是头节点,都会走shouldParkAfterFailedAcquire方法。
下面展示一些 内联代码片

// An highlighted block
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;
    }

上述方法解析:如果前驱节点Node的waitStatus为SINGAL就表示该节点可以阻塞(前面说了SINGAL的头驱节点才会唤醒后继节点,所以阻塞的条件必须是前驱节点为SINGAL),我们知道默认Node的waitStatus是0,如果前驱节点是0则利用cas将其变为SINGAL,表示后继节点即将或者已经阻塞。那什么时候才能获取到锁呢?SINGAL的头节点(获取锁的节点),释放锁的时候!
以上就是获取锁的大致流程,值得注意的地方是:1、获取锁的时候会插入到尾节点中,这个过程必须是cas保证,因为可能多个线程获取锁失败要插入
2、获取锁失败的线程会不断尝试获取锁,直到成功,或者阻塞。而阻塞之前必须将前驱节点置为SINGAL,才会在前驱节点释放锁的时候唤醒。3、获取锁的过程是不公平的,每个线程调用lock方法的时候都会尝试一次获取锁,这个时候该线程可能并非在队列中,第一次获取锁失败的线程不断尝试获取锁,前驱节点是头节点的时候才有资格去尝试获取锁,否则可能阻塞。4、如第一点所说获取锁的过程是并发的,但是释放锁只能是一个线程去做的,因为只有该线程获得了锁,所以获取到锁后setHead操作是不用cas保证的。
4、2 释放锁
释放锁的过程很简单,代码如下
下面展示一些 内联代码片

// An highlighted block
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 final boolean tryRelease(int releases) {
            int c = getState() - releases;
            if (Thread.currentThread() != getExclusiveOwnerThread())
                throw new IllegalMonitorStateException();
            boolean free = false;
            if (c == 0) {
                free = true;
                setExclusiveOwnerThread(null);
            }
            setState(c);
            return free;
        }
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);
    }

首先是tryRelease,将state-1这个过程不需要cas因为只有一个线程释放锁,不存在多个线程释放锁。这和前面的setHead一样,因为只有一个线程在获取到锁后,将新的头节点插入。是否要cas操作最好的判别方法是,会不会有多个线程进行该操作。其次释放锁后调用unparkSuccessor方法去唤醒阻塞的线程,调用它的前提是头节点不为null(前面也说过头节点为null只会出现在偏向锁的时候,也就是只有一个线程获取锁的时候),而且waitStatus不为0(所以线程阻塞前要置为-1),unparkSuccessor方法才被调用,先将头节点置为0,清楚后继阻塞标志,如果头节点的后继节点为null,直接结束方法,里层的for循环不会调用,如果头节点的后继节点不为null但是后继节点的waitStatus>0则表示该节点取消,在for循环中从后往前查找一个正常节点,将其释放。

  • 4
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值