专栏首页程序员备忘录ReentrantLock源码解析

ReentrantLock源码解析

在java编程中,经常需要多代码进行加锁来防止多线程可能引起的数据不一致。而锁的类型有公平锁和非公平锁。公平锁的意义就是按照顺序,而非公平锁则是相反的。也就是说非公平锁会让参与资源竞争的线程都具有获取资源的概率,而公平锁则是谁先进入队列谁就具有获取锁的概率。而越是队列靠后的线程越是没有提前使用资源的权利。在JUC工具包中,多线程的锁机制其实就是基于队列实现的,其主要就是先进先出队列实现的(FIFO)。

我们还是通过构造方法的静态方法等类的初始化顺序进行逐步阅读源码。

初步查看ReentrantLock,我们发现其中有抽象类Sync,已经Sync的两种不同实现方式,其中NonfairSync是非公平锁,FairSync是公平锁,Sync继承了AbstractQueuedSynchronizer。而AbstractQueuedSynchronizer就是传说中的AQS,此处我们可以将AQS理解为专门用来维护FIFO队列的同步器。它提供了很多方法,能够提供多线程情况下的节点的安全操作。

在ReetrantLock的构造函数中,我们发现默认采用的非公平锁,在使用的时候可以通过条件变量fair决定采用那种锁。

我们在代码中经常会使用的Lock方法其实也是直接调用Sync类的子类中的方法。我们使用公平锁FairSync近追踪。

发现方法已经进入了Sync类的父类AQS,果然AQS才是最后的大BOSS,本来打算绕过去的,看来所有的一切都还是要面对啊。

这里边有三个方法

public final void acquire(int arg) {
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}

通过字面理解,trayAcquire就是试图获取。通过之前的lock的追踪,那么这个方法就是获取锁的意思。acquireQueued就是在获取锁失败之后才进行,而传入的参数为AddWaiter方法,AddWaiter的参数又是Node.EXCLUSIVE,exclusive是排斥的意思,通过前边的解释,addWaiter函数的基本作用就是将当前线程添加到队列里。如果没有获取锁就将其添加到队列中,那么acquireQueued方法是做什么的?现在还不得而知,selfInterrupt方法按照字母理解就是中断当前线程。

分析到这里的时候我觉得有必要重新理一理对多线程的理解。我们看到这里并没有传入线程,那是因为我们可以通过Thread.currentThread的方法获取。当单个线程阻塞了的时候我们可以唤醒它并让其进行运行。所以说我们的AQS其实并不是我理解的一种专门针对后边JUC框架的工具类,而是具有极大兼容性的。只要我们继承了AbstractQueuedSynchronizer,并去操作其提供的保护方法,便可以实现高并发条件下的代码块同步。是的AbstractQueuedSynchronizer将复杂的队列操作已经帮我们做好了。

但是当我们追踪到tryAcquire方法的时候发现AQS啥都没有做???不是说好的么,AQS帮我们搞定一切,我们只需要调用即可啦,怎么可以这样。这让人情何以堪。好吧,既然父类没有搞定,那么这里的逻辑应该写在哪里呐?那当然是在子类中了,也就是FairSycn和NoFairSycn进行了重写。那么这块怎么理解?那当然就是两个兄弟需要进行不同的实现了。就是差异化了。我们学java的时候就知道,一般重写父类方法之后,子类的实体会直接调子类已经重写的新方法,而调用父类的方法则需要super的调用方法,是的,大佬的java基础很强大。。

继续我们分析的事业,依旧按照FairSync的子类进行寻找。

发现首先进行了getStatue的操作,而getStaue是在AQS中,根据解释,该变量就是用来表示同步状态的。那么同步是什么意思?而且这个是volative的,于是,又陷入了无边的迷惑之中。但是我们知道volative是直接操作主存的,而且当这个变量为0的时候就直接调用了compareAndSetState(0,acquires);

