java8 concurrent之AQS源码解析

前言

  1. 文章内容均以java 8_u151版本的代码为准。
  2. 文章内容绝大部分都是个人见解,如有错误的地方,欢迎大家指正。

概述

  1. AbstractQueuedSychronizer(AQS)是一个concurrent的framework,AQS从java5+支持,是一个抽象类,继承了AbstractOwnableSychronizer(后面统称为AOS),但AOS却是从java6才添加的,也是个抽象类。AOS类的作用很简单,源码中只包含一个成员变量和对应成员变量的get set方法,作用就是记录Thread对象,主要用于排它锁模式中,用于记录获取到AQS state(也就是锁资源)的线程对象。
  2. AQS有一个abstract subclass,AbstractQueuedLongSychronizer(AQLS),这个也是从java6才添加,与AQS的区别是,AQLS的state字段是long类型的,而AQS是int类型。
  3. AQS框架的内部维护着一个int类型的state变量,这个变量对于共享锁(semaphore为例)和排它锁(ReentrantLock为例)来说,具有不同的意义。对于共享锁,state表示共享许可证的数量(也可以参考令牌桶,对应令牌的数量),每个线程需要获取N(N<=state)个许可证才能运行,每次获取到许可后,state的数量都要相应减少,当线程所需的许可证数量大于state剩余数量时,就会进入阻塞状态;对于排它锁,state表示当前锁资源被分配的次数,初始值是0,当线程A每次执行lock()方法时,state++,当线程B执行lock()方法时,因为state非0,且state的所属者为ThreadA,就会进入阻塞模式。
  4. 对于AQS state的修改,都是通过AQS内部的线程安全方法实现。而这些方法都是final修饰的,因此不允许外部重写。
  5. 在使用AQS时,JDK官方要求要以helper class的方式提供服务,即AQS的实现类要是某个class的内部类,且不能用public修饰。
  6. AQS分为exclusive mode和share mode,当处于排它锁模式时,只能有一个线程能够acquire state;而当处于共享锁模式时,允许多个线程同时持有state(但也可以不允许多个线程持有)。
  7. AQS的序列化只保留了state的信息,而对于queue中的信息不会序列化,因此如果需要序列化,需要重写readObject方法
  8. 要使用AQS,需要重写一些方法。share mode:tryAcquireShared(int);tryReleaseShared(int)。exclusive mode:tryAcquire(int);tryRelease(int);isHeldExclusively()。通常情况下,一个AQS的实现只会实现其中一个mode,因此不需要实现的mode锁需要的方法不需要重写。

Exclusive Mode解析

下面以ReentrantLock为例,讲解AQS exclusive mode的工作流程。

ReentrantLock分为公平锁(Fair Lock)和非公平锁(Non Fair Lock),通过构造方法来指定使用的锁类型。默认的构造函数创建的是Non Fair Lock。

public ReentrantLock(boolean fair) {
    sync = fair ? new FairSync() : new NonfairSync();
}

NonFair Lock

static final class NonfairSync extends Sync {
    private static final long serialVersionUID = 7316153563782823691L;

    /**
     * Performs lock.  Try immediate barge, backing up to normal
     * acquire on failure.
     */
    final void lock() {
        if (compareAndSetState(0, 1))
            setExclusiveOwnerThread(Thread.currentThread());
        else
            acquire(1);
    }

    protected final boolean tryAcquire(int acquires) {
        return nonfairTryAcquire(acquires);
    }
}

首先看一下非公平锁的AQS实现,使用non-pulic static inner class,继承自abstract Sync class,这个Sync继承了AbstractQueuedSychroinzer,且二者均使用无参构造函数。

因此,在创建ReentrantLock时,首先AQS的state对象值为0这里与共享锁不一样,共享锁的state初始化时设置了一个给定的值,每次通过acquire获取到permit时,都会减一)。

