从 AbstractQueuedSynchronizer 理解 ReentrantLock

简介

Java 并发编程离不开锁, Synchronized 是常用的一种实现加锁的方式,使用比较简单快捷。在 Java 中还有另一种锁,即 Lock 锁。 Lock 是一个接口,提供了超时阻塞、可响应中断以及公平非公平锁等特性,相比于 Synchronized,Lock 功能更强大,可以实现更灵活的加锁方式。

Lock 的主要实现类是 ReentrantLock,而 ReetrantLock 中具体的实现方式是利用另外一个类 AbstractQueuedSynchronizer,所有的操作都是委托给这个类完成。AbstractQueuedSynchronizer 是 Lock 锁的重要组件,本文从 AbstractQueuedSynchronizer 来分析 ReetrantLock 的实现原理。

基本用法

先看一下 Lock 的基本用法:

Lock lock = ...;
lock.lock();
try{
    //处理任务
}catch(Exception ex){
     
}finally{
    lock.unlock();   //释放锁
}

lock.lock() 即是加锁, lock.unolck() 是释放锁,为了保证所能够释放,unlock() 应该放到 finally 中。

下面分别从 lock()unlock() 方法来分析加锁和解锁到底做了什么。

lock

下面是 lock() 的代码:

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

可以看到,只是简单调用了 sync 对应的 lock() 方法。那么这个 sync 是什么呢?其实这个就是 AbstractQueuedSynchronizer 的实现类。可以看一下 ReentrantLock 的构造方法:

    /**
     * Creates an instance of {@code ReentrantLock}.
     * This is equivalent to using {@code ReentrantLock(false)}.
     */
    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();
    }

ReentrantLock 有两个方法,主要目的是选择是公平锁还是非公平锁。公平锁指的是先来后到,先争抢锁的线程先获得锁,而非公平锁则不一定。ReentrantLock 默认使用的是非公平锁,也可以通过构造参数选择公平锁。选择哪个锁其实是生成了一个对象并赋值给变量 sync,下面是涉及到的代码:


/**
     * Base of synchronization control for this lock. Subclassed
     * into fair and nonfair versions below. Uses AQS state to
     * represent the number of holds on the lock.
     */
    abstract static class Sync extends AbstractQueuedSynchronizer {
        private static final long serialVersionUID = -5179523762034025860L;

        /**
         * Performs {@link Lock#lock}. The main reason for subclassing
         * is to allow fast path for nonfair version.
         */
        abstract void lock();

        /**
         * Performs non-fair tryLock.  tryAcquire is implemented in
         * subclasses, but both need nonfair try for trylock method.
         */
        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;
        }

        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() {
            // While we must in general read state before owner,
            // we don't need to do so to check if current thread is owner
            return getExclusiveOwnerThread() == Thread.currentThread();
        }

        final ConditionObject newCondition() {
            return new ConditionObject();
        }

        // Methods relayed from outer class

        final Thread getOwner() {
            return getState() == 0 ? null : getExclusiveOwnerThread();
        }

        final int getHoldCount() {
            return isHeldExclusively() ? getState() : 0;
        }

        final boolean isLocked() {
            return getState() != 0;
        }

        /**
         * Reconstitutes the instance from a stream (that is, deserializes it).
         */
        private void readObject(java.io.ObjectInputStream s)
            throws java.io.IOException, ClassNotFoundException {
            s.defaultReadObject();
            setState(0); // reset to unlocked state
        }
    }
    // 非公平锁
    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);
        }
    }

    /** 
     * Sync object for fair locks
     *  公平锁
     */
    static final class FairSync extends Sync {
        private static final long serialVersionUID = -3000897897090466540L;

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

ReentrantLock 中有一个抽象的内部类 Sync,继承于 AbstractQueuedSynchronizer 并实现了一些方法。另有两个类 FairSyncNoFairSync 继承了 Sync,它们自然就是公平锁以及非公平锁的实现。下面分析将从公平锁出发,非公平锁与公平锁差别并不是很多。

公平锁 FairSync 加锁的代码如下:

    final void lock() {
        acquire(1);
    }

只有一行,调用了 acquire,这是 AbstractQueuedSynchronizer 中的一个方法,代码如下:

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

acquire 的实现也很短,不多在其中却包含了加锁的具体实现,关键就在内部调用的几个方法中。

为了理解加锁和解锁的过程,下面具体介绍一下 AbstractQueuedSynchronizer(以下简称 AQS)。

AbstractQueuedSynchronizer

AQS 中使用一个同步队列来实现线程同步状态的管理,当一个线程获取锁失败的时候, AQS将此线程构造成一个节点(Node)并加入同步队列并且阻塞线程。当锁释放时,会从同步队列中将第一个节点唤醒并使其再次获取锁。

同步队列中的节点用来保存获取锁失败的线程的相关信息,包含如下属性:

static final class Node {
        /** Marker to indicate a node is waiting in shared mode */
        // 标识共享模式
        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. */
        // 线程取消
        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;
        ...
}

Node 是 AQS 的内部类,其中包含一些属性标识一个阻塞线程的节点,包括是独占模式还是共享模式、节点的状态、前驱结点、后继结点以及节点所代表的线程。

同步队列是一个双向列表,在 AQS 中有这样几个属性:

     /**
     * Head of the wait queue, lazily initialized.  Except for
     * initialization, it is modified only via method setHead.  Note:
     * If head exists, its waitStatus is guaranteed not to be
     * CANCELLED.
     */
     // 头结点
    private transient volatile Node head;

    /**
     * Tail of the wait queue, lazily initialized.  Modified only via
     * method enq to add new wait node.
     */
     // 尾节点
    private transient volatile Node tail;

    /**
     * The synchronization state.
     */
     // 锁的状态
    private volatile int state;

其中,headtail 分别指向同步队列的头结点和尾节点,state 标识锁当前的状态,为 0 时表示当前锁未被占用,大于 1 表示被占用,之所以是大于 1 是因为锁可以重入,每重入一次增加 1。同步队列的结构大致如下图:

lock_queue
lock_queue

了解了同步队列后,下面具体看看加锁和解锁的过程。

加锁

    final void lock() {
        acquire(1);
    }
    public final void acquire(int arg) {
        // 加锁的主要代码
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }

主要逻辑其实就是一行代码:if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg))tryAcquire 是尝试获取一下锁,为什么说是尝试呢?看代码:

