ReentrantLock、CountDownLatch、Semaphore实现原理

1.概述

ReentrantLockCountDownLatchSemaphore底层都是基于AQS实现的,其中tryAcquiretryReleasetryAcquireSharedtryReleaseShared即加锁以及释放锁的逻辑则由这三个子类自己实现。AQS的实现细节详见AbstractQueuedSynchronizer(AQS)中独占模式与共享模式的设计与实现

2.ReentrantLock

2.1 ReentrantLock中加锁逻辑的实现

ReentrantLock中拥有Sync类型的内部类,其中Sync继承AQS,采用独占模式下的AQSSync的子类包括NonfairSync(非公平)与FairSync(公平)两种模式,所有对AQS的操作便通过Sync进行。在初始化的时候会实例化一个Sync对象,默认实现为非公平模式。

    /**
     * Creates an instance of {@code ReentrantLock}.
     * This is equivalent to using {@code ReentrantLock(false)}.
     */
    public ReentrantLock() {
        sync = new NonfairSync();
    }

同时也可以指定Sync的实现为公平模式or非公平模式

    /**
     * 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();
    }
2.1.1 ReentrantLock非公平模式下的加锁实现

非公平模式下获取锁的逻辑,线程第一次进入的时候会利用cas操作更新state状态为1,如果更新成功,则设置当前线程为owner;当cas操作失败(包括两种情况1.重入锁逻辑:已经获取锁的线程再次调用lock方法;2.当前线程持有锁,其他线程调用lock方法,即持有锁的线程和调用lock方法的线程不是同一个),则进入到acquire方法中,acquire会优先调用子类的tryAcquire方法,具体获取锁的实现就是在该方法中。

/**
 * Sync object for non-fair locks
 */
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() {
        /*第一次获取锁时,设置state为1*/
        if (compareAndSetState(0, 1))
            /*设置当前线程为owner*/
            setExclusiveOwnerThread(Thread.currentThread());
        else
            /*抢夺锁以及重入锁逻辑*/
            acquire(1);
    }

    /*获取锁的具体实现*/
    protected final boolean tryAcquire(int acquires) {
        return nonfairTryAcquire(acquires);
    }
}

tryAcquire中调用nonfairTryAcquire方法,采用cas操作更新state的值,成功后就设置当前线程为owner,代表获取锁成功;如果当前线程等于owner,就累加state值(重入锁的实现逻辑),多次调用lock方法加锁,解锁时也要调用同样次数的unlock方法,直到state值减为0。

/**
 * 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();
    /*获取state值*/
    int c = getState();
    if (c == 0) {
        /*cas操作更新state状态*/
        if (compareAndSetState(0, acquires)) {
            /*更新成功后设置当前线程为owner*/
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    /*如果当前线程是onwer(重入锁逻辑)*/
    else if (current == getExclusiveOwnerThread()) {
        /*获取state的值,并且加上本次的acquires再更新state*/
        int nextc = c + acquires;
        if (nextc < 0) // overflow
            throw new Error("Maximum lock count exceeded");
        setState(nextc);
        return true;
    }
    /*获取锁失败*/
    return false;
}
2.1.2 ReentrantLock公平模式下的加锁实现

公平模式下的加锁实现与非公平的比较在于多了hasQueuedPredecessors方法的判断,该方法主要是判断阻塞队列的队首是否含有元素以及如果队首有元素是否与当前线程相等,目的其实就是检测有没有排在当前线程之前的线程需要出队,严格保证先进先出。tryAcquire除了在阻塞线程被唤醒的时候会被调用外,外部代码调用lock方法的时候也会间接调用到,当外部代码调用lock时,就会通过hasQueuedPredecessors方法校验阻塞队列中是否还有等待中的线程,如果有,就不会让该线程获取锁。

/**
 * 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) {
            /*hasQueuedPredecessors用于判断AQS的阻塞队列里是否有等待中的线程*/
            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;
    }
}

hasQueuedPredecessors的实现

    public final boolean hasQueuedPredecessors() {
        // The correctness of this depends on head being initialized
        // before tail and on head.next being accurate if the current
        // thread is first in queue.
        Node t = tail; // Read fields in reverse initialization order
        Node h = head;
        Node s;
        return h != t &&
            ((s = h.next) == null || s.thread != Thread.currentThread());
    }
2.2 ReentrantLock释放锁逻辑

公平模式与非公平模式下释放锁的实现都是相同的,tryRelease方法在Sync类中实现。释放锁的过程就是减少state的值直到0(因此在重入锁的情况下,调用unlock的次数要与lock的次数相同),同时将owner置空。

