深入ReentrantLock源码与解读

学习忠告

ReentrantLock的源码涉及到的类比较多,如果想真正掌握ReentrantLock的原理,建议独自去阅读源码,会有更好的效果。


课前小菜

我们都知道ReentrantLock简称(可重入锁),我们也知道ReentrantLock可以实现非公平锁和公平锁机制(转载的一篇公平锁和非公平机制)。默认(指无参构造得到的ReentrantLock实例)是非公平锁,通过有参构造器传入一个boolean值,如果传入true则是公平锁。

源码见真章:

深入ReentrantLock源码与解读


在阅读ReentrantLock前,我们将ReentrantLock的整体先掌握个大概,然后将大的一块逐步分成一个个小块,进一步掌握ReentrantLock源码的原理。

针对读者的疑问

为什么要阅读源码?掌握怎么使用不就行了?
ReentrantLock很简单!?

笔者的回答

第一个问题:读源码的必要性回答

为什么要阅读源码,如果只是做一个api调用工程师,那么源码似乎无多大用处。但对于读过源码的小伙伴我相信他们都会和我有一样的感受(发自内心的感受、由内到外的透露):我们不一样!~~~
读过源码的小伙伴对ReentrantLock的原理成竹在胸
有没有读过源码区别就在于两者的基础不一样,读源码有助于个人的成长!也能建立自信心。

第二个问题:ReentrantLock简不简单?

这个问题仁者见仁智者见智。

读ReentrantLock的源码 说难也难,说不难也不难。真正区别在于你有没有耐心去读下去,遇到困难时是选择放弃,还是坚持磨刀霍霍不放弃!

话不多说,我们省点笔墨灌鸡汤和扯皮。


哒哒哒 开课啦!

为了省篇幅和排版,以下知识点我大多采用链接方式引用我之前写过的一些文章,如果是哪里自己不是很熟悉的可以先阅读我之前的文章(有我的一些总结)。同时自己也通过搜索引擎查阅一下相关资料,我们争取每一个知识点都不要错过!

第一个知识点:可重入锁和不可重入锁的区别
第二个知识点:公平锁和不公平锁的区别
第三个知识点:LockSupport源码和原理以及坑
第四个知识点(非常重要但对于本篇文章非必须掌握):AbstractQueuedSynchronizer源码和原理

ReentrantLock的源码涉及到的类是比较多的,要想真正掌握下来需要花一段时间好好消化。
首先我们按照常规逻辑去跟踪一下源码。
常规的逻辑是:

逻辑一:先lock(),然后unlock()

我们看下lock()到底做了哪些事情

下面是ReentrantLock内部类的整体结构

public class ReentrantLock implements Lock, java.io.Serializable {
    private final Sync sync; // 同步器
    /**
     * Sync是公平锁和非公平锁的父类
     */
    abstract static class Sync extends AbstractQueuedSynchronizer {
    ...//省略部分代码,具体要用到的时候我再粘出来。先有一个整体概念
    }
    /**
     * 非公平锁
     */
    static final class NonfairSync extends Sync {
    ...//省略部分代码
    }
     /**
     * 公平锁
     */
    static final class FairSync extends Sync {
    ...//省略部分代码
    }
    /** 无参构造方法
     * 默认创建一个非公平锁
     */
    public ReentrantLock() {
        sync = new NonfairSync();
    }
    /** 带布尔值的有参构造方法
     * true则是公平锁,false则是非公平锁
     */
    public ReentrantLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
    }

	/**
     * 尝试加锁
     * 如果没有线程加锁则立即将锁计数记为1
     * 如果是同一个线程调用这个方法则计数+1
     * 如果已经被其它线程上锁了,则禁止当前线程调度。知道其它线程释放了锁
     */
    public void lock() {
        sync.lock();
    }
    /**
     * 释放锁
     */
    public void unlock() {
        sync.release(1);
    }
}

