并发——深入分析ReentrantLock的实现原理

一、前言

  之前花了点时间研究了一下并发包下的一个重要组件——抽象队列同步器AQS,在并发包中,很多的类都是基于它实现的,包括Java中常用的锁ReentrantLock。知晓了AQS的实现原理,那理解ReentrantLock的实现就非常简单了,因为它的锁功能的实现就是由AQS实现的,而它的工作仅仅是重写了一些AQS中的相关方法,并使用其中的模板方法进行加锁解锁。今天这篇博客就来从源码的角度分析一下ReentrantLock的实现。


二、正文

2.1 抽象队列同步器AQS

  在说ReentrantLock前,必须要先提一下AQSAQS全称抽象队列同步器(AbstractQuenedSynchronizer),它是一个可以用来实现线程同步的基础框架。当然,它不是我们理解的Spring这种框架,它是一个类,类名就是AbstractQuenedSynchronizer,如果我们想要实现一个能够完成线程同步的锁或者类似的同步组件,就可以在使用AQS来实现,因为它封装了线程同步的方式,我们在自己的类中使用它,就可以很方便的实现一个我们自己的锁。

  AQS的实现相对复杂,无法通过短短的几句话将其说清楚,我之前专门写过一篇分析AQS实现原理的博客:并发——抽象队列同步器AQS的实现原理

  在阅读下面的内容前,请一定要先学习AQS的实现原理,因为ReentrantLock的实现非常简单,完全就是依赖于AQS的,所以我以下的描述均建立在已经理解AQS的基础之上。可以阅读上面推荐博客,也可以自己去查阅相关资料。


2.2 ReentrantLock的实现原理

  我们先简单介绍一下ReentrantLock的实现原理,这样方便我们下面阅读它的源码。前面也说过,ReentrantLock基于AQS实现,AQS模板方法acquirerelease等,已经实现了加锁和解锁的操作,而使用它的类只需要重写这些模板方法中调用的方法,比如tryAcquiretryRelease等,这些方法通过修改AQS的同步状态state来加锁解锁。AQS的同步状态state是一个int类型的值,根据不同的值,就可以判断当前锁的状态,同时修改这个值就是加锁和解锁的方式。

  使用AQS的一般方式是以内部类的形式继承AQSReentrantLock也是这么实现的,在它的内部,有三个AQS的派生类:

  1. 首先第一个派生类名字叫做Sync,这是一个抽象类,直接继承自AQS,其中定义了一些通用的方法;
  2. 第二个派生类名字叫做NonfairSync,它继承自Sync,实现的是一种非公平锁
  3. 第三个派生类名字叫FairSync,它也继承自Sync,实现的是一种公平锁

  ReentrantLock就是通过NonfairSync对象或者FairSync对象来保证进行线程同步的。而这三个类中编写的方法,实际上就是修改同步状态的方式。当state的值为0时,表示当前并没有线程获取锁,而每获取一次锁,state的值就会+1,释放一次锁,state-1。下面我们就通过这三个类的源码来具体看一看吧。


2.3 Sync类源码解析

  我们直接来看看Sync类中的方法吧,Sync类中的方法不少,我只拿出其中比较重要的几个来讲一讲:

abstract static class Sync extends AbstractQueuedSynchronizer {
	/** 定义一个加锁的抽象方法,由子类实现 */
    abstract void lock();

