ReentrantLock源码详细解读

引言

ReentrantLock是面试中的高频考点,其中实现原理还是很有必要了解的。它与synchronized类似,都是互斥锁,但具有更好的扩展性。ReentrantLock是基于AQS实现的,遗忘的同学可以回顾一下AQS源码详细解读

文章导读

  • ReentrantLock继承树及重要方法
  • 非公平锁及公平锁的获取
  • tryLock(),lockInterruptibly()
  • 释放资源
  • ReentrantLock相关面试题
  • 总结

一、ReentrantLock的内部类概述与重要方法

1.1 继承关系概述
首先看一下继承关系图,对它整体的构造有一个初步的认识。

ReentrantLock内部类继承树

我们发现,ReentrantLock实现了公平锁和非公平锁。都通过他们的父类Sync来调度。

  • Sync:是提供AQS实现的工具,类似于适配器,提供了抽象的lock(),便于快速创建非公平锁。
  • FairSync(公平锁):线程获取锁的顺序和调用lock()的顺序一样,FIFO。
  • NoFairSync(非公平锁):线程获取锁的顺序和调用lock()的顺序无关,抢到CPU的时间片即可调度。

1.2 ReentrantLock中的重要方法

构造方法:无参构造方法,默认创建非公平锁;有参构造方法,并且fair==true时,创建公平锁。

    //维护了一个Sync,对于锁的操作都交给sync来处理
    private final Sync sync;   
 
    public ReentrantLock() {
        sync = new NonfairSync();
    }
    public ReentrantLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
    }

获取资源资源(锁)的方法:可以看出,请求都是交给Sync来调度的。

    //请求锁资源,会阻塞且不处理中断请求,
    //没有调用unLock(),则会一直被阻塞。
    public void lock() {
        sync.lock();
    }
    //线程在请求lock并被阻塞时,如果被interrupt,则此线程会被唤醒并被要求处理
    public void lockInterruptibly() throws InterruptedException {
        sync.acquireInterruptibly(1);
    }
    //尝试获取锁,默认获取的是非公平锁,失败后不会阻塞
    //直接返回true或false
    public boolean tryLock() {
        return sync.nonfairTryAcquire(1);
    }
    //重载方法,在规定时间内获取锁,获取不到则返回false
    public boolean tryLock(long timeout, TimeUnit unit)
            throws InterruptedException {
        return sync.tryAcquireNanos(1, unit.toNanos(timeout));
    }

释放资源(锁)的方法:不管是公平还是非公平锁,都会调用AQS.release(1),给当前线程持有锁的数量-1。

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

二、获取资源(锁)

我们主要看非公平锁与公平锁获取资源的方法,因为释放资源的逻辑是一样的。

2.1 Sync获取资源

sync中定义了获取资源的总入口。具体的调用还是看实现类是什么。

        abstract void lock();

2.2 非公平锁获取资源

lock():获取锁时调用AQS的CAS方法,是阻塞的。如果获取成功,则把当前线程设置为锁的持有者;如果获取失败,则通过AQS.acquire()获取锁。通过AQS源码详细解读,了解到acquire()中使用了模板模式,调用子类的tryAcquire()尝试获取锁,如果tryAcquire()返回false,则进入等待队列自旋获取,再判断前驱的waitStatus,判断是否需要被阻塞等。这里就不一一赘述了,感兴趣的可以看上一篇文章。

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

tryAcquire():走的是Sync.nofairTryAcquire()。

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