假设我们创建的是非公平锁NonfairSync
那么成员变量被赋值sync= new NonfairSync();
当我们调用ReentrantLock的lock()的时候回发现实际上调用的是内部类NonfairSync中的lock()而这个lock方法是由父类Sync继承过来的。这个时候我们将Sync的lock方法粘贴出来看下

/**
 * Sync是公平锁和非公平锁的父类
 */
abstract static class Sync extends AbstractQueuedSynchronizer {
	@ReservedStackAccess
    final void lock() {
        if (!initialTryLock())
            acquire(1);//这个方法来自父类AbstractQueuedSynchronizer
    }
    // 尝试加锁,由子类实现具体功能
    abstract boolean initialTryLock();
    
}

会发现判断需要调用子类NonfairSync的initialTryLock()返回值
这个时候粘贴除非公平锁的这个方法做了哪些事情

static final class NonfairSync extends Sync {
    //初始化锁
    final boolean initialTryLock() {
        Thread current = Thread.currentThread();        // 获取当前线程
        if (compareAndSetState(0, 1)) {   // 如果之前没有加锁,则将state更新为1,并进行加锁 setExclusiveOwnerThread(current);
            setExclusiveOwnerThread(current);// 将当前线程设置为独占意思就是加锁
            return true;                                // 加锁成功
        } else if (getExclusiveOwnerThread() == current) {// 之前加过锁,相当于重入锁(也意味着当前线程本来就获得到了锁),第二次进入则进入代码块
            int c = getState() + 1;  // 将锁计数+1,相当于多次加锁,每加一次锁就会+1
            if (c < 0) // 小于0,超过int的上限导致变成负数,就抛异常
                throw new Error("Maximum lock count exceeded");
            setState(c);//更新state值
            return true; //加锁成功
        } else //这个分支说明当前线程所操作的资源已经被加锁了,需要等待释放锁后获得锁。所以返回false
            return false; // 加锁失败
    }
}

通过阅读代码会发现这个initialTryLock()内部由3个分支,分别是处理
第一次加锁
重入锁
其它线程尝试加锁

这个逻辑就是判断当前线程是第一个获得到锁的线程?还是获得到锁的线程再一次获得锁?还是其它线程想要获得锁?三种情况。

他们的作用好比我之前文章中提到的:可重入锁与不可重入锁的区别
如果是重入锁,则通过这个state成员属性记录加锁次数,想要防止死锁现象,就得加几次锁就得解几次锁。也就是最终将这个state更新为0。


这里由于我们是第一次进入lock()也就是第一个分支返回true,我们回到之前得代码会发现由于!true也就是不执行if的代码块acquire(1);

@ReservedStackAccess
final void lock() {
    if (!initialTryLock()) //最终为false
        acquire(1);//这个方法来自父类AbstractQueuedSynchronizer
}

那么意味着我们第一次加锁成功了!


在上面的代码中我们遇到了一行未知的方法setExclusiveOwnerThread(current);。我们深入这个方法看下这个方法到底在哪里,做了什么事情。
通过追踪发现是AQS的父类AbstractOwnableSynchronizer中定义的final方法

public abstract class AbstractOwnableSynchronizer implements java.io.Serializable {
    /**
     * 成员变量
     */
    private transient Thread exclusiveOwnerThread;
    
	// 发现是final修饰的方法,不可以被继承而且里面只有一行代码
	protected final void setExclusiveOwnerThread(Thread thread) {
        exclusiveOwnerThread = thread;
    }
}

对于这类方法我们只需要知道它是做什么的就行了。通过英文注释翻译大致的意思就是将传入的线程设置为独占线程(实际也是Thread的实例,只是做一个标记而已,表示这个线程是独占的,相当于加锁的含义)
回过头来,第一次加锁我们加锁成功了。那么其它线程执行同一段加锁代码段会经过哪些逻辑呢?

思考一个问题:并发访问加锁代码段本质上到底做了什么事情?

