从ReentrantLock的实现了解AQS(上)

4 篇文章 0 订阅
3 篇文章 0 订阅


之前文章很多次提到了AQS,AQS全名是 AbstractQueuedSynchronizer,它是 java.util.concurrent.locks包下的一个抽象类,和它的包名一样,AQS主要的作用就是用来实现锁,它跟synchronized的区别是,synchronized虽然我们可以使用,但是内部都是通过底层C语言级别的锁实现,而AQS是在java代码层面为我们提供出来的一个实现锁的框架,并且jdk工具包中的很多类已经是基于AQS的实现,比如: ReentrantLock,ReentrantReadWriteLock,Semaphore,CountDownlatch。虽然最终实现的逻辑以及用途不一样,其实都是对AQS已经提供的锁功能的一些扩展。本篇文章我们通过ReentrantLock内部的实现来了解一下AQS的内部结构以及原理。

ReentrantLock内部结构

首先ReentrantLock是一个可重入锁,它并不是直接实现的AQS,而是通过几个静态内部类对AQS的扩展,实现了可重入的公平以及非公平锁,虽然ReentrantLock内部提供了非常多锁的使用方法,但其实都是通过代理这几个内部类来完成,我把ReentrantLock中的内部结构拿出来一些进行组合放到下边,方便我们理解大致结构以及流程。

public class ReentrantLock implements Lock, java.io.Serializable {
    private final Sync sync;
    
    public ReentrantLock() {
        sync = new NonfairSync();
    }
    
    public ReentrantLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
    }
    
    public void lock() {
        sync.lock();
    }
    
    public void unlock() {
        sync.release(1);
    }
    
    abstract static class Sync extends AbstractQueuedSynchronizer {  
      abstract void lock();
      ...
    }
    
    static final class NonfairSync extends Sync {
        private static final long serialVersionUID = 7316153563782823691L;
        
        final void lock() {
            if (compareAndSetState(0, 1))
                setExclusiveOwnerThread(Thread.currentThread());
            else
                acquire(1);
        }
       ...
    }
    
     static final class FairSync extends Sync {
        private static final long serialVersionUID = -3000897897090466540L;

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

从上边代码可以看出来,ReentrantLock默认是一个非公平锁,可以通过设置布尔类型来改变锁规则,非公平锁和公平锁最终其实都是AQS抽象类的实现,Sync对AQS进行了进一步抽象,提供了可以支持公平与非公平锁的lock方法,当然实际内部比我要拿出来的部分复杂很多,我们先了解了大概的内部结构,然后再进一步去了解一些细节实现。

Sync

Sync是AQS的实现类,它除了包含AQS的所有功能之外,还提供了一些对AQS的扩展实现,比如上边提到过的Condition控制。Condition我们暂时先不说,主要从Sync提供的抽象方法也是核心锁的实现方法lock入手,所以我们要分析的重点就是Sync的实现类,NonfairSync和FairSync

FairSync

公平锁从名字就能知道它的主要特点就是公平,公平锁的公平保证的是多个线程在争抢锁的过程中,让他们按照先到先拿锁的顺序规则执行。可以试想一下这种实现的优缺点,简单点说,优点肯定是能保证线程如果没拿到锁,最先睡眠的线程在可以拿锁的时候第一个去拿到锁往下执行,不会因为抢不上锁导致当前线程的任务一直停滞。但是这样做的弊端是什么呢?这样的话是不是就要把那些本来准备一哄而来争抢锁的线程按照顺序规规矩矩的排好队,哪怕是线程数量非常多,那也得一个一个排队,每一个都要先排到队内睡下,然后等待之前的线程唤醒,N个排队的线程就需要N次睡眠,N次唤醒,一次都落不下。这个可以优化吗?如何优化?我们先抛开这个问题,看下公平锁的内部实现。

static final class FairSync extends Sync {
        private static final long serialVersionUID = -3000897897090466540L;

        final void lock() {
            acquire(1);  //AQS中的逻辑
        }

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

可以看到,其实公平锁的内部真正扩展实现的就是拿锁的逻辑,没有其他任何的多余,虽然这两个方法看着互相没有啥联系,但其实只是因为公平锁把AQS中的拿锁方法tryAcquire给重写了acquire才是AQS实现锁的核心入口以及实现,但是不论它是怎么实现的,抢锁的第一步肯定就是抢锁,抢不成功才有后续的处理tryAcquire就是拿锁,所以我们就先看它就可以。

我将作者tryAcquire方法的注释备注了中文翻译,从意思中我们就可以理解到,既然公平锁中的tryAcquire是一个拿锁的公平版本,那就一定有不公平的版本了呗。是的,非公平锁中的tryAcquire方法就是非公平版本。正因为是公平版本,想要保证公平的拿锁,那就只能有以下几种情况才能给。

1. 无等待或优先才给机会尝试

没有等待的线程(优先)就给拿锁的机会

if (!hasQueuedPredecessors() && compareAndSetState(0, acquires)) {
     setExclusiveOwnerThread(current);
     return true;
}

从这段代码看语义就可以看出来,不能队列中有前辈,没有等待的线程那肯定就是说该我了呗,那就给我,但是为什么是说只给一个机会呢?是因为锁本来就是一个需要被竞争的东西,谁能保证A在拿锁的过程中,就没有B也在尝试拿锁?所以哪怕是可以给你拿锁了,也只是给你一个机会,让你用CAS的方式去修改State,如果修改成功了,那就代表锁是你的了,使用setExclusiveOwnerThread方法将锁的所属人设置为你,修改失败就是代表又被别人占了,那你再等会儿吧。没错**compareAndSetState就是AQS中占锁的方法**,利用CAS的方式修改State的值,State就是AQS中锁被占用的状态setExclusiveOwnerThread也是AQS中的方法,用来设置当前锁的所属线程

2. 递归调用

已经拿过锁了还来拿(递归调用)?那行,给你

else if (current == getExclusiveOwnerThread()) {
                int nextc = c + acquires;
                if (nextc < 0)
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
}

作者说的递归调用的意思其实就是说这个线程已经拿到过锁了,但是又调用了持有相同锁的代码块,那总不能阻塞,让拿锁的线程在阻塞的状态下,还要等自己释放?只能让它再次拿到锁,将State的值加1,代表一共递归拿了一次,只有将State减回0的时候,才能代表这个线程释放了锁。这就是我上边提到过的ReentrantLock是一个可重入锁

NonfairSync

上边解释了公平锁的实现,非公平锁的实现流程几乎一样。为什么不是全部一样呢?那肯定非公平锁需要体现非公平这个特点呀。个人总结非公平锁跟公平锁实现有两点不同,第一点是非公平的体现,第二点是非公平锁的拿锁方法写在了Sync中(emm,我也暂时没有想到为什么这么做,看出猫腻之后补充)。那非公平如何体现呢?我们先不看代码,公平是按顺序,那非公平肯定要做的就是不需要按顺序给锁了呀。是的,非公平锁的特点就是,谁来拿锁的时候,都可以试试。我们先看下实现

/**
 * 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() {
        if (compareAndSetState(0, 1))
            setExclusiveOwnerThread(Thread.currentThread());
        else
            acquire(1);
    }

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

上边是非公平锁内部的实现,但是跟我说的一样,它非公平方式拿锁的方法是在Sync类中实现的。下边附上部分代码

abstract static class Sync extends AbstractQueuedSynchronizer {
    ...
     	/**
         * 执行块tryLock。tryAcquire在子类,但两者都需要trylock方法的非公平尝试。
         */
        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;
        }
    ...
}    