当调用reentrantLock.lock()获取锁时,实际出发的是NonFairSync.lock()方法,Non Fair Sync的lock方法逻辑如上所示,使用CAS尝试将AQS.state由0修改为1,如果修改成功,就认为lock()执行成功,此时调用AOS(AbstarctOwnableSychronizer).setExclusiveOwnerThread方法,记录AQS.state的持有线程。如果CAS修改失败(被其他线程抢占锁资源),那么进入acquire(1)的逻辑。

acquire(1)方法触发的是AQS的内部final method,源码如下:

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

首先注意:acquire是final修饰的,因此无法被继承重写。

在acquire方法中,首先执行tryAcquire(1)第二次尝试获取AQS.state资源,如果返回true,表示获取成功;如果返回false,则currentThread需要进入AQS的阻塞队列中(CLH队列)。

tryAcquire()方法在AQS内部并没有任何实现,因此属于需要开发人员重写的方法,该方法的重写实现在第一个代码片段中,其内部最终调用nonfairTryAcquire(1)方法,下面看一下这个方法的实现:

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

在第二次尝试获取锁资源的过程中,首先第一个if中的逻辑与lock()方法一样,要求AQS.state必须是0;第二个if判断属于锁的重入操作,从这里可以看出,ReentrantLock的重入,会通过AQS来记录锁重入的次数,但是同一个ReentrantLock的可重入次数只能是2^31-2次(第一次lock不认为是重入)。

现在假设,currentThread=Thread1,而目前AQS.state资源被Thread0占用,因此tryAcquire(1)方法返回false,会继续触发addWaiter()方法和acquireQueued()方法。

addWaiter()方法负责将Thread1封装在一个Node对象中,然后将Thread1 Node插入AQS FIFO queue队列的tail,源码如下:

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;
}
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的代码逻辑分为两块:AQS FIFO queue已经构建,此时只需要将Thread1 Node节点通过CAS的方式插入到queue tail即可;AQS FIFO queue没有构建,此时需要先初始化queue的head和tail节点,然后将Thread1 Node以CAS的方式插入queue。有关AQS FIFO queue的相关介绍,在文章最后。

在Thread1 enqueue到AQS的队列后,就会触发acquireQueued()方法进行第三次尝试获取AQS.state资源,源码如下:

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

acquireQueued方法中,判断Node是否是FIFO队列中优先级最高的节点,如果优先级最高,尝试获取锁资源,获取成功就会解除Thread block并执行相应的业务代码;如果不满足优先级最高或者获取资源失败,则调用sholdParkAfterFailedAcquire()方法,这个方法的主要工作是清除node节点的canceled predecessor,并将node.pre的waitstatus设置成SIGNAL状态(对AQS FIFO queue中Node的状态解释,在文末部分)。

parkAndCheckInterrupt()方法的作用是通过LockSupport.park方法阻塞当前线程,并返回当前线程是否被interrupt。通过park阻塞后,当前线程只能被reentrantLock.release()或者其他线程通过interrupt唤醒,但无论任何唤醒方式,都需要进行自旋操作,第N次尝试获取AQS.state资源,直到能够成功获取时,才能正常解除阻塞并执行后续操作(例如因为interrupt造成抛出InterruptException异常)。

对于LockSupport的park和unpark,有两个注意点:

  1. park方法造成的block,当被外界interrup时,不会抛出InterruptException,只能通过Thread.isInterrupted来判断。
  2. JDK API中说到,park方法的block broken可以被三种情况触发,常见的两种方法是unpark和interrupt,还有一种是没有任何原因的自动broken,造成这个原因的相关文章可以参考https://blog.csdn.net/hengyunabc/article/details/28126139?utm_medium=distribute.pc_relevant.none-task-blog-baidujs_baidulandingword-4&spm=1001.2101.3001.4242;而对于无原因自动broken,官方给出的解决方案是自旋判断condition,在condition不满足时重新调用park方法。