    /**
     * 此方法的作用是以非公平的方式尝试获取一次锁,获取成功则返回true,否则返回false;
     * 需要注意,AQS的获取锁,实际上就是修改同步状态state的值。
     * 这里有个疑惑,既然是非公平地获取锁,那这个方法为什么不写在NonfairSync类中?
     * 因为ReentrantLock有一个方法tryLock,即尝试获取一次锁,调用tryLock方法时,
     * 无论使用的是公平锁还是非公平锁,实际上都需要尝试获取一次锁,也就是调用这个方法,
     * 所以这个方法定义在了父类Sync中
    */
    final boolean nonfairTryAcquire(int acquires) {
        // 获取当前正在运行的线程
        final Thread current = Thread.currentThread();
        // 获取同步状态state的值,state定义在父类AQS中,
        int c = getState();
        // 若当前state的值为0,表示还没有线程获取锁,于是当前线程可以尝试获取锁
        if (c == 0) {
            // compareAndSetState方法通过CAS的方式修改state的值,
            // 实际上就是让state从0变为1,因为acquires的值就是1,
            // 每次有线程获取了锁时,同步状态就+1
            if (compareAndSetState(0, acquires)) {
                // 若compareAndSetState方法返回true,表示修改state成功,
                // 则调用setExclusiveOwnerThread方法将当前线程记录为占用锁的线程
                setExclusiveOwnerThread(current);
                return true;
            }
        }
        // 若以上c == 0不满足,则表示已经有线程获取锁了,
        // 于是调用getExclusiveOwnerThread方法获取当前正在占用锁的线程,
        // 然后和当前线程比较,若当前线程就是占用锁的线程,则当前线程不会被阻塞,
        // 可以再次获取锁,从这里可以看出,ReentrantLock是一个可重入锁
        else if (current == getExclusiveOwnerThread()) {
            // 计算当前线程获取锁后,state的值应该是多少,实际上就是让state + 1
            int nextc = c + acquires;
            // 如果nextc小于0,则保存,因为理论上同步状态是不可能小于0的
            if (nextc < 0) // overflow
                throw new Error("Maximum lock count exceeded");
            // 使用上面计算出的nextc更新state的值,这里需要注意一点
            // setState不像compareAndSetState方法,setState方法并不保证操作的原子性
            // 这里不需要保证原子性,因为这里线程已经获取了锁,所以不会有其他线程进行操作
            setState(nextc);
            // 返回true表示加锁成功
            return true;
        }
        // 若以上条件均不满足,表示有其他线程获取了锁,当前线程获取锁失败
        return false;
    }

    
    /**
     * 此方法是的作用是尝试释放锁,其实也就是让state的值-1
     * 这个方法是一个通用的方法,不论使用的是公平锁还是非公平锁
     * 释放锁时都是调用此方法修改同步状态
     */
    protected final boolean tryRelease(int releases) {
        // getState方法获取state的值,并与参数相减,计算释放锁后state的值
        // 在ReentrantLock中其实就是-1
        int c = getState() - releases;
		// 判断当前线程是不是占用锁的线程,若不是则抛出异常
        // 因为只有占用了锁的线程才可以释放锁
        if (Thread.currentThread() != getExclusiveOwnerThread())
            throw new IllegalMonitorStateException();
        
        // 变量free用来标记锁释放真正的被释放,因为ReentranLock是一个重入锁
        // 获取锁的线程可以多次获取锁,只有每一次获取都释放,锁才是真正的释放
        boolean free = false;
        // 判断c的值是否是0,只有c的值是0,也就是state的值为0时
        // 才说明当前的线程在这次释放锁后,锁真正的处于没有被使用的状态
        if (c == 0) {
            // 若满足此条件,则free标记为true,表示锁真的被释放了
            free = true;
            // 然后标记当前占用锁的线程为null,也就是没有线程占用锁
            setExclusiveOwnerThread(null);
        }
        // 将c的值更新同步状态state
        setState(c);
        return free;
    }
	
    /** 此方法判断当前线程是不是获取锁的线程 */
    protected final boolean isHeldExclusively() {
        // getExclusiveOwnerThread方法返回当前正在占用锁的线程
        // 于当前的运行的线程进行比较
        return getExclusiveOwnerThread() == Thread.currentThread();
    }
}

  以上就是Sync类的实现。其实Sync中的方法不仅仅只有上面这几个,但是剩下的那些方法都是一些零零碎碎,对我们理解ReentrantLock没有太大帮助的方法,所以这里就不一一列举了。从上面的方法实现中,我们可以知道以下信息:线程获取锁的方式实际上就是让同步状态state的值增加,而释放锁的方式就是让state的值减小;而且ReentrantLock实现的是可重入锁,已经获取锁的线程可以不受阻碍地再次获取锁,state的值可以不断增加,而释放锁时,只有state的值减小为0,锁才是真正被释放


2.4 NonfairSync类源码解析

下面我们再看看第二个内部类NonfairSync,它实现的是非公平锁

/**
 * 此类继承自Sync,它实现的是非公平锁
 */
static final class NonfairSync extends Sync {
    private static final long serialVersionUID = 7316153563782823691L;
    
