高并发编程-ReentrantLock非公平锁深入解析

要点解说

ReentrantLock是一个可重入的互斥锁,它不但具有synchronized实现的同步方法和同步代码块的基本行为和语义,而且具备很强的扩展性。ReentrantLock提供了公平锁和非公平锁两种实现,在默认情况下构造的ReentrantLock实例是非公平锁,可以在创建ReentrantLock实例的时候通过指定公平策略参数来指定是使用公平锁还是非公平锁。本篇将基于JDK7深入源码解析非公平锁的实现原理。

实例演示

下面是使用ReentrantLock非公平锁的典型代码。

class X {
  private final ReentrantLock lock = new ReentrantLock();

  public void m() {
    lock.lock();
    try {
      // do some thing
    } finally {
      lock.unlock()
    }
  }
}

方法解析

  1. ReentrantLock()创建一个非公平锁ReentrantLock实例;
  2. ReentrantLock(boolean fair)根据公平策略fair参数创建ReentrantLock实例;
  3. lock()获取锁;
  4. unlock()释放锁;
  5. newCondition()返回与此ReentrantLock实例一起使用的Condition的实例;
  6. getHoldCount()获取当前线程持有此锁的次数;
  7. getQueueLength()返回正在等待获取此锁的线程数;
  8. getWaitQueueLength(Condition condition)返回等待与此锁相关的给定条件的线程数;
  9. hasQueuedThread(Thread thread)返回指定线程是否正在等待获取此锁;
  10. hasQueuedThreads()返回是否有线程正在等待获取此锁;
  11. hasWaiters(Condition condition)返回是否有线程正在等待与此锁有关的给定条件;
  12. isFair()返回锁是否是公平锁;
  13. isHeldByCurrentThread()返回当前线程是否持有此锁;
  14. tryLock()尝试获取锁,仅在调用时锁未被其它线程持有时才可以获取该锁;
  15. tryLock(long timeout, TimeUnit unit)尝试获取锁,如果锁在指定等待时间内没有被另一个线程持有,并且当前线程未被中断,则可以获取该锁。

源码解析

从上面的方法解析可以看到它有很多方法,本文将重点深入分析lock()和unlock()方法。首先,从构造函数开始。

    //默认构造方法创建的是非公平锁
    public ReentrantLock() {
        //构造非公平锁
        sync = new NonfairSync();
    }

    public ReentrantLock(boolean fair) {
        //根据公平策略构造公平锁或非公平锁
        sync = fair ? new FairSync() : new NonfairSync();
    }

从上面的代码可以看到,默认构造函数使用NonfairSync创建的是非公平锁,当然也可以指定公平策略参数fair为false创建非公平锁。NonfairSync是ReentrantLock中的静态内部类,它继承了Sync,而Sync是ReentrantLock中的抽象静态内部类,Sync又继承自AbstractQueuedSynchronizer,分析到这里可以看到,ReentrantLock的具体实现使用了AQS。

    abstract static class Sync extends AbstractQueuedSynchronizer {
        //此处省略内部代码,后面具体分析
    }

    static final class NonfairSync extends Sync{
        //此处省略内部代码,后面具体分析
    }