分析到这一步,我们就可以猜测出整个ReentrantLock的lock与unlock机制了,lock阻塞后,unlock会从AQS FIFO queue中获取到queue header.next Node对于的Thread,通过调用LockSupport.unpark(thread)来唤醒,唤醒后的thread通过自旋重新进入tryAcquire()方法尝试获取AQS.state,获取成功后修改AQS FIFO queue的header节点。这里可以发现,Non Fair Lock不公平的原因是,从AQS FIFO queue中唤醒的thread1,在tryAcquire的时候,很可能被不存在于queue中的thread2抢占资源,从而导致thread1重新调用LockSupport.park进入阻塞。

因此,Fair Lock的实现就是在Non Fair Lock的基础上,在tryAcquire方法时,提前判断AQS FIFO queue中是否存在block的Node,如果存在,当前线程不应该直接获取锁,而是直接加入到queue tail中。

在acquireQueued方法中,还剩下最后一块finally语句块,要触发这部分,对于reentrantLock,只有当调用lock(long waitTime)或者lockInterrupt()这种允许线程在没有获取到AQS.state时也能自行进入runnable状态的方法,才会触发finally语句块中的cancelAcquire(node),这个方法的源码逻辑并不难,可以自行阅读,主要的作用就是设置node的waitstatus为CANCLE,并且当node是AQS FIFO queue队列中优先级最高的节点时,自动调用lock.unlcok的逻辑唤醒successor Node。

