ReentrantLock中公平锁实现的源码解析(超多动图一看就会)

ReentrantLock

​ 首先我们先尝试使用ReentrantLock(下面简称RLock)来感受一下RLock的强大。之后再对其源码进行详细解读

​ 我们首先尝试创建两个线程来调用同一个方法,通过是否使用RLock来体验一下RLock的作用

不使用RLcok的情况

    public static void main(String[] args) {
      // 我们首先创建两个线程,并且让他们同时调用 doPrint() 方法
      Thread t1 = new Thread(Test::doPrint);
      t1.setName("t1");
      Thread t2 = new Thread(Test::doPrint);
      t2.setName("t2");

      t1.start();
      t2.start();

   }

   public static void doPrint() {
      System.out.println(Thread.currentThread().getName() + "已经被锁住了");
      try {
         Thread.sleep(5000);
      } catch (InterruptedException e) {
         e.printStackTrace();
      }
   }

​ 尝试执行之后会发现,将会同时打印***t1已经被锁住了***和***t2已经被锁住了***。这里虽然将线程睡眠了5s,但是由于是开启了两个线程去跑的,所以这里将会同时打印这两段文字之后再进行睡眠。

在这里插入图片描述

使用RLock的情况

尝试使用RLock并再doPrint()中加锁

static ReentrantLock lock = new ReentrantLock();

public static void main(String[] args) {
   Thread t1 = new Thread(Test::doPrint);
   t1.setName("t1");
   Thread t2 = new Thread(Test::doPrint);
   t2.setName("t2");

   t1.start();
   t2.start();

}

public static void doPrint() {
   lock.lock();
   System.out.println(Thread.currentThread().getName() + "已经被锁住了");
   try {
      Thread.sleep(5000);
   } catch (InterruptedException e) {
      e.printStackTrace();
   }
   lock.unlock();
}

​ 这时会发现t1先打印了***t1已经被锁住了***之后过了5秒才打印了***t2已经被锁住了***

在这里插入图片描述

​ 这也就是我们经常说的上锁。当线程t1获取锁之后,如果其他线程想获取锁,则需要在等待t1释放锁之后才可以获取到锁。打个比方,没有上锁的代码他就像是一个杂乱的菜市场,所有来买菜的人他就像是一个个的线程,而卖菜的摊主他就像一个cpu,菜市场买菜从不讲究先来后到。假设现在顾客1(线程t1)先来到了菜摊,他左顾右看迟迟不知道想买什么,这时又来了一个顾客2(线程2),他拿了一个白菜就跟摊主(CPU)说,这个白菜我买了哈,钱我转微信给你了,付款完顾客2(线程2)就走了,可是顾客1还在犹豫要买什么菜。这也真实反应了在没上锁的情况下,是不需要讲究先来后到这一说法的。

​ 上了锁的代码就像是一个整齐有序的菜摊(这里指公平锁,至于公平锁和非公平锁的区别下面会讲),每个来买菜的人(线程),都需要排队,讲究先来后到,先来的先得到摊主(CPU)的青睐,慢来的就得老老实实排队等待前面的人接受完服务。在用户角度看来,就是t1先执行了,执行完之后t2才开始执行。但是如果想知道底层到底是怎么实现的,我们需要从源码层面开始剖析

分析加锁过程

​ 还是先拿我们实际生活中的过程举例。假如我们有一个公厕,公厕的门上有一个指示灯,指示灯为红色表示里面有人,指示灯为绿色表示里面没人,一开始卫生间没有人,指示灯为绿色。这时来了第一个客户A,他先看了一下指示灯,发现灯是绿色的,他发现不需要排队,他就开门进去蹲大号了,这时指示灯变为红色。这时有第二个客户B也想过来上厕所,他先看了看指示灯,发现灯是红色的,他就发现自己需要排队等待了,所以他想看了看这个厕所的门前面是不是有其他人,由于他是第二个来上厕所的,所以他没有看他其他人在排队等待,这时客户B就站在了门前第一个位置。客户B等了等有点急,就推了推门暗示厕所里面的人,尝试获得坑位使用权,无奈门从里面反锁尝试失败,这时客户B就会在内心想着,这个人一定是要搞很久才出来了。这时客户B想拿出手机来一把王者荣耀,但是想了想会不会里面的会不会在差不多上完了,就又推了推门尝试获取坑位使用权,结果客户A还是没拉完,客户B索性就开了一把王者慢慢等A上完了。

​ 在现实生活中竞争坑位的过程其实和RLock的上锁过程也是十分相似。

