ReentrantLock源码分析

本文详细介绍了ReentrantLock的设计理念和实现过程,对比了它与synchronized的优缺点。通过分析DougLea如何解决synchronized的不足,如加锁代价大、不支持部分线程操作、不公平性等问题,展示了ReentrantLock如何通过AQS框架实现性能与公平性的平衡。文章还探讨了公平锁与非公平锁的实现、Condition的使用、中断与超时机制,并强调了阅读源码对理解类设计的重要性。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

前言

ReentrantLock这个类,相信大家多多少少在项目里都会去用到这个类,但我相信大部分人都没去研究过源码。我在这里把我学习这个类的一些经验和心得分享出来,希望对大家有所帮助,水平有限,文章中有错误的地方也请不吝指正,共同进步。

学习基础

这个类依旧是根据aqs框架去实现的,如果不知道什么是aqs的,可以去看看我写的aqs的学习笔记。独占锁共享锁,aqs是基础,关于aqs的部分,本文将不再赘述。

引导

关于ReentrantLock的源码解析,百度上没有一万个人写,也至少有八千个人写,大家随便百度一篇原创,我相信大家都会有很大收获。所以这篇文章,我想从产品设计的角度去猜测还原Doug Lea是如何开发出来ReentrantLock这个类的。

功能规划

我们假设自己就是Doug Lea,当前jdk是1.4版本。我们需要在源码里提供一个非常方便的加锁的工具类,名字已经想好了,就叫ReentrantLock,目的是开发者在使用的时候可以很方便的进行加锁,解锁。而此时java中只有一个synchronized关键字。既然我们要开发一个新的工具类,我们肯定是希望比synchronized更好用,覆盖的场景更广泛。所以我们首先分析synchronized的缺点(1.4版本的缺点):

  1. synchronized加锁的代价太大。
  2. synchronized不支持操作部分线程,释放锁的时候等同于唤醒全部。
  3. synchronized毫无公平性可言,我们需要公平,公平,还是TMD公平。
  4. 锁的粒度太粗,一个实例的多个synchronized方法不能同时被多个线程访问。
  5. synchronized在阻塞的过程中不可被中断。
  6. synchronized超时时会一直锁住,甚至会造成死锁。

设计

第一点:
早期synchronized获取不到锁就会被阻塞,而阻塞线程需要cpu从用户态转换到内核态,这是一个很消耗资源的操作,所以我们需要开发一个api级别的框架,这个框架就是aqs,aqs利用cas自旋和阻塞,多次cas获取不到资源的时候再去阻塞,被唤醒的线程再使用cas去获取资源,不是单纯的cas自旋或者阻塞,这样就同时兼顾了性能和效率。这样,我们开发了一个aqs框架作为ReentrantLock的基础,来解决synchronized加锁代价大的问题。同时也提供了一个可扩展的aqs框架。
(synchronized以前确实是很笨重, 但是在各个版本优化之后,现在的性能和ReentrantLock是相差无几的,官方建议是如果synchronized能实现你的需求,那么尽量使用synchronized,具体的优化大家可以百度一下。)

第二点:
我们想自定义规则让部分线程去争抢资源,让部分线程不去争抢资源。我们需要使用到aqs的node节点链表。但是我们知道,node队列是轮询唤醒全部,没有对线程做区分,我们只想要操作部分线程。于是我们想到一种方法:
我们根据我们的定义,把线程归类,然后把归好类的线程放到一个个单独的队列里,我们需要操作哪部分线程, 就把这部分线程所在的队列的所有线程,放到aqs队列里,剩下的事情我们交给aqs,这样我们就做到了操作部分线程,而不影响其他线程。

第三点
我们需要设计一个公平锁,但是非公平锁也肯定要有,因为非公平锁性能更好,而且大部分人可能并不关心公平性。所以我们的想法是在构造方法里放入一个Boolean参数,用户想要公平锁就传入true,想要非公平锁就传入false,同时在代码里对公平和非公平分开实现。

第四点
synchronized锁粒度太粗,这跟synchronized的实现机制有关,因为synchronized是jvm去实现的,是对对象和类的操控。所以synchronized方法获取的是对象锁或者类锁,当synchronized锁住实例或者类对象的时候,其他线程再去获取会被阻塞。而我们设计了ReentrantLock,这样我们需要几把锁,在我们的类里定义几个成员变量就可以了,各个ReentrantLock对象之间互不影响,这样这个问题也就不解自解了。

第五点
synchronized在阻塞的过程中是忽略中断的,只有在获取到锁的时候才会去响应中断。为了解决这个问题我们需要在ReentrantLock设计一个可以检测中断的lock方法, 我们暂且命名为lockInterruptibly()。
aqs中分为cas和阻塞唤醒两个过程,我们可以在自旋cas中加入检测中断的逻辑,这样我们在获取锁的部分过程中就做到了检测中断。

第六点
synchronized代码块中,如果碰到一段代码偶尔需要执行1分钟或者两分钟,那其他的线程获取锁等待的时间就太长了,那显然不是我们能接受的,所以我们在ReentrantLock设计一个根据我们设定时间去获取锁的操作,超过我们期望的时间就不再去获取锁, 直接返回结果。

设计工作已经做完了,我们接下来就是把我们的需求翻译成代码。

实现公平非公平(copy)

公平锁和非公平锁,区别只有在tryAcquire()方法上,所有会有很多可以共用的代码。为了代码美观,我们先写一个公平锁和非公平锁都可以继承的基类,这个类就是Sync,Sync的功能依赖aqs,所以我们要继承aqs。

	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
        }
    }