private void cancelAcquire(Node node) {
    // Ignore if node doesn't exist
    if (node == null)
        return;

    node.thread = null;

    // Skip cancelled predecessors
    Node pred = node.prev;
    while (pred.waitStatus > 0)
        node.prev = pred = pred.prev;

    // predNext is the apparent node to unsplice. CASes below will
    // fail if not, in which case, we lost race vs another cancel
    // or signal, so no further action is necessary.
    Node predNext = pred.next;

    // Can use unconditional write instead of CAS here.
    // After this atomic step, other Nodes can skip past us.
    // Before, we are free of interference from other threads.
    node.waitStatus = Node.CANCELLED;

    // If we are the tail, remove ourselves.
    if (node == tail && compareAndSetTail(node, pred)) {
        compareAndSetNext(pred, predNext, null);
    } else {
        // If successor needs signal, try to set pred's next-link
        // so it will get one. Otherwise wake it up to propagate.
        int ws;
        if (pred != head &&
            ((ws = pred.waitStatus) == Node.SIGNAL ||
             (ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) &&
            pred.thread != null) {
            Node next = node.next;
            if (next != null && next.waitStatus <= 0)
                compareAndSetNext(pred, predNext, next);
        } else {
            unparkSuccessor(node);
        }

        node.next = node; // help GC
    }
}

AQS中的ConditionObject

AQS的ConditionObject是一个inner class,是ReentrantLock.newCondition核心对象。

这里只说明一点:condition.await方法时,Thread Node对象也是保存在FIFO队列,但不同于上面说到的AQS的FIFO queue,而是一个单独的condition FIFO queue,其数据结构类似于:

【Thread1 Node】->【Thread2 Node】

header Node一定是一个有效的Node(这不同于AQS FIFO queue,AQS queue在第一次初始化时的header Node是一个虚节点,用来表示当前reentrantLock.lock()成功获取到锁资源的虚拟对象),所有处于condition FIFO queue中的Node,其waitStatus一定是Condition。在其他线程通过condition.signal()唤醒Node时,Node从condition FIFO queue进入到AQS FIFO queue中,添加到队尾,然后继续按照AQS的逻辑执行资源抢占。

注意:无论是condition还是java sychronized原语中的Object.wait()方法,,在wait方法被外部interrupt后,必须获取到锁资源,才会抛出interruptException异常信息,否则即使被外部中断,只要没有资源,依然是死锁的。

Share Mode

AQS的共享模式,我们以Semaphore为例分析。

Semaphore相关介绍

  1. 从java5+支持。
  2. semaphore内部维护了N个permits(许可证),通过acquire(int n)方法尝试获取permit,如果获取不到就阻塞;通过release(int n)方法释放permit。注意:release操作是线程不敏感的,可以有ThreadA来释放ThreadB获取到的permit。
  3. Semaphore常用于对resource资源的控制,例如控制Pool中可用的连接数量。
  4. Semaphore分为公平锁和非公平锁,通过构造函数时指定:默认创建非公平锁,在非公平锁下,适合多线程任务的抢占式工作;对于公平锁,适合对于resource的获取,以防止线程饥饿;但需要注意:tryAcquire(int)方法会打破公平锁的功能,如果能够获取到permits,那么会直接获取,而不会加入到thread queue中,如果想使用公平锁的功能,可以使用tryAcquire(int,long=0,TimeUnit.SECONDS)方法来代替
  5. Semaphore的permits设置为1时,就可以作为Lock对象使用,此时称为binary semaphore;这个用途可以用来防止死锁问题。
  6. Happen-Before原则:release()方法之前的代码一定早于acquire()之后的方法。

源码解析

Semaphore的await()方法会调用acquireSharedInterruptibly方法,可以看到,Share Mode所使用的API不同于Exclusive Mode,所有的方法都带有后缀Shared,例如tryAcquireShared()。

public final void acquireSharedInterruptibly(int arg)
        throws InterruptedException {
    if (Thread.interrupted())
        throw new InterruptedException();
    if (tryAcquireShared(arg) < 0)
        doAcquireSharedInterruptibly(arg);
}

在Share mode中,AQS.state的初始值等于permit的数量,每次调用tryAcquireShared方法,都有可能执行AQS.state--操作,对应的表示permitPool中的permit被某个thread获取。

当permitPool中的剩余数量无法满足Thread的需要时,就会进入Thread Node enqueue的逻辑,源码如下:

private void doAcquireSharedInterruptibly(int arg)
    throws InterruptedException {
    final Node node = addWaiter(Node.SHARED);
    boolean failed = true;
    try {
        for (;;) {
            final Node p = node.predecessor();
            if (p == head) {
                int r = tryAcquireShared(arg);
                if (r >= 0) {
                    setHeadAndPropagate(node, r);
                    p.next = null; // help GC
                    failed = false;
                    return;
                }
            }
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                throw new InterruptedException();
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

我们能够发现,share mode的enqueue几乎与exclusive mode一样,只有一点不同之处,setHeadAndPropagate()方法,可能同时唤醒N个queue node,而不仅仅是1个。

CLH Queue介绍

clh队列,全称Craig, Landin, and Hagersten queue,是自旋锁的主要数据结构,二AQS中所使用的FIFO queue,是CLH queue的一种变体,其结构如下:

queue需要一个head node,一个tail node和N各节点node,在出队列时,只需要将head节点设置成head.next就可以,需要注意的是,出队列一定是线程安全的,因此不需要任何原子性保证;入队列时,需要将新的node设置成tail,将old tail.next指向new node,入队列需要注意线程安全问题

队列中每一个Node节点都会维护当前节点的status(0,SIGNAL(-1),CONDITION(-2),PROPAGATE(-3),CANCEL(1)),tail node status都是0,当enqueue发生后,old tail node的 status设置成SIGNAL,表示当前节点存在successor。

CONDITION状态只会出现在exclusive mode的ConditionObject中,用于表示当前Node是一个condition.await产生的节点。

PROPAGETE目前只有share mode中出现,但还弄明白出现的时机。

CANCEL状态表示当前NODE因为interrupt或者timeout取消获取,但是cancel node不会自己自动的从CLH队列中出队,只会被successor入队或者predecessor release资源的时候发现并剔除(如果使用lockInterrupt()方法,则可以通过抛出InterruptException来达到自动出队的效果)。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值