    /**
     * 在父类Sync中定义的lock方法,在子类中实现
     */
    final void lock() {
        // 调用compareAndSetState方法,企图使用CAS机制将state的值从0修改为1
        // 若state的值不为0,表示锁已经被其他线程获取了,则当前线程将获取锁失败
        // 或者state的值一开始是0,但是在当前线程修改的过程中,被其他线程修改了,
        // 也会返回false。若修改state失败,则就需要执行下面的acquire方法
        if (compareAndSetState(0, 1))
            setExclusiveOwnerThread(Thread.currentThread());
        // acquire方法是AQS定义的模板方法,这个方法会调用tryAcquire尝试获取锁,
        // 而tryAcquire方法是由子类实现的,也就是下面那个方法;
        // 若调用tryAcquire获取锁失败,则AQS会将线程封装成一个节点Node
        // 丢入AQS的同步队列中排队(这个具体实现请参考AQS的实现博客)
        // 归根到底,这个方法就是让线程获取锁,不断尝试,直到成功为止.
        // 注意这里传入的参数是1,表示加锁实际上就是让state的值+1
        else
            acquire(1);
    }

    
    /** 
     * 此方法tryAcquire在AQS的模板方法中被调用,它的作用就是尝试获取一次锁,
     * 也就是尝试修改一次同步状态state;
     * 不同的实现类根据不同的需求重写tryAcquire方法,就可以按自己的意愿控制加锁的方式
     * AQS就是通过这种方式来提供给其他类使用的
     */
    protected final boolean tryAcquire(int acquires) {
        // 此处直接调用了父类Sync中,非公平地获取一次锁的nonfairTryAcquire方法
        return nonfairTryAcquire(acquires);
    }
}

  上面就是NonfairSync类完整的代码,并没有删减,可以看出,非常的简短。实现了Sync类中定义的lock方法,同时重写了tryAcquire方法,供AQS的模板方法acquire调用,且tryAcquire的实现仅仅是调用了Sync中的nonfairTryAcquire方法。为了有助于我们理解,我们还是来看看AQSacquire方法的代码吧:

public final void acquire(int arg) {
    // 这里首先调用tryAcquire方法尝试获取一次锁,在AQS中这个方法没有实现,
    // 而具体实现是在子类中,也就是调用的是NonfairSync的tryAcquire方法,
    // 若方法返回true,表示成功获取到锁,于是后面代码都不会执行了,
    // 否则,将先执行addWaiter方法,这个方法的作用是将当前线程封装成为一个Node节点,
    // 加入到AQS的同步队列的尾部,同时将返回这个Node节点,并传入acquireQueued方法
    // acquireQueued方法的作用就是让当前线程阻塞,直到成功获取到锁才会从这个方法返回
    // acquireQueued会返回这个线程在等待的过程中是否被中断,若被中断,
    // 则调用selfInterrupt方法真正执行中断。
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}

  为什么说NonfairSync是非公平锁?我们可以看到,在NonfairSynclock方法中,一个线程尝试去获取锁前,并不会判断在它之前是否有线程正在等待获取锁,而是直接尝试调用compareAndSetState方法获取一次锁,若获取失败,进入acquire方法,在这个方法中又会调用tryAcquire方法获取一次锁。此时若再次获取失败,才会进行进入同步队列中排队,这个过程中插了两次队,所以NonfairSync是非公平锁。


2.5 FairSync类源码解析

  下面我们来看看最后一个内部类FairSync,它实现的是公平锁,也就是线程按照先来后到的顺序获取锁,而不会插队:

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

    /** 实现父类的lock方法,加锁 */
    final void lock() {
        // 直接调用AQS的模板方法acquire进行加锁,调用这个方法后,线程若没有获取锁
        // 则会被阻塞,直到获取了锁后才会返回。这里需要注意一点,和NonfairSync中的lock不同
        // 这里直接调用acquire,而不会先调用一次compareAndSetState方法获取锁
        // 因为FairSync是公平锁,所以不会执行这种插队的操作.
        // 注意这里传入的参数是1,表示加锁实际上就是让state的值+1
        acquire(1);
    }

    /** 
     * 和NonfairSync一样,重写AQS的tryAcquire方法,若使用的是FairSync,
     * 则acquire中将调用此tryAcquire方法,尝试获取一次锁
     */
    protected final boolean tryAcquire(int acquires) {
        // 首先获取当前正在执行的线程
        final Thread current = Thread.currentThread();
        // 记录同步状态
        int c = getState();
        // 若state的值为0,表示现在没有线程占用了锁,于是当前线程可以尝试获取锁
        if (c == 0) {
            // 尝试获取锁前,先调用hasQueuedPredecessors方法,这个方法是判断
            // 是否有其他线程正在排队尝试获取锁,若有,方法将返回true,那为了公平性,
            // 当前线程不能获取锁,于是直接结束,否则调用compareAndSetState修改state
            // 若修改成功,调用setExclusiveOwnerThread方法将自己设置为当前占用锁的线程
            if (!hasQueuedPredecessors() &&
                compareAndSetState(0, acquires)) {
                setExclusiveOwnerThread(current);
                return true;
            }
        }
        // 若state不等于0,则表示当前锁已经被线程占用,那此处判断占用锁的线程是否是自己
        // 若是,则当前线程可以再次获取锁,因为ReentrantLock实现的是可重入锁,
        else if (current == getExclusiveOwnerThread()) {
            // 计算当前线程再次获取锁后,state的值将变为多少,此处实际上就是 + 1
            int nextc = c + acquires;
            // 理论上state的值不可能小于0,于是若小于0,就报错
            if (nextc < 0)
                throw new Error("Maximum lock count exceeded");
            // 修改state的值为上面计算的新值,此处不需要CAS操作保证原子性,
            // 因为当前线程已经获取了锁,那其他线程就不能修改state,所以这里可以放心修改
            setState(nextc);
            return true;
        }
        // 若以上条件均不满足,表示有其他线程占用了锁,则直接返回false
        return false;
    }
}

  FairSync的实现也比较简单。值得注意的是,因为FairSync实现的是公平锁,所以线程获取锁前,会先判断是否有在它之前尝试获取锁的线程在排队,若有,则当前线程不能插队,也需要进行排队,并且排在那些线程之后