然后我们定义一个公平锁类和一个非公平锁类,继承这个Sync

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

这样,公平锁和非公平锁就实现了,他们俩唯一的区别就是公平锁在获取资源的时候,会调用hasQueuedPredecessors()方法判断一下是否存在等待队列,如果有就去乖乖排队,这样就保证了公平性,先来后到。

	public ReentrantLock() {
        sync = new NonfairSync();
    }
    public ReentrantLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
    }

我们用构造方法区控制使用者想要什么类型的锁,至此,公平和非公平我们已经实现。

实现操作部分线程

我们已经有了思路,定义一个个队列来分别存放线程,那么我们可以把这一个个线程定义为对象的一个属性,所以我们定义一个接口,来操作这些线程。这个类就是Condition

public interface Condition {

    void await() throws InterruptedException;
    
    void awaitUninterruptibly();

    long awaitNanos(long nanosTimeout) throws InterruptedException;

    boolean await(long time, TimeUnit unit) throws InterruptedException;

    boolean awaitUntil(Date deadline) throws InterruptedException;

    void signal();

    void signalAll();

同样,定义一个类来保存这个队列。

	public class ConditionObject implements Condition, java.io.Serializable {
        private static final long serialVersionUID = 1173984872572414699L;
        /** First node of condition queue. */
        private transient Node firstWaiter;
        /** Last node of condition queue. */
        private transient Node lastWaiter;
    }
	public Condition newCondition() {
        return sync.newCondition();
    }
    final ConditionObject newCondition() {
            return new ConditionObject();
    }

我们只需要调用newCondition()方法就可以获得一个保存一类线程的对象。ConditionObject对象中还有很多方法,我想单独写一篇文章来说明。
至此,操作部分线程的功能我们也实现了,这其实就是ReentrantLock的Condition场景的应用。

实现可中断

	public void lockInterruptibly() throws InterruptedException {
        sync.acquireInterruptibly(1);
    }
    public final void acquireInterruptibly(int arg)
            throws InterruptedException {
        // 获取锁之前校验一下线程的中断状态
        if (Thread.interrupted())
            throw new InterruptedException();
        if (!tryAcquire(arg))
            doAcquireInterruptibly(arg);
    }
    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判断至少会走两边,每走一遍都去判断一下线程的中断状态
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    throw new InterruptedException();
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

中断操作已经在代码里注释说明。

获取锁设定超时时间

	public boolean tryLock(long timeout, TimeUnit unit)
            throws InterruptedException {
        return sync.tryAcquireNanos(1, unit.toNanos(timeout));
    }
    public final boolean tryAcquireNanos(int arg, long nanosTimeout)
            throws InterruptedException {
        if (Thread.interrupted())
            throw new InterruptedException();
        return tryAcquire(arg) ||
            doAcquireNanos(arg, nanosTimeout);
    }
    private boolean doAcquireNanos(int arg, long nanosTimeout)
            throws InterruptedException {
        if (nanosTimeout <= 0L)
            return false;
        final long deadline = System.nanoTime() + nanosTimeout;
        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 true;
                }
                nanosTimeout = deadline - System.nanoTime();
                if (nanosTimeout <= 0L)
                    return false;
                if (shouldParkAfterFailedAcquire(p, node) &&
                    nanosTimeout > spinForTimeoutThreshold)
                    LockSupport.parkNanos(this, nanosTimeout);
                if (Thread.interrupted())
                    throw new InterruptedException();
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

代码很简单,就是获取锁和每次自旋cas过程前,判断一下是否超时,如果超时,直接返回false

思考

至此,ReentrantLock的大部分功能我们已经实现了,当你去翻阅源码的时候。你会发现,ReentrantLock里也就这么点东西,剩下的方法无非是开发者在开发时封装了一些公用的方法。大家对比一下我们平时开发的过程,流程是不是非常的相似:

  • 确定需求
  • 确定需求实现的方式
  • 书写代码
  • 对代码重构,封装公共方法

由此可见,一个类,我们只要知道了设计这个类的初衷是为了解决什么问题,然后我们再去看源码里作者是如何解决这个问题的,用什么方式去解决这个问题。
再深入一点我们可以研究一下,用这种方式去解决这个问题,但是代码为什么要这么写,这么写有什么好处?如果是我们来实现这个功能,我们的代码跟作者的代码孰优孰劣?一番比较下来,我们可以学习源码里对方法的优雅封装,对变量的合理使用以及一些对异常情况的处理方式,源码里真的是行行都是精髓。

写后感

自从开始写博客到现在,我发现我对于一件事情的梳理更快速和规范了,我觉得这有利于我思考,这得益于我每写一篇博客做的总结,这也是一种我认为的提升自己的方式。
比如这篇文章所讲的ReentrantLock和synchronized,在写这篇文章之前,如果你问我他们两个有什么区别,我的回答就是不知道,因为我不能条理清晰的去告诉你这两个的区别和优劣。也就是说原理我懂一点,但是我不知道如何组织语言。
现在如果你们再问我,那么我可以很自信的跟你说个一二三来。这篇文章中有我以前就懂的知识,也有我在写的时候一知半解而去学习的知识。我把这些知识用我的语言梳理脉络去讲给你们听,我自身也在一个高度学习的状态。赠人玫瑰,手有余香。如果大家到了一个学习的瓶颈期,不妨也试试写博客这种方式。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值