AQS之独占锁ReentrantLock

是一种基于AQS框架的应用实现,是一种互斥锁,可以保证线程安全。

Posted by Sunfy on 2021-11-16
Words 2.2k and Reading Time 9 Minutes
Viewed Times
Viewed Times
Visitors In Total

什么是ReentrantLock

ReentrantLock是一种基于AQS框架的应用实现,是JDK中的一种线程并发访问的同步手段,它的功能类似于synchronized是一种互斥锁,可以保证线程安全。

特点

  • 可中断
  • 可以设置超时时间
  • 可以设置为公平锁
  • 支持多个条件变量
  • synchronized一样,支持可重入

synchronizedReentrantLock区别

sYNCHRONIZED rEENTRANTLOCK
层级 JVM层级 JDK层级
锁状态 无法在代码中直接判断 可以通过isLocked判断
公平 非公平锁 可以是公平也可以非公平
中断 不可被中断 lockinterruptibly可以中断
释放锁 发生异常会自动释放锁 在finally块中显示释放锁
获取锁 立即返回是否成功的tryLock(),等待指定时长的获取 已经在等待的线程是后来的线程先获得锁
队列,先进先出,先来的线程先获得锁 栈,先进后出

ReentrantLock用法

  • 创建锁

    公平锁

    ReentrantLock lock = new ReentrantLock(true);

    非公平锁

    ReentrantLock lock = new ReentrantLock();

    参数默认为false,所以传入false或者不传参数都是非公平锁

  • 对临界区加锁

    lock.lock();

    加锁完成后,要将临界区代码使用try进行异常处理

  • 必须显示解锁

    在finally中显示的解锁

    lock.unlock();

下面看几个例子,从锁的五个特性入手,进行针对性的理解

重入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 同一个锁,在线程1中加锁一次,并在线程1中调用线程2,输出结果正常
// 不过注意我们需要对两次加锁分别进行解锁
public static void main(String[] args) {
method1();
}
public static void method1() {
lock.lock();
try {
log.debug("execute method1");
method2();
} finally {
lock.unlock();
}
}
public static void method2() {
lock.lock();
try {
log.debug("execute method2");
} finally {
lock.unlock();
}
}
// 输出结果
execute method1
execute method2

中断

需要在线程中手动设置为可中断,lock.lockInterruptibly();

显示调用中断锁:t1.interrupt();

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public static void main(String[] args) throws InterruptedException {
ReentrantLock lock = new ReentrantLock();
Thread t1 = new Thread(() -> {
log.debug("t1启动...");
try {
// 设置可中断
lock.lockInterruptibly();
try {
log.debug("t1获得了锁");
} finally {
lock.unlock();
}
} catch (InterruptedException e) {
e.printStackTrace();
log.debug("t1等锁的过程中被中断");
}
}, "t1");
lock.lock();
try {
log.debug("main线程获得了锁");
t1.start();
//先让线程t1执行
Thread.sleep(1000);
// 手动中断锁
t1.interrupt();
log.debug("线程t1执行中断");
} finally {
lock.unlock();
}
}

超时

通过lock.tryLock()尝试获取锁,如果未获取到,直接返回失败

1
2
3
4
if (!lock.tryLock()) {
log.debug("获取锁失败,立即返回false");
return;
}

通过lock.tryLock(3, TimeUnit.SECONDS)尝试在指定时间内获取锁,如果在设定时间内获取到则放回true,否则返回false

1
2
3
4
5
6
7
8
9
try {
if (!lock.tryLock(3, TimeUnit.SECONDS)) {
log.debug("等待 3s 后获取锁失败,直接返回");
return;
}
} catch (InterruptedException e) {
e.printStackTrace();
return;
}

公平

先创建500个线程并启动线程,等所有线程创建好之后,然后去竞争锁,如果设置的为公平锁,那必然符合队列的先进先出原则(FIFO),若设置为非公平锁,那就可能出现后创建的线程在之前先执行。因为非公平锁在创建后会先尝试去获取一下锁,如果获取到则直接执行,如果获取不到才会放入等待队列中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
//ReentrantLock lock = new ReentrantLock(true); //公平锁
ReentrantLock lock = new ReentrantLock(); //非公平锁
for (int i = 0; i < 500; i++) {
new Thread(() -> {
lock.lock();
try {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug(Thread.currentThread().getName() + " running...");
} finally {
lock.unlock();
}
}, "t" + i).start();
}
// 1s 之后去争抢锁
Thread.sleep(1000);
for (int i = 0; i < 500; i++) {
new Thread(() -> {
lock.lock();
try {
log.debug(Thread.currentThread().getName() + " running...");
} finally {
lock.unlock();
}
}, "强行插入" + i).start();
}

条件变量

定义信号量为变量1,变量2