​ RLock有一个状态值state,这就相当于我们卫生间的指示灯。当线程t1准备获取锁的时候,先判断state是否为0,这就相当于每个客户去上厕所都会先看看指示灯是不是绿色的,如果是绿色的才会进厕所。由于线程t1是第一个来尝试获取锁的,所以他顺利的拿到了锁,并将其state修改为1。这就相当于客户进去之后,指示灯会变为红色的过程。这时线程t2也准备来尝试获取锁,他先判断state是否为0,由于t1还没释放锁所以state的值1,导致线程2没办法马上获得到锁。这是线程t2就会想去开始排队,他首先判断在队列中有没有其他人,也就是相当于客户判断厕所门前是不是有其他人。但是这里有区别的是!线程t2不是将自己放在头节点!而是在头节点放了一个空值,自己放在头节点的下一个位置,来表示厕所里面还有个人,我是第二个使用厕所的人!(这里如果没听懂没关系,待会会结合源码来看和分析)。在形成队列之后线程t2会第二次去尝试获取锁,也就是修改state的值,如果还是得不到锁,那么t2就会将头节点的状态(waitStatus)修改为-1。这就像是客户会试探性的推推门来看看里面的人好了没有,如果里面的人还是没好,那么B就会觉得那里面的人估计要蹲很久了。之后,线程t2在准备park的时候,还会再尝试获取锁,因为park是一个代价比较大的行为,如果能避免则避免,这就像客户B会在准备开王者之前尝试去推推门来尝试获取蹲位所有权,因为开一局王者他是需要很长时间的,需要用户就开始全神贯注傻傻的等,不再做其他事的。只有t2第三次获取不到锁之后,才会真正的被park。

入队过程

在线程t2第一次获取不到锁的时候,开始创建队列

  1. t2先将自己变成一个节点,并判断此时需不需要排队。它发现tail尾指针为空,则确定当前没有人在排队。

  2. 创建一个新的空节点,并将头指针和尾指针都指向空节点。

  3. 将t2节点的pre设置为tail指向的空节点,并将tail指向t2节点,并将空节点的next指向t2节点

  4. 判断当前节点t2的前一个节点是否为头节点,如果是的话第二次尝试获取锁。如果获取不到锁则将头节点的waitStatus状态修改为-1

  5. 判断当前节点t2的前一个节点是否为头节点,如果是的话第三次尝试获取锁,并且如果前一个节点的waitStatus为-1,则当前线程park

在这里插入图片描述

源码分析

​ 人会说谎,但是代码不会。0就是0,1就是1。如果没有从源码层面来解析RLock,那我上面说的一切都有可能是假的,是错的!所以我们接下来从源码层面来逐行分析RLock。

第一次加锁过程

​ 接着第二节中使用RLock的例子来讲。第一次加锁过程中,由于锁的状态(state)为0,所以线程t1使用CAS修改完state的状态之后,表示上锁成功,则接着执行doPrint()方法中的打印等一系列操作。

​ 先看一张动图,证明我下面图片的所有代码,都是我ctrl+alt+b进去的。里面鼠标的指向,其实就是真实代码执行时候的情况。下面一步步分析

在这里插入图片描述

​ 在我们执行了加锁操作之后,就会调用ReentrantLock#lock()方法,接着会根据我们当前的锁是公平锁或者非公平锁来选择进入对应的lock方法(本文讲的都是以公平锁为参照)。

在这里插入图片描述

接着会调用FairSync#lock方法

在这里插入图片描述

紧接着会先走到acquire中的if判断,if中的tryAcquire(arg)就是尝试获取锁

在这里插入图片描述

接下来我们分析这个tryAcquire()方法

    protected final boolean tryAcquire(int acquires) {
        // 获取当前线程
        final Thread current = Thread.currentThread();
        // 获取锁的状态
        int c = getState();
        // 如果当前锁状态为0,则进入if语句
        if (c == 0) {
            /*
            1. hasQueuedPredecessors():这里首先判断当前队列是否有人在排队,这里由于是第一次进入,所以返回false(下面有这个方法的解析)
      			由于返回false,再加上取反,所以if判断会接着走
            2. compareAndSetState(0, acquires)
            	这里的acquires是前面传过来的,固定为1
            	这行代码的意思是使用cas将锁的状态由0修改为1。注意,这是一个原子操作,调用的是unsafe类的方法
            
            结果if都为true,进入if分支
            
            setExclusiveOwnerThread(current)
            	这个方法的含义是,将当前锁的所有者,标记为当前线程。
            	因为ReentrantLock是重入锁,所以标记锁的持有者非常有必要!(如果不理解重入锁的可以先不考虑)
            
            最后返回true
            */
            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;
    }
}

在进入tryAcquire(arg)方法之后,由于第一次进入返回的是true,而当前if分支加了取反,所以不会走if分支,直接返回

在这里插入图片描述

前面的代码也是接着返回。最后就会接着执行接下去的代码。这就是完整的第一次加锁过程。

第二次加锁过程

​ 第一次加锁的gif中并没有展示debug,这是因为如果使用了debug,那么t1线程就会卡住,从而t2线程有机可乘先去拿锁并上锁,这也导致了没办法用debug的方式来展示第一次加锁过程。不过我们可以用debug来展示第二次的加锁过程。

由于GIF太大超过上传上限,所以分成两个gif播放
1
在这里插入图片描述

到此,锁竞争过程也通过debug的方式完美的展现出来!

  • 3
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值