戏说java AQS和源码分析

花了两天时间,终于把AQS的基本原理和源码理了一遍,本不想记下来,但是害怕时间久了就淡忘了,毕竟好记性不如烂笔头,还是开始码字吧。

一、楔子

说起编程的发展,大趋势就是面向过程的编程向面向对象的编程发展的。现在大多数语言都是面向对象的了,究其原因,大多是因为面向对象编程,提供了更高的可扩展性,封装、继承、多态等特性,给面向对象提供了更多的操作空间,精简了更多的重复代码,更加符合我们人类世界的活动。我想还有另外一个重要原因是,随着硬件的升级,现在大多都是多核服务器,多个cpu可以同时运作,那么我们的过程化编程就显得有点浪费资源,我们把一个过程拆分到多个模块,由多个cpu同时运作,它不香吗?

具体到java语言,那就是多线程高并发的编程。一说到这里,各位javaer马上脑袋里就反应出了synchronize和lock字样。确实,java里面基本就靠这两个东西实现多线程编程了。多线程是什么?多个线程同时运行,充分利用服务器的计算上限,恨不能把cpu一直干到100%,超级开心。但是,多线程效率高了,那么就会带来相应的一些问题,最首当其冲的就是对共享资源的可见性和安全性。同时多个线程访问同一个资源或者同一个变量,肯定会造成这个资源的误读误写的。你可以想象一下,突然有一天,平时跟你勾心斗角的同事A,嘴角带着一丝邪魅微笑,给你介绍了一个家财万贯,身材婀娜多姿的人美路子野的妙龄少女,你会怎么办?当然是不管三七二十八,花个几万,从头到脚把自己打整干净,贷款60w在高新区整一套公寓,逼着父母给了22w买个马自达cx-5,骚红那种,第二天兴冲冲准备998朵玫瑰去跟这个女孩儿求婚,结果呢?如果这个女孩心地善良,她会直接告诉你她结婚了,麻蛋,花了这么些钱白干了么?家底花完了,每个月还要还1w的房贷车贷,苦啊!!!这还算好的,女孩比较耿直,如果女孩不告诉你她结婚了还跟你睡了一晚,第二天她老公来捉奸在床了,那会怎么办?我不知道,我也没经历过啊。

知道线程安全性的重要了吧,这里的安全不是指的线程不安全,指的是你追的那个白富美(共享资源)不安全啊,一群色狼追着在屁股后面,还想一起上,能安全么?那怎么办呢?给自己加把锁把,你们可以一个个上~~~额,这里请仔细品一下这个上字^_^。

二、修炼内功

市面上的锁太多了,互斥、共享、排他、读写、公平、非公平,andsoon。这些不在我们的讨论范围,我们主要谈谈经常用来和Synchrogazer进行比对的Lock锁吧,我们都知道,Lock是显式锁,需要我们自己加锁然后自己关闭锁,它让我们能够更加灵活多变的对我们的代码进行加锁处理。那么,Lock对代码进行加锁解锁的基本原理是什么呢?它是怎么做到多线程并发访问共享资源时候,保证多个线程能对共享资源进行安全的读写的呢?说道这里,我们就不得不说到我们今天的主角:AQS,如果说Lock是一个武林高手的话,那么AQS就是这个武林高手在爬山时候,失足掉下悬崖,偶然拾得的一本绝世武功的修炼心法。因为java的JUC下面的大多数保证线程安全同步的类,都是基于AQS实现的,包括我们常用的Lock。

那么,AQS究竟是什么东西呢?它是AbstractQueuedSynchronizer的缩写,见名思意,Abstract表明它是一个抽象类,也可以看成它是一系列保证线程安全性的方法和概念的抽象。Queued,我们一眼就能看出,AQS里面肯定会用到了队列,不要问我为什么知道,作为一个征战多年的javaer,这不就类似于一看到91开头的网址,我们的脑海里就出现了宾馆的床么。但是AQS里面的队列有点不一样,它是用了CLH同步锁队列,这是一个双向的队列,有头有尾,方便我们阻塞 和唤醒里面的线程,并对已经中断的线程进行清除。当然这个队列的节点并不是一个个的线程,而是包含了线程 引用的node节点。Synchronizer,表明了我们的AQS它是一个用于线程同步的东西,主要的功能是保证线程的安全同步。

那么,它是怎么做到线程同步安全的呢?首先,它维护了一个int类型的同步状态码,这个状态码是volatile的,保证了每次这个码改变,其他线程能get到。这个状态码牛逼了,在独占锁里,它能表示当前对象是否加锁,加了几把锁(重入了几次),在读写锁里,它能用高位表示读锁的情况,低位表示写锁的占有情况,厉害吧,很多看似简单的东西,其实它是最核心的东西,大道至简嘛。就这么一个简单的volatile int state,就是我们武林秘籍最重要的两个心法之一,另外一个至关重要的心法就是里面维护的CLH队列了。