protected final boolean tryRelease(int releases) {
    /*获取state值并减少对应次数*/
    int c = getState() - releases;
    /*判断当前线程是否为owner*/
    if (Thread.currentThread() != getExclusiveOwnerThread())
        throw new IllegalMonitorStateException();
    boolean free = false;
    /*state减为0后,设置owner为null*/
    if (c == 0) {
        free = true;
        setExclusiveOwnerThread(null);
    }
    setState(c);
    return free;
}
2.3 ReentrantLocklockInterruptibly响应中断原理

当线程调用lock方法后并处于阻塞状态下,该线程是无法响应中断的,只有获取到锁之后才能响应中断,但是使用lockInterruptibly就能在阻塞状态下响应中断。lockInterruptibly方法调用acquireInterruptibly(该方法在AQS中实现)

public final void acquireInterruptibly(int arg)
        throws InterruptedException {
    /*判断是否被中断过,如果被中断过则直接抛出异常*/
    if (Thread.interrupted())
        throw new InterruptedException();
    /*获取锁失败则调用doAcquireInterruptibly方法*/
    if (!tryAcquire(arg))
        doAcquireInterruptibly(arg);
}

重点关注doAcquireInterruptibly方法,该方法的实现与acquireQueued非常相似(acquireQueued实现可以参考AQS),区别在于阻塞状态下的线程因为中断被唤醒时,parkAndCheckInterrupt返回true,就会抛出InterruptedException异常。

/**
 * Acquires in exclusive interruptible mode.
 * @param arg the acquire argument
 */
private void doAcquireInterruptibly(int arg)
    throws InterruptedException {
    final Node node = addWaiter(Node.EXCLUSIVE);
    boolean failed = true;
    try {
        for (;;) {
            final Node p = node.predecessor();
            if (p == head && tryAcquire(arg)) {
                setHead(node);
                p.next = null; // help GC
                failed = false;
                return;
            }
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                /*线程因为中断被唤醒后直接抛出异常*/
                throw new InterruptedException();
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

3.CountDownLatch

CountDownLatch同样也拥有一个Sync类型的内部类,其中Sync继承AQS,使用的是共享模式下的AQSSync的子类只有一个,在初始化的时候实例化。Sync的实现如下,包括构造函数(给state赋初始化值),获取锁逻辑(判断state值是否为0,为0则返回1否则返回-1),释放锁逻辑(state值减少1)。

/**
 * Synchronization control For CountDownLatch.
 * Uses AQS state to represent count.
 */
private static final class Sync extends AbstractQueuedSynchronizer {
    private static final long serialVersionUID = 4982264981922014374L;

    /*初始化的时候state的值*/
    Sync(int count) {
        setState(count);
    }

    int getCount() {
        return getState();
    }

    /*获取锁逻辑*/
    protected int tryAcquireShared(int acquires) {
        return (getState() == 0) ? 1 : -1;
    }

    /*释放锁逻辑*/
    protected boolean tryReleaseShared(int releases) {
        // Decrement count; signal when transition to zero
        for (;;) {
            /*获取state状态*/
            int c = getState();
            if (c == 0)
                return false;
            /*state值减1并使用cas更新*/
            int nextc = c-1;
            if (compareAndSetState(c, nextc))
                return nextc == 0;
        }
    }
}
2.1.1 CountDownLatch初始化构造

CountDownLatch初始化的时候会根据传入的值设置state的值,当调用countDown方法的时候会把该值减少1,直到减为0的时候,阻塞中的线程就能继续执行。

public CountDownLatch(int count) {
    /*校验count合法性*/
    if (count < 0) throw new IllegalArgumentException("count < 0");
    /*设置state初始化值*/
    this.sync = new Sync(count);
}
2.1.2 await方法实现原理

调用await方法时会调用acquireSharedInterruptibly,该方法会首先判断当前线程是否有中断标记,如果有则直接抛出异常。接着会调用tryAcquireShared方法获取锁,该方法通过state是否等于0决定返回1还是-1,因为在构造函数中,会初始化state的值为传递进去的数字,所以在没有调用countDown方法的前提下(同时初始化传递的值为非0),tryAcquireShared获取锁是一定会失败的,因此返回-1,接着进入AQS提供的doAcquireSharedInterruptibly方法中。

public final void acquireSharedInterruptibly(int arg)
        throws InterruptedException {
    /*如果有中断标记则直接抛出异常*/
    if (Thread.interrupted())
        throw new InterruptedException();
    /*获取锁失败则进入阻塞队列*/
    if (tryAcquireShared(arg) < 0)
        /*AQS方法,让当前线程进入阻塞队列并阻塞*/
        doAcquireSharedInterruptibly(arg);
}

doAcquireSharedInterruptibly在该场景下主要完成两件事情,1.将当前线程加入到阻塞队列中;2.阻塞当前线程。doAcquireSharedInterruptiblydoAcquireShared(doAcquireShared实现参考)的实现很相似,区别在于doAcquireSharedInterruptibly如果因为中断被唤醒,就会直接抛出异常,该方法是能响应中断的。

/**
 * Acquires in shared interruptible mode.
 * @param arg the acquire argument
 */
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);
    }
}