在主线程中,分别修改变量1和变量2的值,并分别调用相应条件的signal1.signal();,激活线程继续执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//变量1
public void signal1(){
lock.lock();
try {
while(!has1){
try {
log.debug("未获取到变量1,等待中...");
signal1.await();
}catch (Exception e){
e.printStackTrace();
}
}
log.debug("获取到变量1");
}finally {
lock.unlock();
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//变量2
public void signal2(){
lock.lock();
try {
while(!has2){
try {
log.debug("未获取到变量2,等待中...");
signal2.await();
}catch (Exception e){
e.printStackTrace();
}
}
log.debug("获取到变量2");
}finally {
lock.unlock();
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
private static ReentrantLock lock = new ReentrantLock();
private static Condition signal1 = lock.newCondition();
private static Condition signal2 = lock.newCondition();

private static boolean has1 = false;
private static boolean has2 = false;
public static void main(String[] args) {
ReentrantLockDemo6 test = new ReentrantLockDemo6();
new Thread(() ->{
test.signal1();
}).start();
new Thread(() -> {
test.signal2();
}).start();
new Thread(() ->{
lock.lock();
try {
has1 = true;
log.debug("唤醒变量1等待线程");
signal1.signal();
}finally {
lock.unlock();
}
},"t1").start();
new Thread(() ->{
lock.lock();
try {
has2 = true;
log.debug("唤醒变量2等待线程");
signal2.signal();
}finally {
lock.unlock();
}
},"t2").start();
}

输出结果

未获取到变量1,等待中…
未获取到变量2,等待中…
唤醒变量1等待线程
唤醒变量2等待线程
获取到变量1
获取到变量2

源码分析

带着问题看源码:

  • ReentrantLock加锁解锁逻辑
  • 如何实现公平与非公平
  • 如何实现可重入锁机制
  • 线程竞争锁失败的入队阻塞逻辑
  • 获取锁的线程释放锁唤醒阻塞线程
  • 竞争锁的逻辑如何实现

首先我们先看下ReentrantLock所有的方法,从方法的名称中我们也大概能判断相应的方法的功能。

image-20211116222602021

Node说明

1
2
3
4
5
6
7
8
9
10
11
12
13
14
static final class Node {
/** 指示节点在共享模式下等待的标记 */
static final Node SHARED = new Node();
/** 指示节点正在以独占模式等待的标记 */
static final Node EXCLUSIVE = null;
/** 指示线程已取消的 waitStatus 值 */
static final int CANCELLED = 1;
/** 指示后继线程需要解停的waitStatus 值 */
static final int SIGNAL = -1;
/** waitStatus 值指示线程正在等待条件 */
static final int CONDITION = -2;
/** 指示下一个acquireShared 应无条件传播的waitStatus值 */
static final int PROPAGATE = -3;
}

ReentrantLock在进行线程判断的时候重点会使用到上面的几个参数进行判断。

加锁逻辑

image-20211116224412185

先来看下公平锁:

image-20211116230146439

acquire:以独占模式获取,忽略中断。 通过至少调用一次tryAcquire ,成功返回。 否则线程会排队,可能会反复阻塞和解除阻塞,调用tryAcquire直到成功。 此方法可用于实现方法Lock.lock 。

image-20211116224648154

tryAcquire:tryAcquire 的公平版本。 除非递归调用或没有服务员或是第一个,否则不要授予访问权限。

image-20211117100515974

非公平版本:执行不公平的 tryLock。 tryAcquire 在子类中实现,但两者都需要对 trylock 方法进行非公平尝试

image-20211117100926315

addWaiter:为当前线程和给定模式创建和排队节点。参数:模式 – Node.EXCLUSIVE 为独占,Node.SHARED 为共享返回:新节点

此处因为初始化pred==null会先调用enq(node)进行等待队列的初始化,第二个线程再次调用到此方法时,pred不为null,则会进行入队操作

image-20211116224734790

acquireQueued:以独占不间断模式获取已在队列中的线程。 由条件等待方法以及获取使用。

image-20211116225053821

enq:将节点插入队列,必要时进行初始化。此处才对等待队列进行了初始化,此处非常值得学习,体会如何初始化一个队列

image-20211116225235439

接下来我们再对比看下非公平锁:

image-20211116230245671

对比可以看到,非公平锁比公平锁就多了一层判断

1
2
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());

方法中直接去尝试获取锁,如果锁获取成功,则直接设置独占所有者线程。这我们就能明白ReentrantLock就是通过这个判断来实现了公平锁和非公平锁。

解锁逻辑

unlock:尝试释放此锁。
如果当前线程是此锁的持有者,则持有计数递减。 如果保持计数现在为零,则释放锁。 如果当前线程不是此锁的持有者,则抛出IllegalMonitorStateException

image-20211116230919961

release:以独占模式发布。 如果tryRelease返回 true,则通过解除阻塞一个或多个线程tryRelease实现。 此方法可用于实现方法Lock.unlock 。

image-20211116231028070

unparkSuccessor:唤醒节点的后继节点(如果存在)。

image-20211116231209250

解锁后去唤醒等待队列中的后继节点。

重入锁机制

ReentrantLock中定义,在尝试获取锁时会进行判断定义的state的数值,如果获取到state是0,则说明是首次获取锁,加锁成功并设置当前拥有独占访问权限的线程。否则会判断当前线程尝试加锁线程是否为当前锁的线程,如果是同一个线程,那么会累加state值,从而支持可重入。

image-20211117103436500


Copyright 2021 sunfy.top ALL Rights Reserved

...

...

00:00
00:00