我们简单看一下,具体的线程是怎么被AQS控制执行的吧。首先,代码里面lock或者什么其他加锁得分方法,都表示哥们儿要开始加锁了,要独自品尝白富美了。但是这个世界告诉你,等等,白富美不是那么好尝的。你要先武装一下自己,把自己打扮成一个node,这才具备了追求白富美的基本条件,然后,进入队列里面等待这个世界和白富美的召唤。当然,作为一个不甘寂寞的男人,你肯定不能这么坐以待毙,你会一直主动想要获取白富美手中的锁,一旦我们的神奇的state变为0,我们里面抢占锁。这就是我们的CAS了,主动出击总好于坐以待毙是不。node里面维护了一个等待状态waitstatus,当它大于0的时候,意味着这个哥们儿跑了,熬不住了,线程取消了。如果它等于-1,那就好办了,说明我已经取到号了,这里得多说一句,现在的社会不像以前,排个队就能办事儿了。信息社会,你排好队了不行,还得要网上取到号,没有 号也不行,这个-1就表示你取了号,处于随时可以办事的状态,否则,就算排队拍到你了,你也得等着,先取到号再来吧。

三、下山历练

我们学习技术,就跟做人一样,最怕的是什么?"道理我都懂,可我依然过不好我的一生"。学习技术,原理你都懂了,最多能应付面试,在实际工作中,我们还得要落实到代码上面去,才能完成我们的任务。那么,我们一起看一下AQS的具体代码吧。

我们就用常用到的ReentrantLock锁来举例跟一下代码吧。我们都知道,在用ReentrantLock的时候,首先调用的是它的lock加锁方法,我吗看一下lock():

public void lock() {
       sync.lock();
}

很简单的一个sync的lock 方法调用,那么这个sync是什么呢?

private final Sync sync;
abstract static class Sync extends AbstractQueuedSynchronizer {
    abstract void lock();
    // 这里实现的非公平获取锁
    final boolean nonfairTryAcquire(int acquires) {
        final Thread current = Thread.currentThread();
        int c = getState();
        if (c == 0) {
            if (compareAndSetState(0, acquires)) {
                setExclusiveOwnerThread(current);
                return true;
            }
        }
        // 如果当前线程正拥有锁,比如递归调用的时候,AQS状态码加1实现可重入锁
        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;
    }
    // 释放锁
    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;
    }
    protected final boolean isHeldExclusively() {
        return getExclusiveOwnerThread() == Thread.currentThread();
    }
    final ConditionObject newCondition() {
        return new ConditionObject();
    }
}

这个sync其实是一个内部类,它继承自我们的AbstractQueuedSynchronizer类,实现了其中的几个特定方法。其实JUC包下面的大多数同步类,都是采用的这种适配器设计模式,类的内部定义一个类继承自其它某一个类,这样我们可以调用其它类的方法达到继承方法的目的。锁队列的安全加入、获取锁、线程的执行到最后的锁释放,AQS都帮我们完成了,AQS的子类只需要实现他的几个特定的用于获取和释放锁方法,这里又使用的是模板方法设计模式。我们接着看代码:

// 非公平锁
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() {
            // CAS设置状态从0到1,如果设置成功,表示获取到锁
            if (compareAndSetState(0, 1))
                // 设置锁的拥有者线程为当前线程
                setExclusiveOwnerThread(Thread.currentThread());
            else
                // 调用AQS公用方法尝试获取锁,失败则加入CLH锁队列
                acquire(1);
        }

        // 自旋获取锁
        protected final boolean tryAcquire(int acquires) {
            return nonfairTryAcquire(acquires);
        }
    }

    /**
     * Sync object for fair locks
     */
    static final class FairSync extends Sync {
        private static final long serialVersionUID = -3000897897090466540L;
        // 新来一个线程,不先去尝试获取锁,而是直接进入CLH锁队列
        final void lock() {
            acquire(1);
        }

        /**
         * Fair version of tryAcquire.  Don't grant access unless
         * recursive call or no waiters or is first.
         */
        protected final boolean tryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();
            // 公平非公平锁的区别在这里,公平锁多调用了一个hasQueuedPredecessors方法
            if (c == 0) {
                if (!hasQueuedPredecessors() &&
                    compareAndSetState(0, acquires)) {
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            else if (current == getExclusiveOwnerThread()) {
                int nextc = c + acquires;
                if (nextc < 0)
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
            return false;
        }
    }

代码中我们可以看到,lock的lock()方法,调用了sync的lock()方法,而这个全局变量sync又有两种实例化方式,根据lock实例化时,传入的参数fair是否为公平锁,分别实例化为不同的sync。

public ReentrantLock() {
    sync = new NonfairSync();
 }

/**
 * Creates an instance of {@code ReentrantLock} with the
 * given fairness policy.
 *
 * @param fair {@code true} if this lock should use a fair ordering policy
 */
public ReentrantLock(boolean fair) {
    sync = fair ? new FairSync() : new NonfairSync();
}

这里提一下,公平非公平锁的区别从字面 上就可以区分开来,公平锁是先进先出,也就是先尝试获取锁的线程,会在锁 队列的头部。

我们看到,非公平锁和公平锁,在获取锁的时候,都调用了acquire (),这个方法就实现了从获取锁-》线程-》锁队列元素-》进队列-》 得到锁的整个过程。

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

首先,tryAcquire()方法尝试获取锁,而这个方法,就是AQS没有实现,交由子类自己定义实现的方法,目的是让开发者定义不同类型,不同获取锁方式的锁满足自己项目的需求。此方法就是调用了sync里面的nonfairTryAcquire()方法或者tryAcquire(),具体代码看上面sync部分有注释。

如果竞争锁失败,则通过addWaiter(Node.EXCLUSIVE)方法将当前线程封装成CLH队列的一个node元素。我们先看一下NODE元素是怎样的结构。其实CLH锁队列并不是一个严格意义上的队列,它只是通过node元素定义的前驱后继节点,实现的一个双向链表。除了前后节点,NODE元素还定义了几个int常量,用于保存当前线程的状态值。

static final class Node {
    /** Marker to indicate a node is waiting in shared mode */
    // 此两个NODE变量用于表明当前队列是共享队列还是独占队列,分别对应共享锁和独占锁
    static final Node SHARED = new Node();
    /** Marker to indicate a node is waiting in exclusive mode */
    static final Node EXCLUSIVE = null;

    /** waitStatus value to indicate thread has cancelled */
    // 当前节点的线程已经取消,可能因为线程中断之类的原因,这是唯一一个大于0的状态值,后续代码中    
    // 会很多地方用来判断当前线程是否可用
    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;
    volatile Node prev;
    volatile Node next;

    volatile Thread thread;
    Node nextWaiter;

    /**
     * Returns true if node is waiting in shared mode.
     */
    // 锁是否共享
    final boolean isShared() {
        return nextWaiter == SHARED;
    }

    /**
     * Returns previous node, or throws NullPointerException if null.
     * Use when predecessor cannot be null.  The null check could
     * be elided, but is present to help the VM.
     *
     * @return the predecessor of this node
     */
    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之后,调用acquireQueued()方法,首先尝试获取锁,注意,这里用到了自旋获取锁for(;;)。

/**
 * Acquires in exclusive uninterruptible mode for thread already in
 * queue. Used by condition wait methods as well as acquire.
 * 自旋尝试获取锁
 * @param node the node
 * @param arg the acquire argument
 * @return {@code true} if interrupted while waiting
 */
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);
    }
}

如果获取锁失败,则调用方法shouldParkAfterFailedAcquire(),设置节点的状态为signal,如果前置节点的状态为cancel,则改变前置节点为前置节点的前置节点,此方法为了保证前置节点状态是signal,这样在以后获取锁时,本节点才能被unpark唤醒,如果前置节点状态非signal,后续节点是没法被唤醒的。随后调用parkAndCheckInterrupt()方法阻塞当前线程并返回线程状态。


/**
 * Checks and updates status for a node that failed to acquire.
 * Returns true if thread should block. This is the main signal
 * control in all acquire loops.  Requires that pred == node.prev.
 *
 * @param pred node's predecessor holding status
 * @param node the node
 * @return {@code true} if thread should 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;
}
private final boolean parkAndCheckInterrupt() {
    LockSupport.park(this);
    return Thread.interrupted();
}

我们注意到,acquireQueued方法的finally块里面,调用了cancelAcquire方法,此处是为了保证,如果自旋获取锁失败,则能够将此节点从队列移除,并保证队列其他节点正常获取锁。

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

上面就是获取锁的过程,失败和成功,锁队列所发生的变化。下面我们看一下释放锁的相关代码。释放锁相对比较容易一点。首先还是从我们熟悉的unlock方法看起。

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

release方法中,首先调用tryRelease()释放锁资源,释放成功则调用unparkSuccessor()方法唤醒后续节点。

protected final boolean tryRelease(int releases) {
    // 状态值减一
    int c = getState() - releases;
    // 当前线程未持有锁则抛异常
    if (Thread.currentThread() != getExclusiveOwnerThread())
        throw new IllegalMonitorStateException();
    boolean free = false;
    // 状态值为0,表明当前线程不再持有锁,则返回true,并重置当前锁的持有线程null
    if (c == 0) {
        free = true;
        setExclusiveOwnerThread(null);
    }
    setState(c);
    // 这里注意,如果c>0 表示当前为重入锁,当前线程还持有锁,返回false
    return free;
}
/**
 * Wakes up node's successor, if one exists.
 *
 * @param node the 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);
}

至此,下一个节点得到unpark唤醒,无缝衔接上面在线程刚加入时候被park起来,还在自旋获取锁,这里的unparkSuccessor()方法,给自旋画上了完美的句号,自旋节点的前驱节点是head,并且当前state为0,自旋节点可以cas成功,那么当前线程就可以执行本地程序后面的代码了。

四、华山论剑

故事太长,又没有金庸先生的情怀,相信大家也看累了,最后的煮酒论英雄环节,我们就长话短说,一张图结束我们的AQS之旅,祝大家在java开发的路上,越走越深入。

参考内功心法:深入理解AQS死磕synchronized

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值