protected final boolean tryAcquire(int acquires) {
        final Thread current = Thread.currentThread();
        int c = getState();  // 获取当前状态
        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;
}

tryAcquire 可以分为三条分支:

  1. 当前锁未被占用(getState() == 0),则判断是否有前驱结点,没有的话就用 CAS 加锁(compareAndSetState(0, acquires)),加锁成功则调用 setExclusiveOwnerThread(current) 标示一下并返回 true

  2. 当前线程已经获取过这个锁,则此时是重入,改变 state 的计数即可,返回 true 表示加锁成功。

  3. 如果不是上面两种情况,那么说明锁被占用或者 CAS 没有抢过其它线程,则需要进入同步队列,返回 false 表示尝试加锁失败。

回到 if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) 这一行,如果 tryAcquire(arg) 返回 false 将会执行 acquireQueued(addWaiter(Node.EXCLUSIVE), arg)。先看一下 addWaiter(Node.EXCLUSIVE) ,这个方法的代码如下所示:

private Node addWaiter(Node mode) {
        // 将线程包装成 Node 
        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 节点并加入同步队列。如果尾节点为空,则用 CAS 设置尾节点,如果入队失败则调用 enq(node),这个方法内部是一个循环,利用自旋 CAS 把节点加入同步队列,具体代码就不分析了。

在节点加入队列之后,执行的是 acquireQueued 方法,代码如下:

final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;
        try {
            boolean interrupted = false;
            // 这是一个无限循环
            for (;;) {
                final Node p = node.predecessor();
                // 如果前驱节点是 head,则尝试获取锁
                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 实现了线程的睡眠与唤醒。在内部是一个无限循环,每次获取前驱节点,如果前驱结点是 HEAD,那么尝试去获取锁,获取成功则将此节点变为新的头结点并将原先的头结点出队。如果前驱节点不是头结点或者获取锁失败,那么就会进入 shouldParkAfterFailedAcquire 方法,判断是否进入睡眠,如果这个方法返回 true,则调用 parkAndCheckInterrupt 让线程进入睡眠状态。下面是 parkAndCheckInterrupt 的代码:

private final boolean parkAndCheckInterrupt() {
        LockSupport.park(this); // 线程在这一步进入阻塞状态
        return Thread.interrupted();
}

对于 shouldParkAfterFailedAcquire 来说,如果前驱节点正常,那么会返回 true,表示当前线程应该挂起,如果前驱结点取消了排队,那么当前线程有机会抢锁,此时返回 false,并继续 acquireQueued 中的循环。

解锁

相比于加锁,解锁稍微简单一点,看一下 unlock 的代码:

public void unlock() {
    sync.release(1);
}

public final boolean release(int arg) {
     // 尝试解锁
    if (tryRelease(arg)) {
        // 解锁成功
        Node h = head;
        if (h != null && h.waitStatus != 0)
            // 唤醒后继节点
            unparkSuccessor(h);
        return true;
    }
    return false;
}

首先调用 tryRelease 解锁,如果解锁成功则唤醒后继结点,返回值表示是否成功释放锁。那为什么会解锁不成功,其实是因为重入,看一下 tryRelease 的代码:

protected final boolean tryRelease(int releases) {
    //  更新 state 计数值
    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;
}

state 减去对应的值,如果 state == 0,那么说明锁已经完全释放。

release 中,如果锁已经完全释放,那么将调用 unparkSuccessor 唤醒后继节点,唤醒的节点所代表的线程阻塞在 parkAndCheckInterrupt 中:

private final boolean parkAndCheckInterrupt() {
        LockSupport.park(this); // 线程在这一步进入阻塞状态
        return Thread.interrupted();
}

线程被唤醒后,将继续 acquireQueued 中的循环,尝试获取锁。

总结

本文简要分析了 Lock 锁的原理,主要是利用 AbstractQueuedSynchronizer这个关键的类。AQS 的核心在于使用 CAS 更新锁的状态,并利用一个同步队列将获取锁失败的线程进行排队,当前驱节点解锁后再唤醒后继节点,是一个几乎纯 Java 实现的加锁与解锁。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,590评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 86,808评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,151评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,779评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,773评论 5 367
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,656评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,022评论 3 398
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,678评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,038评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,659评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,756评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,411评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,005评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,973评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,053评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,495评论 2 343

推荐阅读更多精彩内容