nonfairTryAcquire(int acquires):如果锁空闲,则用CAS修改state;如果锁被占用,则判断占有者是不是自己,实现可重入。最终没有获取锁到就返回false。

        final boolean nonfairTryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();
            //state是0,锁处于空闲状态,使用CAS获取
            if (c == 0) {
                if (compareAndSetState(0, acquires)) {
                    //获取成功后,当前线程就是锁的持有者
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            //获取失败则先判断当前线程是否是锁的持有者
            //这里很重要,也是ReentrantLock实现可重入的原因
            else if (current == getExclusiveOwnerThread()) {
                int nextc = c + acquires;
                //可以看出源码设计者的周到,考虑到了锁溢出的情况
                if (nextc < 0) 
                    throw new Error("Maximum lock count exceeded");
                //将当前线程持有的锁+1
                setState(nextc);
                return true;
            }
            return false;
        }

2.3 公平锁获取资源

lock():也是阻塞的。与非公平锁的区别是,不能直接通过CAS修改state,而是直接走AQS.acquire()。

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

tryAquire():与非公平锁类似,AQS.acquire()会调用这个钩子方法。只不过多判断了hasQueuedPredecessors(),判断当前节点在等待队列中是否有前驱节点,如果有,则说明有线程比当前线程更早的请求资源,根据公平性,当前线程请求资源失败;如果当前节点没有前驱节点,才有做后面的逻辑判断的必要性。

        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有3中获取锁的方法,lock(),tryLock(),lockInterruptibly()。

3.1 tryLock()--尝试获取资源

tryLock():走的还是sync的方法,在指定时间内获取锁,直接返回结果。

    public boolean tryLock(long timeout, TimeUnit unit)
            throws InterruptedException {
        return sync.tryAcquireNanos(1, unit.toNanos(timeout));
    }

tryAcquireNanos():如果调用tryLock的规定时间内尝试方法,就会调用该方法,先判断是否中断,然后尝试获取资源,否则进入AQS.doAcquireNanos()(这个方法在上篇文章有解释)。在规定时间内自旋拿资源,拿不到则挂起再判断是否被中断。

    public final boolean tryAcquireNanos(int arg, long nanosTimeout)
            throws InterruptedException {
        if (Thread.interrupted())
            throw new InterruptedException();
        return tryAcquire(arg) ||
            doAcquireNanos(arg, nanosTimeout);
    }

3.2 lockInterruptibly()--获取锁时响应中断

lockInterruptibly():交给了调度者sync执行。

    public void lockInterruptibly() throws InterruptedException {
        sync.acquireInterruptibly(1);
    }

acquireInterruptibly():当尝试获取锁失败后,就进行阻塞可中断的获取锁的过程。调用AQS.doAcquireInterruptibly()(这个方法在上篇文章也有详细解释)。

    public final void acquireInterruptibly(int arg)
            throws InterruptedException {
        if (Thread.interrupted())
            throw new InterruptedException();
        if (!tryAcquire(arg))
            doAcquireInterruptibly(arg);
    }

四、释放资源(锁)

公平锁与非公平锁的释放都是一样的。通过前面的阅读,可以知道,ReentrantLock.release()调用的是sync.release(1)。本质还是进入AQS.release(1),下面看看其中的tryRelease()这个钩子方法如何实现。

Sync释放资源

tryRelease():尝试释放锁,彻底释放后返回true。

        protected final boolean tryRelease(int releases) {
            int c = getState() - releases;
            //释放锁必须保证当前线程是锁的持有者
            if (Thread.currentThread() != getExclusiveOwnerThread())
                throw new IllegalMonitorStateException();
            boolean free = false;
            //如果释放后状态值为0,则彻底释放,持有者置空
            if (c == 0) {
                free = true;
                setExclusiveOwnerThread(null);
            }
            //设置释放后的state
            setState(c);
            return free;
        }

五、ReentrantLock的相关面试题

1)ReentrantLock是如何实现可重入的?
不管是公平锁还是非公平锁,在获取锁时调用的tryAcquire()方法,获取成功后会setExclusiveOwnerThread(current)。将本线程设置为主人,之后每次调用tryAcquire()时,发现当前线程就是主人,直接返回true。

2)简述公平锁与非公平锁的区别?
获取锁的顺序与请求锁的时间顺序一致就是公平锁,反之则为非公平锁。公平锁每次都是从同步队列中的第一个节点获取到锁,而非公平性锁则不一定,有可能刚释放锁的线程能再次获取到锁。
公平锁为了保证时间上的绝对顺序,需要频繁的上下文切换,而非公平锁会降低一定的上下文切换,降低性能开销。因此,ReentrantLock默认选择的是非公平锁,则是为了减少一部分上下文切换,保证了系统更大的吞吐量。

3)AQS中有哪些资源访问模式?区别?
独占模式和共享模式。
只有一个线程才能持有这个锁就是独占模式,由Node节点中的nextWait来标识。
ReentrantLock就是一个独占锁;而WriteAndReadLock的读锁则能由多个线程同时获取,但它的写锁则只能由一个线程持有,因此它使用了两种模式。

4)为什么ReentrantLock.lock()方法不能被其他线程中断?
因为当前线程前面可能还有等待线程,在AQS.acquireQueued()的循环里,线程会再次被阻塞。parkAndCheckInterrupt()返回的是Thread.interrupted(),不仅返回中断状态,还会清除中断状态,保证阻塞线程忽略中断。

总结

其实看完AQS源码后,ReentrantLock就是个弟弟。在实现上其实并不复杂,实现了AQS的独占模式。希望看完之后能对可重入锁,响应中断锁有更深入的理解。

文章若有不当之处,欢迎评论指出~
如果喜欢我的文章,欢迎关注知乎专栏Java修仙道路~

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值