🍀 JVM已经帮我们内置了synchronized关键字来实现同步,为什么还要引入Lock呢?

首先需要明白synchronized是JVM层面的锁,Lock是API层面的锁,synchonized的灵活度是远不及Lock的;在JDK5时 Lock的效率是优于synchronized,在JDK6开始官方对synchronized进行了大量优化,包括锁升级、锁消除、锁粗化等,事实证明在锁竞争激烈的场景,ReentrantLock还是优于synchronized,但是synchronized还有增长空间,官方也推荐关注synchronized,怎么感觉有种亲儿子的待遇。

认识ReentrantLock的具体实现前,我们应该了解一下ReentrantLock的设计意图是什么?为什么要这样设计?而不是只去看一下具体怎么实现的,然后应付一下面试之类的;我们看别人的代码之前应该搞清楚一个事实,我是奔着提高能力去的,想一想它为什么这样设计?心中多一点思考!看一下Doug Lea大神写的并发代码,我们怎么说也会进步的吧~,或许你会说基础太薄弱,一下子无法理解透;理解一下即可,未来某一刻遇到了类似的设计理念,你会深有同感的。

一文彻底搞懂ReentrantLock原理【基于AQS的公平锁+非公平锁】_面试


话不多说,进入今天的正题;今天的核心:ReentrantLock只是指挥的,具体在它的内部类

一文彻底搞懂ReentrantLock原理【基于AQS的公平锁+非公平锁】_java_02


🍀其中的Lock接口不用多说,这是API层面的锁的统一规范,后续你必须进行遵守,定义一些加锁、解锁、尝试获取锁的接口,因为在API层面,无论你使用什么锁,肯定少不了这几个方法的。

🍀然后ReentranLock内部定义了Sync抽象类,通过组合的方式持有该类

一文彻底搞懂ReentrantLock原理【基于AQS的公平锁+非公平锁】_抽象类_03


内部的FairSync和NonFairSync通过继承Sync来实现公平锁和非公平锁,扩展性得到提高,耦合度降低;此时你会发现ReentrantLock就像一个"老板"一样,指挥FairSync和NonFairSync这两个主管去做两个任务,而具体的怎么做的一概不管,但是必须给我完成。而FairSync和NonFairSync这两个小主管也会偷懒,把它们相同的部分交给了AbstractQueuedSynchronizer这个底层员,emmm~果然是层层压榨!

一文彻底搞懂ReentrantLock原理【基于AQS的公平锁+非公平锁】_ReentrantLock_04

首先思考一下ReentrantLock怎么使用的?

一文彻底搞懂ReentrantLock原理【基于AQS的公平锁+非公平锁】_面试_05


官方注释中也定义了一般情况下应该怎么使用ReentrantLock

一文彻底搞懂ReentrantLock原理【基于AQS的公平锁+非公平锁】_ReentrantLock_06

在try代码块中执行临界区代码,在finally类进行释放锁,保证即便执行临界区代码报错,也会进行锁的释放。

一文彻底搞懂ReentrantLock原理【基于AQS的公平锁+非公平锁】_面试_07


❓ 接下来分析一下ReentrantLock内部有什么东东?(Alt + 7)

一文彻底搞懂ReentrantLock原理【基于AQS的公平锁+非公平锁】_java_08

📑其中最为突出有3个内部类,首先Sync抽象类,该类继承了AbstractQueuedSynchronizer抽象同步队列(AQS)

一文彻底搞懂ReentrantLock原理【基于AQS的公平锁+非公平锁】_公平锁_09


AQS可以说是很多同步锁实现的核心,比如说ReentrantLock、Semaphore、CountDownLatch等等,这些同步锁都是基于AQS来实现的(定义一个内部类继承了AQS),所以需要搞清楚AQS这个东西。

一文彻底搞懂ReentrantLock原理【基于AQS的公平锁+非公平锁】_抽象类_10

AQS即"抽象同步队列",它有5个核心要素:同步状态、等待队列、独占模式、共享模式和条件队列。

(1)同步状态

顾名思义,同步状态就是用来实现锁机制的。如何实现?AQS抽象类中有一个属性state

一文彻底搞懂ReentrantLock原理【基于AQS的公平锁+非公平锁】_面试_11


然后看我们调用lock方法进行加锁的底层实现,首先是调用了ReentrantLock内部类Sync的尝试获取锁方法,看下图

一文彻底搞懂ReentrantLock原理【基于AQS的公平锁+非公平锁】_ReentrantLock_12


参数传入了一个1,这个如何理解呢?也就是线程CAS尝试将0修改为1,如果修改成功,那么它就是成功抢到锁(这里我说的没包含锁重入的情况),然后我以非公平锁为例,简单分析获取锁源码实现。

一文彻底搞懂ReentrantLock原理【基于AQS的公平锁+非公平锁】_抽象类_13

需要注意一点就是那个state状态值,为什么会大于1呢?,这时因为ReentrantLock是支持可重入的,一旦该值>1,说明某个线程多次获取了同一把锁(业务复杂时可能会用到)。

