java selfinterrupt_死磕 java同步系列之ReentrantLock源码解析(一)——公平锁、非公平锁...

问题

(1)重入锁是什么?

(2)ReentrantLock如何实现重入锁?

(3)ReentrantLock为什么默认是非公平模式?

(4)ReentrantLock除了可重入还有哪些特性?

简介

Reentrant = Re + entrant,Re是重复、又、再的意思,entrant是enter的名词或者形容词形式,翻译为进入者或者可进入的,所以Reentrant翻译为可重复进入的、可再次进入的,因此ReentrantLock翻译为重入锁或者再入锁。

重入锁,是指一个线程获取锁之后再尝试获取锁时会自动获取锁。

在Java中,除了ReentrantLock以外,synchronized也是重入锁。

那么,ReentrantLock的可重入性是怎么实现的呢?

继承体系

d37c0332826484dc9158e13fe020a3f7.png

ReentrantLock实现了Lock接口,Lock接口里面定义了java中锁应该实现的几个方法:

// 获取锁

Lock接口中主要定义了 获取锁、尝试获取锁、释放锁、条件锁等几个方法。

源码分析

主要内部类

ReentrantLock中主要定义了三个内部类:Sync、NonfairSync、FairSync。

abstract 

(1)抽象类Sync实现了AQS的部分方法;

(2)NonfairSync实现了Sync,主要用于非公平锁的获取;

(3)FairSync实现了Sync,主要用于公平锁的获取。

在这里我们先不急着看每个类具体的代码,等下面学习具体的功能点的时候再把所有方法串起来。

主要属性

private 

主要属性就一个sync,它在构造方法中初始化,决定使用公平锁还是非公平锁的方式获取锁。

主要构造方法

// 默认构造方法

(1)默认构造方法使用的是非公平锁;

(2)第二个构造方法可以自己决定使用公平锁还是非公平锁;

上面我们分析了ReentrantLock的主要结构,下面我们跟着几个主要方法来看源码。

lock()方法

彤哥贴心地在每个方法的注释都加上方法的来源。

公平锁

这里我们假设ReentrantLock的实例是通过以下方式获得的:

ReentrantLock 

下面的是加锁的主要逻辑:

// ReentrantLock.lock()

看过之前彤哥写的【死磕 java同步系列之自己动手写一个锁Lock】的同学看今天这个加锁过程应该思路会比较清晰。

下面我们看一下主要方法的调用关系,可以跟着我的 → 层级在脑海中大概过一遍每个方法的主要代码:

ReentrantLock

获取锁的主要过程大致如下:

(1)尝试获取锁,如果获取到了就直接返回了;

(2)尝试获取锁失败,再调用addWaiter()构建新节点并把新节点入队;

(3)然后调用acquireQueued()再次尝试获取锁,如果成功了,直接返回;

(4)如果再次失败,再调用shouldParkAfterFailedAcquire()将节点的等待状态置为等待唤醒(SIGNAL);

(5)调用parkAndCheckInterrupt()阻塞当前线程;

(6)如果被唤醒了,会继续在acquireQueued()的for()循环再次尝试获取锁,如果成功了就返回;

(7)如果不成功,再次阻塞,重复(3)(4)(5)直到成功获取到锁。

以上就是整个公平锁获取锁的过程,下面我们看看非公平锁是怎么获取锁的。

非公平锁

// ReentrantLock.lock()

相对于公平锁,非公平锁加锁的过程主要有两点不同:

(1)一开始就尝试CAS更新状态变量state的值,如果成功了就获取到锁了;

(2)在tryAcquire()的时候没有检查是否前面有排队的线程,直接上去获取锁才不管别人有没有排队呢;

总的来说,相对于公平锁,非公平锁在一开始就多了两次直接尝试获取锁的过程。

lockInterruptibly()方法

支持线程中断,它与lock()方法的主要区别在于lockInterruptibly()获取锁的时候如果线程中断了,会抛出一个异常,而lock()不会管线程是否中断都会一直尝试获取锁,获取锁之后把自己标记为已中断,继续执行自己的逻辑,后面也会正常释放锁。

题外话:

线程中断,只是在线程上打一个中断标志,并不会对运行中的线程有什么影响,具体需要根据这个中断标志干些什么,用户自己去决定。

比如,如果用户在调用lock()获取锁后,发现线程中断了,就直接返回了,而导致没有释放锁,这也是允许的,但是会导致这个锁一直得不到释放,就出现了死锁。

lock

当然,这里只是举个例子,实际使用肯定是要把lock.lock()后面的代码都放在try...finally...里面的以保证锁始终会释放,这里主要是为了说明线程中断只是一个标志,至于要做什么完全由用户自己决定。

tryLock()方法

尝试获取一次锁,成功了就返回true,没成功就返回false,不会继续尝试。

// ReentrantLock.tryLock()

tryLock()方法比较简单,直接以非公平的模式去尝试获取一次锁,获取到了或者锁本来就是当前线程占有着就返回true,否则返回false。

tryLock(long time, TimeUnit unit)方法

尝试获取锁,并等待一段时间,如果在这段时间内都没有获取到锁,就返回false。

// ReentrantLock.tryLock()

tryLock(long time, TimeUnit unit)方法在阻塞的时候加上阻塞时间,并且会随时检查是否到期,只要到期了没获取到锁就返回false。

unlock()方法

释放锁。

// java.util.concurrent.locks.ReentrantLock.unlock()

释放锁的过程大致为:

(1)将state的值减1;

(2)如果state减到了0,说明已经完全释放锁了,唤醒下一个等待着的节点;

未完待续,下一章我们继续学习ReentrantLock中关于条件锁的部分

彩蛋

为什么ReentrantLock默认采用的是非公平模式?

答:因为非公平模式效率比较高。

为什么非公平模式效率比较高?

答:因为非公平模式会在一开始就尝试两次获取锁,如果当时正好state的值为0,它就会成功获取到锁,少了排队导致的阻塞/唤醒过程,并且减少了线程频繁的切换带来的性能损耗。

非公平模式有什么弊端?

答:非公平模式有可能会导致一开始排队的线程一直获取不到锁,导致线程饿死。

推荐阅读

  1. 死磕 java同步系列之AQS起篇
  2. 死磕 java同步系列之自己动手写一个锁Lock
  3. 死磕 java魔法类之Unsafe解析
  4. 死磕 java同步系列之JMM(Java Memory Model)
  5. 死磕 java同步系列之volatile解析
  6. 死磕 java同步系列之synchronized解析

欢迎关注我的公众号“彤哥读源码”,查看更多源码系列文章, 与彤哥一起畅游源码的海洋。

ace06fd9d4095bb07cbb5c6d03205ee0.png