因此当前线程调用await方法的时候,如果state不为0时就会被阻塞,等待其他线程调用countDown直到state减少到0才会继续执行下去,同时,如果阻塞中的线程被调用中断方法,则会直接抛出InterruptedException异常。

2.1.3 countDown方法实现原理

countDown方法最终会调用CountDownLatch实现的tryReleaseShared释放锁方法,该方法会将state中的值减少1,并返回state是否等于0的信息。当调用countDown方法直到state减少到0的时候,tryReleaseShared就会返回true,通过调用AQS中的doReleaseShared方法,唤醒阻塞队列中因为调用await所阻塞的线程。

public void countDown() {
    sync.releaseShared(1);
}
public final boolean releaseShared(int arg) {
    /*获取锁成功则唤醒等待队列中的线程*/
    if (tryReleaseShared(arg)) {
        doReleaseShared();
        return true;
    }
    return false;
}

3.Semaphore

3.1 Semaphore的构造方法

Semaphore同样拥有Sync类型的内部类,其中Sync继承AQS,采用共享模式下的AQSSync的子类与ReentrantLock类似,包括NonfairSync(非公平)与FairSync(公平)两种模式,默认实现为非公平模式。

public Semaphore(int permits) {
    sync = new NonfairSync(permits);
}

同时也能指定使用哪一种模式

public Semaphore(int permits, boolean fair) {
    sync = fair ? new FairSync(permits) : new NonfairSync(permits);
}

Semaphore初始化的时候,会传递permits,该值会被设置为state的值。

3.2 acquire方法实现原理

acquire会调用acquireSharedInterruptibly方法,tryAcquireShared方法(获取锁逻辑)由Semaphore自己实现。如果获取锁失败,则进入阻塞队列。

public final void acquireSharedInterruptibly(int arg)
        throws InterruptedException {
    /*发现中断标记则直接抛出异常*/        
    if (Thread.interrupted())
        throw new InterruptedException();
    /*获取锁失败则进入阻塞队列*/
    if (tryAcquireShared(arg) < 0)
        doAcquireSharedInterruptibly(arg);
}

tryAcquireShared有两种不同的实现,先说在非公平模式下的实现,直接减去acquires的大小并更新state

final int nonfairTryAcquireShared(int acquires) {
    for (;;) {
        /*获取state的值*/
        int available = getState();
        /*较少acquires的值并更新state*/
        int remaining = available - acquires;
        if (remaining < 0 ||
            compareAndSetState(available, remaining))
            return remaining;
    }
}

公平模式下获取锁的逻辑相对于非公平模式下的多了hasQueuedPredecessors函数判断出队的元素是否位于队首,如果是外部代码调用的,则判断阻塞队列中是否还有未出队的元素,此处逻辑与ReentrantLock的公平锁实现逻辑相似。

protected int tryAcquireShared(int acquires) {
    for (;;) {
        if (hasQueuedPredecessors())
            return -1;
        int available = getState();
        int remaining = available - acquires;
        if (remaining < 0 ||
            compareAndSetState(available, remaining))
            return remaining;
    }
}

3.3 release方法实现原理

release方法会调用releaseShared,其中tryReleaseShared(释放锁)成功后就会通过AQS提供的doReleaseShared方法唤醒阻塞队列中的线程。

public final boolean releaseShared(int arg) {
    /*成功释放则唤醒阻塞队列中的线程*/
    if (tryReleaseShared(arg)) {
        doReleaseShared();
        return true;
    }
    return false;
}

tryReleaseShared的逻辑非常简单,就是增加对应的releases值并更新state

protected final boolean tryReleaseShared(int releases) {
    for (;;) {
        int current = getState();
        /*增加state的值并更新*/
        int next = current + releases;
        if (next < current) // overflow
            throw new Error("Maximum permit count exceeded");
        if (compareAndSetState(current, next))
            return true;
    }
}

4.总结

对于ReentrantLockCountDownLatchSemaphore实现加锁解锁逻辑最重要的变量是AQS中的state变量。

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

推荐阅读更多精彩内容