zoukankan      html  css  js  c++  java
  • (转)ReentrantLock实现原理及源码分析

    背景:ReetrantLock底层是基于AQS实现的(CAS+CHL),有公平和非公平两种区别。

    这种底层机制,很有必要通过跟踪源码来进行分析。

    参考

    ReentrantLock实现原理及源码分析

    源码分析

    接下来我们从源码角度来看看ReentrantLock的实现原理,它是如何保证可重入性,又是如何实现公平锁的。

      ReentrantLock是基于AQS的,AQS是Java并发包中众多同步组件的构建基础,它通过一个int类型的状态变量state和一个FIFO队列来完成共享资源的获取,线程的排队等待等。AQS是个底层框架,采用模板方法模式,它定义了通用的较为复杂的逻辑骨架,比如线程的排队,阻塞,唤醒等,将这些复杂但实质通用的部分抽取出来,这些都是需要构建同步组件的使用者无需关心的,使用者仅需重写一些简单的指定的方法即可(其实就是对于共享变量state的一些简单的获取释放的操作)。

      上面简单介绍了下AQS,详细内容可参考本人的另一篇文章《Java并发包基石-AQS详解》,此处就不再赘述了。先来看常用的几个方法,我们从上往下推。

    无参构造器(默认为非公平锁)

    public ReentrantLock() {
            sync = new NonfairSync();//默认是非公平的
        }

    sync是ReentrantLock内部实现的一个同步组件,它是Reentrantlock的一个静态内部类,继承于AQS,后面我们再分析。

      带布尔值的构造器(是否公平)

    public ReentrantLock(boolean fair) {
            sync = fair ? new FairSync() : new NonfairSync();//fair为true,公平锁;反之,非公平锁
        }

    看到了吧,此处可以指定是否采用公平锁,FailSync和NonFailSync亦为Reentrantlock的静态内部类,都继承于Sync

    小结

      其实从上面这写方法的介绍,我们都能大概梳理出ReentrantLock的处理逻辑,其内部定义了三个重要的静态内部类,Sync,NonFairSync,FairSync。Sync作为ReentrantLock中公用的同步组件,继承了AQS(要利用AQS复杂的顶层逻辑嘛,线程排队,阻塞,唤醒等等);NonFairSync和FairSync则都继承Sync,调用Sync的公用逻辑,然后再在各自内部完成自己特定的逻辑(公平或非公平)。

     NonFairSync(非公平可重入锁)

    static final class NonfairSync extends Sync {//继承Sync
            private static final long serialVersionUID = 7316153563782823691L;
            /** 获取锁 */
            final void lock() {
                if (compareAndSetState(0, 1))//CAS设置state状态,若原值是0,将其置为1
                    setExclusiveOwnerThread(Thread.currentThread());//将当前线程标记为已持有锁
                else
                    acquire(1);//若设置失败,调用AQS的acquire方法,acquire又会调用我们下面重写的tryAcquire方法。这里说的调用失败有两种情况:1当前没有线程获取到资源,state为0,但是将state由0设置为1的时候,其他线程抢占资源,将state修改了,导致了CAS失败;2 state原本就不为0,也就是已经有线程获取到资源了,有可能是别的线程获取到资源,也有可能是当前线程获取的,这时线程又重复去获取,所以去tryAcquire中的nonfairTryAcquire我们应该就能看到可重入的实现逻辑了。
            }
            protected final boolean tryAcquire(int acquires) {
                return nonfairTryAcquire(acquires);//调用Sync中的方法
            }
        }

    nonfairTryAcquire()

    final boolean nonfairTryAcquire(int acquires) {
                final Thread current = Thread.currentThread();//获取当前线程
                int c = getState();//获取当前state值
                if (c == 0) {//若state为0,意味着没有线程获取到资源,CAS将state设置为1,并将当前线程标记我获取到排他锁的线程,返回true
                    if (compareAndSetState(0, acquires)) {
                        setExclusiveOwnerThread(current);
                        return true;
                    }
                }
                else if (current == getExclusiveOwnerThread()) {//若state不为0,但是持有锁的线程是当前线程
                    int nextc = c + acquires;//state累加1
                    if (nextc < 0) // int类型溢出了
                        throw new Error("Maximum lock count exceeded");
                    setState(nextc);//设置state,此时state大于1,代表着一个线程多次获锁,state的值即是线程重入的次数
                    return true;//返回true,获取锁成功
                }
                return false;//获取锁失败了
            }

    简单总结下流程:(ps:获取锁的过程,也是共享锁的实现过程

        1.先获取state值,若为0,意味着此时没有线程获取到资源,CAS将其设置为1,设置成功则代表获取到排他锁了;

        2.若state大于0,肯定有线程已经抢占到资源了,此时再去判断是否就是自己抢占的,是的话,state累加,返回true,重入成功,state的值即是线程重入的次数;

        3.其他情况,则获取锁失败。

      来看看可重入公平锁的处理逻辑

      FairSync

    static final class FairSync extends Sync {
            private static final long serialVersionUID = -3000897897090466540L;
    
            final void lock() {
                acquire(1);//直接调用AQS的模板方法acquire,acquire会调用下面我们重写的这个tryAcquire
            }
    
            protected final boolean tryAcquire(int acquires) {
                final Thread current = Thread.currentThread();//获取当前线程
                int c = getState();//获取state值
                if (c == 0) {//若state为0,意味着当前没有线程获取到资源,那就可以直接获取资源了吗?NO!这不就跟之前的非公平锁的逻辑一样了嘛。看下面的逻辑
                    if (!hasQueuedPredecessors() &&//判断在时间顺序上,是否有申请锁排在自己之前的线程,若没有,才能去获取,CAS设置state,并标记当前线程为持有排他锁的线程;反之,不能获取!这即是公平的处理方式。
                        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()这个判断逻辑,即便state为0,也不能贸然直接去获取,要先去看有没有还在排队的线程,若没有,才能尝试去获取,做后面的处理。反之,返回false,获取失败。

      看看这个判断是否有排队中线程的逻辑

      hasQueuedPredecessors()

    public final boolean hasQueuedPredecessors() {
            Node t = tail; // 尾结点
            Node h = head;//头结点
            Node s;
            return h != t &&
                ((s = h.next) == null || s.thread != Thread.currentThread());//判断是否有排在自己之前的线程
        }

     需要注意的是,这个判断是否有排在自己之前的线程的逻辑稍微有些绕,我们来梳理下,由代码得知,有两种情况会返回true,我们将此逻辑分解一下(注意:返回true意味着有其他线程申请锁比自己早,需要放弃抢占)

      1. h !=t && (s = h.next) == null,这个逻辑成立的一种可能是head指向头结点,tail此时还为null。考虑这种情况:当其他某个线程去获取锁失败,需构造一个结点加入同步队列中(假设此时同步队列为空),在添加的时候,需要先创建一个无意义傀儡头结点(在AQS的enq方法中,这是个自旋CAS操作),有可能在将head指向此傀儡结点完毕之后,还未将tail指向此结点。很明显,此线程时间上优于当前线程,所以,返回true,表示有等待中的线程且比自己来的还早。

      2.h != t && (s = h.next) != null && s.thread != Thread.currentThread()同步队列中已经有若干排队线程且当前线程不是队列的老二结点,此种情况会返回true。假如没有s.thread !=Thread.currentThread()这个判断的话,会怎么样呢?若当前线程已经在同步队列中是老二结点(头结点此时是个无意义的傀儡结点),此时持有锁的线程释放了资源,唤醒老二结点线程,老二结点线程重新tryAcquire(此逻辑在AQS中的acquireQueued方法中),又会调用到hasQueuedPredecessors,不加s.thread !=Thread.currentThread()这个判断的话,返回值就为true,导致tryAcquire失败。

    ps:一句话就是检查当前线程前面有没有等待的线程

      最后,来看看ReentrantLock的tryRelease,定义在Sync中

    protected final boolean tryRelease(int releases) {
                int c = getState() - releases;//减去1个资源
                if (Thread.currentThread() != getExclusiveOwnerThread())
                    throw new IllegalMonitorStateException();
                boolean free = false;
                //若state值为0,表示当前线程已完全释放干净,返回true,上层的AQS会意识到资源已空出。若不为0,则表示线程还占有资源,只不过将此次重入的资源的释放了而已,返回false。
                if (c == 0) {
                    free = true;//
                    setExclusiveOwnerThread(null);
                }
                setState(c);
                return free;
            }

    总结

    ReentrantLock是一种可重入的,可实现公平性的互斥锁,它的设计基于AQS框架,可重入和公平性的实现逻辑都不难理解,每重入一次,state就加1,当然在释放的时候,也得一层一层释放。至于公平性,在尝试获取锁的时候多了一个判断:是否有比自己申请早的线程在同步队列中等待,若有,去等待;若没有,才允许去抢占。

    ReentrantLock原理

    ps:这篇博客讲起来更加的通俗易懂

    AQS使用一个FIFO的队列表示排队等待锁的线程,队列头节点称作“哨兵节点”或者“哑节点”,它不与任何线程关联。其他的节点与等待线程关联,每个节点维护一个等待状态waitStatus

    ReentrantLock的基本实现可以概括为:先通过CAS尝试获取锁。如果此时已经有线程占据了锁,那就加入AQS队列并且被挂起。当锁被释放之后,排在CLH队列队首的线程会被唤醒,然后CAS再次尝试获取锁。在这个时候,如果:

    非公平锁:如果同时还有另一个线程进来尝试获取,那么有可能会让这个线程抢先获取;

    公平锁:如果同时还有另一个线程进来尝试获取,当它发现自己不是在队首的话,就会排到队尾,由队首的线程获取到锁。(区别)

    可重入锁。可重入锁是指同一个线程可以多次获取同一把锁。ReentrantLock和synchronized都是可重入锁。

    可中断锁。可中断锁是指线程尝试获取锁的过程中,是否可以响应中断。synchronized是不可中断锁,而ReentrantLock则提供了中断功能。

    公平锁与非公平锁。公平锁是指多个线程同时尝试获取同一把锁时,获取锁的顺序按照线程达到的顺序,而非公平锁则允许线程“插队”。synchronized是非公平锁,而ReentrantLock的默认实现是非公平锁,但是也可以设置为公平锁。

    lock()

    1. 第一步。尝试去获取锁。如果尝试获取锁成功,方法直接返回。

    2. 第二步,入队。由于上文中提到线程A已经占用了锁,所以B和C执行tryAcquire失败,并且入等待队列。如果线程A拿着锁死死不放,那么B和C就会被挂起。

    3. 第三步,挂起。B和C相继执行acquireQueued(final Node node, int arg)。这个方法让已经入队的线程尝试获取锁,若失败则会被挂起。

    线程入队后能够挂起的前提是,它的前驱节点的状态为SIGNAL,它的含义是“Hi,前面的兄弟,如果你获取锁并且出队后,记得把我唤醒!”。所以shouldParkAfterFailedAcquire会先判断当前节点的前驱是否状态符合要求,若符合则返回true,然后调用parkAndCheckInterrupt,将自己挂起。如果不符合,再看前驱节点是否>0(CANCELLED),若是那么向前遍历直到找到第一个符合要求的前驱,若不是则将前驱节点的状态设置为SIGNAL。

     整个流程中,如果前驱结点的状态不是SIGNAL,那么自己就不能安心挂起,需要去找个安心的挂起点,同时可以再尝试下看有没有机会去尝试竞争锁。

        最终队列可能会如下图所示

    static final class Node {
            /** waitStatus值,表示线程已被取消(等待超时或者被中断)*/
            static final int CANCELLED =  1;
            /** waitStatus值,表示后继线程需要被唤醒(unpaking)*/
            static final int SIGNAL    = -1;
            /**waitStatus值,表示结点线程等待在condition上,当被signal后,会从等待队列转移到同步到队列中 */
            /** waitStatus value to indicate thread is waiting on condition */
            static final int CONDITION = -2;
           /** waitStatus值,表示下一次共享式同步状态会被无条件地传播下去
            static final int PROPAGATE = -3;
            /** 等待状态,初始为0 */
            volatile int waitStatus;
            /**当前结点的前驱结点 */
            volatile Node prev;
            /** 当前结点的后继结点 */
            volatile Node next;
            /** 与当前结点关联的排队中的线程 */
            volatile Thread thread;
            /** ...... */
        }

    unlock()

    如果理解了加锁的过程,那么解锁看起来就容易多了。流程大致为先尝试释放锁,若释放成功,那么查看头结点的状态是否为SIGNAL,如果是则唤醒头结点的下个节点关联的线程,如果释放失败那么返回false表示解锁失败。这里我们也发现了,每次都只唤起头结点的下一个节点关联的线程。

     用一张流程图总结一下非公平锁的获取锁的过程。 

  • 相关阅读:
    自动化测试先关
    hadoop集群(第二节机器信息分布表)
    hadoop集群(第一节)
    SpringCloud微服务架构学习笔记
    SpringBoot启动一个项目
    VUE框架介绍
    SpringMVC配置与使用
    Spring学习笔记(二)
    8、XML与JSON
    7、主页面访问权限控制
  • 原文地址:https://www.cnblogs.com/lixuwu/p/10788297.html
Copyright © 2011-2022 走看看