但是这个方法比较好理解,这里采用了CAS进行了原子操作,先将state的值与0进行比较,如果相等就将其值变为acquires,否则就是一顿自旋!!!

而且在设置成功之后还setExclusiveOwnerThread,看来是进行设置当前线程为独占和排它的意思。但是排它了之后呐?我们在添加锁的时候,一般都是先lock,然后再unlock,中间进行代码运行。当其他线程进来的时候,根据上述分析,应该都是加入的AQS队列中,如果AQS中没有排队的线程,那么就直接设置当前线程为独占?????这有什么意思。不就是我一个么,为啥还得搞个独占的?我走还不行吗?但是仔细思考之后,还是觉得现实生活和代码差别太大,这种感觉就像万有引力和广义相对论一般,为了安全期间还是设置一下独占,就是为了防止因为没有标识而导致所以需要线程都直接run的格局吧,那么咱们的AQS还有意义么?答案肯定是没有了。

这里需要注意一下的是设置独占模式的代码直接写在了abstractOwnableSynchronizer在里,是不是感慨大佬就是大佬。佩服佩服

我们继续我们的事业

既然上边的队列显示没有其他线程,那么我们的线程就是成功了,然后可以直接按照代码往下跑了。但是请看下边这个else if,如果当前是独占线程,那么就将同步变量进相加,然后setSate,那么这里的setState一定是CAS的。但是你有没有发现有点不科学,既然getState不等于0,也就是已经有独占的线程了,但是这个独占的线程还是我自己。。就需要把state进行累加。那么这里就是说代码如果发生了类似循环的模式,当前线程还是持有锁的意思。是的,这就是“可重入锁”。显然对于其他线程来说如果没有独占锁,那么妥妥的进入AQS队列里排着。。。

通过上述分析,我们大概了解了tryAcquire的方法。那么接下来就是acqureQueued方法系。

这个方法还是比较容易理解的,大概的意思就是将当前线程进行封装,然后将其添加到队列的最后,并将这个线程节点进行返回,然后作为参数进行我当时感到迷惑的那个方法。感觉暴风雨即将来临啊!我们需要严阵以待

但是如果tail尾节点是空的,那么是不是说明我们的机会来了。所以enq方法大概就是做这个事情的。

看到没,如果尾节点为空,那么我当前的节点就是老大,否则也能排个老二

。如果这样的话,那么之前的让我感到迷惑的方法似乎不那么神秘了。感觉那么方法就是要我享受独占资源了,是不是都是自己人。呵呵呵

。但是咋不能凭空猜测,需要用事实考证。

好像跟上边的猜测有些出入,因为addwaite还有处于队尾的可能啊,好吧那么这个方法就是为了兼容这两种情况了,感觉就是进行二次确认的意思了。通过自旋拿到前置节点,然后判断其是否为头节点,因为头节点就是下一次资源的享受者,如果其前置节点为头结点,那么就尝试获取锁,如果成功了就头节点的线程已经从临界区走完,轮到我来表演了。那么当然先设置自己为老大,然后反回false,让之后的自我中断操作不在进行。

如果这种尝试并没有成功,也就是说我们还不在老二的位置,或者处于老三甚至老十三的位置。那么我应该进入阻塞队列吗?按理说这块应该结束啊,还尝试什么啊,既不是老二,还没有抢到头节点的位置,那就应该进入阻塞队列,并进行自我中断啊。搞不懂这块要做什么。。不过我们还是得仔细分析。

这块的代码似乎与上边的头节点抢占资源有点类似,如果其前置节点是Node.signal状态那么当前线程就可以中断了,因为之前至少有一个处于等待信号的状态。其中有一个do。。。while循环,大概就是跳过的意思。那么这里的ws>0应该不是一个好信号,估计是打算抛弃的线程节点。那么他们的ws在什么时候会大于0?看来当ws大于0就表示当前线程已经选择放弃竞争了...但是我还是比较迷惑,它为何会选择放弃。他放弃的边界条件是什么?