2.6 ReentrantLock的成员属性与构造方法

  看完了内部类,下面就正式来看一看ReentrantLock是如何操作的吧,首先看一看它的成员属性和构造方法构造方法:

/** 记录使用的锁对象 */
private final Sync sync;

/** 默认构造方法,初始化锁对象,默认使用非公平锁 */
public ReentrantLock() {
    sync = new NonfairSync();
}

/** 参数为boolean类型的构造方法,若为false,使用非公平锁,否则使用公平锁 */
public ReentrantLock(boolean fair) {
    sync = fair ? new FairSync() : new NonfairSync();
}

2.7 ReentrantLock的加锁与解锁

  下面我就来看看ReentrantLock最重要的两个操作,加锁和解锁。

(1)获取锁的方法实现

/** 
 * 此方法让当前线程获取锁,若获取失败,线程将阻塞,直到获取成功为止 
 * 此方法不会响应中断,也就是在没有获取锁前,将无法退出
 */
public void lock() {
    // 直接调用锁对象的lock方法,也就是之前分析的内部类中的lock方法
    sync.lock();
}

/**
 * 此方法获取锁,和上面的方法类似,唯一的不同就是,调用这个方法获取锁时,
 * 若线程被阻塞,可以响应中断
 */
public void lockInterruptibly() throws InterruptedException {
    // 调用sync对象的acquireInterruptibly方法,这个方法定义在AQS中,
    // 也是AQS提供给子类的一个模板方法,内部也是通过tryAcquire获取锁,
    // 若获取失败,线程将被阻塞,但是此方法会检测中断信号,
    // 若检测到中断,将通过抛出异常的方式退出阻塞
    // 关于这个方法的具体实现,可以去参考AQS的相关博客,此处就不展开描述了
    sync.acquireInterruptibly(1);
}


/** 
 * 调用此方法尝试获取一次锁,不论成功失败,都会直接返回
 */
public boolean tryLock() {
    // 此处直接调用Sync类中的nonfairTryAcquire方法,
    // 这也就是为什么nonfairTryAcquire定义在父类Sync中,
    // 因为不论是使用公平锁还是非公平锁,都需要在此处调用这个方法
    return sync.nonfairTryAcquire(1);
}

(2)是否锁的方法实现

/**
 * 此方法用来释放锁
 */
public void unlock() {
    // 此处调用的是AQS的release方法,这个方法也是AQS提供的一个模板方法,
    // 在这个方法中,将调用子类重写的tryRelease方法尝试释放锁,若释放成功
    // 则会唤醒等待队列中的下一个线程,让它停止阻塞,开始尝试获取锁,
    // 关于这个方法的具体实现,可以参考我之前推荐的AQS源码分析博客。
    // 这里需要注意,传入的参数是1,表明释放锁实际上就是让state的值-1
    sync.release(1);
}

  以上就是ReentrantLock加锁和解锁的方法,出乎意料,非常的简单,每个方法都只有一句代码,调用AQS类中提供的模板方法。这就是AQS的好处,AQS封装了线程同步的代码,我们只需要在类中使用它,就能很简单的实现一个锁。所以我前面才说,在看ReentrantLock前,一定要先学习AQS,理解了AQS,理解ReentrantLock就完全没有难度了。

  上面这些就是ReentrantLock中的关键方法,其实除了这些方法之外,还有许多其他的方法,但是那些方法并不是关键,实现也都非常简单,基本上就是一句代码,可以自己直接去阅读源码,我这里就不一一列举了。


三、总结

  经过上面的分析,我们会发现,ReentrantLock的实现原理非常的简单,因为它是基于AQS实现的,复杂性都被封装在了AQS中,ReentrantLock仅仅是它的使用者,所以,学习ReentrantLock实际上就是学习AQSAQSJava并发中的重要组件,很多的类都是基于它实现的,比如非常常用的CountDownLatchAQS也是面试中的常考题,所以一定要好好研究。此处再次推荐我写的AQS解析博客:并发——抽象队列同步器AQS的实现原理


四、参考

  • JDK1.8源码
posted @ 2020-04-13 04:15  特务依昂  阅读(666)  评论(0编辑  收藏  举报