用非公平锁的实现跟公平锁的代码进行对比,其实就能看出,非公平锁和公平锁的区别就以下几点

1. 立即尝试

别的先不管,先拿一次锁试试

final void lock() {
        if (compareAndSetState(0, 1))
            setExclusiveOwnerThread(Thread.currentThread());
        else
            acquire(1);
    }

从这部分代码,一眼就能看出非公平锁的简单粗暴,它不需要像公平锁那样斯文,谁调用lock方法的时候,就先去尝试拿锁,拿到就是你的。compareAndSetState(0, 1)这个方法在公平锁的部分已经介绍过了,这里不再过多解释。当来拿锁的线程没有拿到,才会走真正的锁的核心逻辑,但是上边我们说过acquire方法第一个逻辑就是拿锁,嗯?拿锁?刚拿完又拿?没错,就是拿过没拿到又拿。这次走非公平锁拿锁的逻辑nonfairTryAcquire

2. 再次尝试

没拿到?那就再拿,不需要考虑有没有人等着排序,不是你的锁就给你机会,已经拿过了?,那就还是你的。

if (compareAndSetState(0, acquires)) {
      setExclusiveOwnerThread(current);
      return true;
}

明显这个方法和公平锁相比少了判断是否有其他优先需要拿锁的线程,直接去尝试拿锁。其他的逻辑就和公平锁一样了,我们不再做解释。

3. 递归调用

same as FairSync.2

NonfairSync vs FairSync ?

  1. ReentrantLock是一个可重入锁
  2. ReentrantLock默认是一个非公平锁

如上是文章中有提到过的两点ReentrantLock特征,第一点我们文章中已经解释了,第二点为什么它默认是非公平锁而不是公平锁呢?

疑问

我们了解了ReentrantLock内部的结构以及公平非公所锁的实现之后,其实发现公平锁和非公平锁的区别只是拿锁的时候,一个在无等待或者优先的情况下才给机会尝试拿锁,另外一个不考虑任何等待,连续两次尝试拿锁。其他的后续逻辑完全一样,都是AQS的内部的后续流程了。为什么只有这一点不一样,ReentrantLock就选择了非公平锁当默认的锁规则呢?

解答

其实,非公平锁就是对公平锁的优化,公平锁虽然保证了公平,但是一旦遇到竞争的情况,线程排队睡眠,被唤醒,这些操作一个少不了。但是非公平锁在不考虑顺序的情况下,只要在拿锁的时候,多试一试,没准就直接拿到了锁,少了先去睡眠,然后被唤醒的操作,这样不就节省了很大一部分的操作,当然效率就会高了。这也是为什么非公平锁要试两次,就是为了尽可能的减少按顺序切换的操作。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值