* Status field, taking on only the values:
*   SIGNAL:     The successor of this node is (or will soon be)
*               blocked (via park), so the current node must
*               unpark its successor when it releases or
*               cancels. To avoid races, acquire methods must
*               first indicate they need a signal,
*               then retry the atomic acquire, and then,
*               on failure, block.
*   CANCELLED:  This node is cancelled due to timeout or interrupt.
*               Nodes never leave this state. In particular,
*               a thread with cancelled node never again blocks.
*   CONDITION:  This node is currently on a condition queue.
*               It will not be used as a sync queue node
*               until transferred, at which time the status
*               will be set to 0. (Use of this value here has
*               nothing to do with the other uses of the
*               field, but simplifies mechanics.)
*   PROPAGATE:  A releaseShared should be propagated to other
*               nodes. This is set (for head node only) in
*               doReleaseShared to ensure propagation
*               continues, even if other operations have
*               since intervened.
*   0:          None of the above

如果ws不大于0,而且不等于-1,则将其设置为signal,并发挥false。对这块比较迷惑

最后的中断是最简单的操作了。

在分析完Lock之后,分析unlock就相对简单多了,无疑就是对其state状态进行反向操作而已。这里就不进行详细的解析了。有兴趣的朋友可以自行研究。

本文分享自微信公众号 - 程序员备忘录(gh_a84f9a607848),作者:学约乐

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2020-06-07

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 线程池参数详解

    我们知道JUC提供了丰富的并发工具类,其中类似于组的结构叫做线程池。就是说我们可以将我们需要运行的线程加入到这个组内,然后通过启动线程池来执行加入到线程池的所有...

    程序员_备忘录
  • CAS机制是什么?

    因为多核CPU的存在,总是需要保障程序高性能的利用计算机的CPU资源,并要保障计算结果与预期的一致。因此常用的方式就是加锁方式。就是占用->占用结束->释放的...

    程序员_备忘录
  • Thread源码解析

    我们上学的时候都知道线程有两种方式,要么继承Thread类,要么实现runable接口。根据我们上次对线程池的分析,发现我们对Thread类的理解还比较浅显。所...

    程序员_备忘录
  • JVM-concurrent-HashSet-problem Java 并发问题

    上午刚到公司,准备开始一天的摸鱼之旅时突然收到了一封监控中心的邮件。 心中暗道不好,因为监控系统从来不会告诉我应用完美无 bug,其实系统挺猥琐。 打开邮件...

    爱明依
  • 线程基础三问——猫眼真题

    线程和进程是项目中常遇到的知识点,面试官对此也是对此类知识点经常考察。其中,线程是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位...

    码上积木
  • 微服务转型,雪崩效应是绕不过的一道坎

    记得在三年前公司因为业务发展需要,就曾经将单体应用迁移到分布式框架上来。当时就遇到了这样一个问题:系统仅有一个控制单元,它会调用多个运算单元,如果某个运算单元(...

    yuanyi928
  • “面试不败计划”:java工程师面试常问的多线程问题【推荐】

    好好学java
  • 线程数究竟设多少合理

    一、需求缘起 Web-Server通常有个配置,最大工作线程数,后端服务一般也有个配置,工作线程池的线程数量,这个线程数的配置不同的业务架构师有不同的经验值,...

    架构师之路
  • Java Concurrent Executor

    在说Executor前, 先来看一下线程创建的几种方式: 1、继承Thread类创建线程 2、 实现Runable接口创建线程 3、使用Callable和...

    邹志全
  • 多线程?怎么用?

    进程是指一个内存中运行的应用程序,每个进程都有自己独立的一块内存空间,一个进程中可以启动多个线程。比如在Windows系统中,一个运行的exe就是一个进程。

    故里

扫码关注云+社区

领取腾讯云代金券