(2)等待队列

顾名思义,等待队列就是用来存放等待锁的线程,在AQS抽象类中有一个内部类Node,它是实现双向队列的核心

//独占锁标识
static final Node EXCLUSIVE = null;
//节点处于取消状态,后面会详解
static final int CANCELLED = 1;
//标识后续节点需要被唤醒
static final int SIGNAL = -1;
//节点状态
volatile int waitStatus;
//前驱指针
volatile Node prev;
//后继指针
volatile Node next;
//当前节点绑定的线程
volatile Thread thread;

AQS同步器将线程封装到了Node里面,维护了一个CHL Node FIFO队列,这是一个非阻塞的FIFO队列,意味着在并发条件下向此队列进行插入和删除时不会发生阻塞。它通过自旋+CAS来保证节点插入和移除的原子性。看下面的AQS中的入队代码

//节点的入队方法
private Node addWaiter(Node mode) {
Node node = new Node(mode);
//自旋+CAS保证快速插入
for (;;) {
Node oldTail = tail;
//如果存在尾节点,说明同步队列已经被初始化过(也就是该节点不是第一个插入到队列的)
if (oldTail != null) {
//设置将要插入节点的前驱节点指向队列尾节点
//注意并发情况下可能有多个节点同时指向尾节点
node.setPrevRelaxed(oldTail);
//CAS---设置插入节点的地址为当尾节点的next域
//设置失败的节点重试(上面的for循环)
if (compareAndSetTail(oldTail, node)) {
oldTail.next = node;//设置tail节点为当前插入节点
return node;
}
} else {
//队列首次插入节点,则要进行初始化操作
initializeSyncQueue();
}
}
}

(3)独占模式

顾名思义,用来实现独占锁的,AQS的内部类Node定义的EXCLUSIVE 就是用来标识独占模式的。

(4)共享模式

用来实现共享锁的,AQS的内部类Node定义的SHARED就是用来标识共享模式的。

到这里AQS就介绍的差不多了,该专注于公平锁和非公平锁了。

一文彻底搞懂ReentrantLock原理【基于AQS的公平锁+非公平锁】_ReentrantLock_14


✨首先公平锁和非公平锁的加锁和解锁都是在AQS中实现的,这两种锁都继承了Sync抽象类,查看该抽象类的具体实现。

abstract static class Sync extends AbstractQueuedSynchronizer {

@ReservedStackAccess
final boolean nonfairTryAcquire(int acquires) {
....
}
@ReservedStackAccess
protected final boolean tryRelease(int releases) {
....
}
....
}

你会发现它居然在内部了非公锁nonfairTryAcquire获取锁操作,它是因为偏心吗?既然是不公平锁那就不公平一点?😂😂,其实不是的,因为公平锁也会用到这个方法,所以将它抽取到了Sync类中;tryRelease方就不用多说了,用户释放锁,两种锁的释放是一致的。

为什么称之为非公平锁,它的不公平体现在哪里?

竞争锁时会有两方面的势力,被唤醒的CLH队列中的线程和非CLH队列中的线程,它们会同时竞争锁;不会因为你来的早我就把锁让给你,一句话:各凭本事!

具体体现在下面代码中:

一文彻底搞懂ReentrantLock原理【基于AQS的公平锁+非公平锁】_面试_15

一文彻底搞懂ReentrantLock原理【基于AQS的公平锁+非公平锁】_ReentrantLock_16


锁释放时调用了release方法(上面讲过这个方法),方法内部调用了unparkSuccessor唤醒后继节点方法。此时如果来了多个线程调用lock方法想要获取锁

一文彻底搞懂ReentrantLock原理【基于AQS的公平锁+非公平锁】_面试_17


一文彻底搞懂ReentrantLock原理【基于AQS的公平锁+非公平锁】_java_18


最终会调用到nonfairTryAcquire尝试获取锁方法

一文彻底搞懂ReentrantLock原理【基于AQS的公平锁+非公平锁】_公平锁_19


所以说非公平就体现在这,但是这样的性能是比较好的,因为可能直接省略了唤醒线程这一步骤。

❓获取锁的流程是怎么样的:

一文彻底搞懂ReentrantLock原理【基于AQS的公平锁+非公平锁】_ReentrantLock_20

为什么称之为公平锁,它的公平体现在哪里?

一文彻底搞懂ReentrantLock原理【基于AQS的公平锁+非公平锁】_抽象类_21


其实FairSync和NonfairSync的获取锁代码基本上一致,只不过NonfairSync比FairSync多了一步,需要判断当前线程是否是在CLH队列中被唤醒的。

❓ ReentrantLock默认使用的是公平锁还是非公平锁?

是非公平锁,性能优于公平锁,前面解释过。

一文彻底搞懂ReentrantLock原理【基于AQS的公平锁+非公平锁】_公平锁_22


也可以传入true值,就会使用公平锁。