大胆的猜测加锁的本质。

深入底层

线程和锁都是比较抽象的概念,为了更好的理解,我们读一小段加锁的字节码,然后再结合上面分析的过程,相互对照和验证。

一段简单的加锁代码段

测试代码类的源码

import java.util.concurrent.locks.ReentrantLock;

public class ReentrantLockTest {

    static ReentrantLock lock = new ReentrantLock();

    public static void main(String[] args) {
        try {
            lock.lock();
            System.out.println("这是一段加锁代码段");
        } finally {
            lock.unlock();
        }
    }

}
通过javap -c ReentrantLockTest.class反编译得到(去除了部分字节码)
public class com.example.demo.test.ReentrantLockTest {
  static java.util.concurrent.locks.ReentrantLock lock;

  public com.example.demo.test.ReentrantLockTest();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  public static void main(java.lang.String[]);
    Code:
       0: getstatic     #7                  // Field lock:Ljava/util/concurrent/locks/ReentrantLock;
       3: invokevirtual #13                 // Method java/util/concurrent/locks/ReentrantLock.lock:()V
       6: getstatic     #7                  // Field lock:Ljava/util/concurrent/locks/ReentrantLock;
       9: invokevirtual #17                 // Method java/util/concurrent/locks/ReentrantLock.unlock:()V
      12: goto          24
      15: astore_1
      16: getstatic     #7                  // Field lock:Ljava/util/concurrent/locks/ReentrantLock;
      19: invokevirtual #17                 // Method java/util/concurrent/locks/ReentrantLock.unlock:()V
      22: aload_1
      23: athrow
      24: return
    Exception table:
       from    to  target type
           0     6    15   any
}

我们很容易得知源码的lock.lock()对应着main方法中的Code标号为3的3: invokevirtual #13
invokevirtual意思就是调用实例方法的指令
lock.unlock()方法对应Code标号为9的代码,又由于lock.unlock()放在finally中,即使try中出异常也会再次执行finally块中的代码,也就是说还会执行解锁,防止线程死锁。
同时也会发现Code 9和19是一样的指令并且都是#17(调用的地址或者称为行号)

根据字节码的显示,我们就更加理解了lock()和unlock()的作用。

总结:

ReentrantLock就像是站岗的交警,它指挥着线程的通行,只不过只能允许一俩汽车通行。而有的汽车通过后,可能绕了一个圈,接着又通行了。达到了可重入锁的效果。
而有的线程则由于执行到lock()时被拦住了,不让通行(因为没有获得到锁,就必须等待)。底层是将线程进行自旋,就是不断的原地踏步(本质就是死循环的含义,但又可以退出循环。需要等其它线程释放锁,才又机会获得锁并退出自旋)


讲了那么多,那么大致可重入锁的lock()我们基本上是理解了,而且也跟踪了一下源码。

同理,unlock()方法,你也可以自己分析出来,即使不用看源码,大致也能猜到,有哪些步骤是一定要走的

比如:执行一次unlock()state的值就得state= state -1。相当于重入锁释放一次锁,计数-1。

这里面无非就是有一个可重入锁的概念夹杂在其中。

最后的寄语

此番分析源码并没有将ReentrantLock的所有源码都解读完毕,ReentrantLock涉及的内容比较多,一篇文章完全不够讲。因此我只是站在一个角度将源码浅略的分析了一下。真正想要掌握ReentrantLock就真的需要自己去发掘,反问一下ReentrantLock难道 就只有lock()、unlock()方法?其它方法你都用过?


当你分析完ReentrantLock后,我希望你能更深入挖掘源码。比如AQS的源码。AbstractQueuedSynchronizer是juc底层的框架,如果想要更深入的理解多线程,那么就需要掌握aqs的原理和源码。

有关 AbstractQueuedSynchronizer的源码和原理 这篇文章我会补上。也可以在juc或者jdk源码中能找到

匿名

发表评论

匿名网友