当调用lock()方法获取锁时,具体实现代码如下。

    //ReentrantLock类的lock方法
    public void lock() {
        //此时的sync是NonfairSync的实例对象,所以执行sync.lock()将执行NonfairSync类的lock()方法
        sync.lock();
    }

    //NonfairSync类的lock()方法
    final void lock() {
        //通过CAS设置AQS中state的值
        //如果此时state值等于0,也就是锁没有被其它线程持有,则返回true
        if (compareAndSetState(0, 1))
            //设置独占锁的当前所有者为当前线程
            setExclusiveOwnerThread(Thread.currentThread());
        else
            //否则,尝试获取独占锁
            //acquire方法继承自AbstractQueuedSynchronizer
            acquire(1);
    }

    //AbstractQueuedSynchronizer类的acquire方法
    public final void acquire(int arg) {
        //调用tryAcquire方法尝试获取锁,如果成功则返回,否则先执行addWaiter方法,再执行acquireQueued方法
        //因为公平锁和非公平锁对锁持有的实现不同,所以这里的tryAcquire使用的是NonfairSync类中的实现
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            //中断当前线程
            selfInterrupt();
    }

    //NonfairSync类的tryAcquire方法
    protected final boolean tryAcquire(int acquires) {
        //nonfairTryAcquire方法继承自Sync类
        return nonfairTryAcquire(acquires);
    }

    //Sync类的nonfairTryAcquire方法
    final boolean nonfairTryAcquire(int acquires) {
        //获取当前线程
        final Thread current = Thread.currentThread();
        //获取AQS中state当前值
        int c = getState();
        //如果state当前值等于0
        if (c == 0) {
            //使用CAS修改state值
            //如果此时state值等于0,也就是锁没有被其它线程持有,则修改成功
            if (compareAndSetState(0, acquires)) {
                //设置独占锁的当前所有者为当前线程
                setExclusiveOwnerThread(current);
                //获取到锁
                return true;
            }
        }
        //如果独占锁的当前所有者是当前线程,锁重入的情况
        else if (current == getExclusiveOwnerThread()) {
            //将state值加一
            int nextc = c + acquires;
            //判断新值是否溢出
            if (nextc < 0) // overflow
                throw new Error("Maximum lock count exceeded");
            //更新state值
            setState(nextc);
            //获取到锁
            return true;
        }
        //获取不到锁
        return false;
    }

    //上面分析的代码可以清晰的看到,当锁没有被线程持有时的获取过程,
    //但是,如果锁此时被其它线程持有,即执行tryAcquire方法返回false,
    //此时将需要先执行addWaiter方法,将当前线程封装成Node节点,并将这个Node节点插入到同步等待队列的尾部,
    //然后执行acquireQueued方法,阻塞当前线程,具体实现代码分析如下:

    //addWaiter方法继承自AQS
    //将当前线程封装成Node节点,并将这个Node节点插入到同步等待队列的尾部
    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;
    }

    //acquireQueued方法继承自AQS
    final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;
        try {
            boolean interrupted = false;
            for (;;) {
                //获取当前节点的前驱节点
                final Node p = node.predecessor();
                //如果当前节点的前驱节点是头结点,并且可以获取到锁,跳出循环并返回false
                if (p == head && tryAcquire(arg)) {
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    return interrupted;
                }
                //当前节点的前驱节点不是头结点,或不可以获取到锁
                //shouldParkAfterFailedAcquire方法检查当前节点在获取锁失败后是否要被阻塞
                //如果shouldParkAfterFailedAcquire方法执行结果是当前节点线程需要被阻塞,则执行parkAndCheckInterrupt方法阻塞当前线程
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

    //parkAndCheckInterrupt方法继承自AQS,用于阻塞当前线程
    private final boolean parkAndCheckInterrupt() {
        //阻塞当前线程,当前线程执行到这里即被挂起,等待被唤醒
        //当当前节点的线程被唤醒的时候,会继续尝试获取锁
        LockSupport.park(this);
        return Thread.interrupted();
    }

当调用unlock()方法释放持有的锁时,具体实现代码如下。

    //ReentrantLock类的unlock方法
    public void unlock() {
        //此时的sync是NonfairSync的实例对象
        //在NonfairSync中没有重写release方法,release方法继承自AQS,所以执行AQS的release方法
        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;
    }

    //尝试释放持有的锁
    protected final boolean tryRelease(int releases) {
        //将state值减去1
        int c = getState() - releases;
        //如果当前线程不是持有锁的线程,抛异常
        if (Thread.currentThread() != getExclusiveOwnerThread())
            throw new IllegalMonitorStateException();
        boolean free = false;
        //如果state的值等于零,则表明此锁已经完全被释放
        //如果state的值不等于零,则表明线程持有的锁(可重入锁)还没有完全被释放
        if (c == 0) {
            //free=true表示锁以被完全释放
            free = true;
            setExclusiveOwnerThread(null);
        }
        setState(c);
        return free;
    }

    //唤醒阻塞线程
    private void unparkSuccessor(Node node) {
        int ws = node.waitStatus;
        if (ws < 0)
            compareAndSetWaitStatus(node, ws, 0);

        Node s = node.next;
        //如果后继节点为空或已被取消,则从尾部开始找到等待队列中第一个waitStatus<=0,即未被取消的节点
        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)
            //唤醒等待队列节点中的线程
            //之前执行到parkAndCheckInterrupt方法的线程继续执行,再次尝试获取锁
            LockSupport.unpark(s.thread);
    }

原理总结

A、B两个线程同时执行lock()方法获取锁,假设A先执行获取到锁,此时state值加1,如果线程A在继续执行的过程中又执行了lock()方法,线程A会直接获取锁,同时state值加1,state的值可以简单理解为线程A执行lock()方法的次数;当线程B执行lock()方法获取锁时,会将线程B封装成Node节点,并将其插入到同步等待队列的尾部,然后阻塞当前线程,等待被唤醒再次尝试获取锁;线程A每次执行unlock()方法都会将state值减1,直到state的值等于零则表示完全释放掉了线程A持有的锁,此时将从同步等待队列的头节点开始唤醒阻塞的线程,阻塞线程恢复执行,再次尝试获取锁。ReentrantLock非公平锁的实现使用了AQS的同步等待队列和state。

END

如果觉得有收获,记得关注、点赞、转发。

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

推荐阅读更多精彩内容

  • “今天你错过了谁,谁又失去了你呢?” 今天被分手不久的小R的前男友小q发了这样一条动态,小R看到了,她也看了这部电...
    你是我未完成的歌阅读 148评论 0 0
  • 工具: 毛笔:秋红斋秀意、小小染、老人头勾线 颜料:温莎牛顿学生用 画纸:宝虹棉浆32k 第一步,硫酸纸线稿,这个...
    花梨花Lena阅读 593评论 1 6
  • 1、 今天我在农场里发生了一件奇怪的事情,那就是在读经典前,我妈妈在做后厨,我就做得非常的好,可是呢,老妈一来,我...
    紫彤阅读 154评论 0 0
  • 不用看年月日 天狗一直在吞噬月亮 多少代人都在探寻 为什么我们会老去 这样的道理 你沉静下就可以 清醒的看见 文字...
    原郎阅读 291评论 0 8