ReentrantLock

一、概述

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

相对于 synchronized 它具备如下特点:

  • 可中断(等待获取锁的过程中可以被打断)
    • synchronized锁加上去不能中断,a线程应用锁,b线程不能取消掉它
  • 可以设置超时时间
    • synchronized它去获取锁时,如果对方持有锁,那么它就会进入entryList一直等待下去。而ReentrantLock可以设置超时时间,规定时间内如果获取不到锁,就放弃锁。
  • 可以设置为公平锁
    • 防止线程饥饿的情况,即先到先得。如果争抢的人比较多,则可能会发生永远都得不到锁。
  • 支持多个条件变量(相当于有多个EntryList)
    • synchronized只支持同一个waitset。
  • 与 synchronized 一样,都支持可重入

二、基本语法

步骤

(1)创建一个ReentrantLock对象;

(2)调用ReentrantLock对象的lock()方法;

(3)将临界区的代码写在try代码块中;

(4)将ReentrantLock对象的unlock()方法写在finally代码块中。

1
2
3
4
5
6
7
8
9
10
private Lock lock = new ReentrantLock();

// 获取锁
lock.lock();
try {
// 临界区
} finally {
// 释放锁
lock.unlock();
}
  • synchronized是在关键字的级别来保护临界区,而reentrantLock是在对象的级别保护临界区。临界区即访问共享资源的那段代码。
  • finally中表明不管将来是否出现异常,都会释放锁,释放锁即调用unlock方法。否则无法释放锁,其它线程就永远也获取不了锁。

注意:lock.lock();与try代码块之间不要有空行或者其它逻辑,且lock.unlock();要写在finally代码块的第一行

三、可重入

3.1 概述

  • 可重入是指同一个线程如果首次获得了这把锁,那么因为它是这把锁的拥有者,因此有权利再次获取这把锁;
  • 如果是不可重入锁,那么第二次获得锁时,自己也会被锁挡住。

3.2 代码示例

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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
import lombok.extern.slf4j.Slf4j;

import java.util.concurrent.locks.ReentrantLock;

/**
* @author xiaowei
* @date 2022-10-25
* @description ReentrantLock 可重入
**/
@Slf4j(topic = "c.Test03")
public class Test03 {

static ReentrantLock lock = new ReentrantLock();

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");
method3();
}finally {
lock.unlock();
}
}

public static void method3(){
lock.lock();

try {
log.debug("execute method3");
}finally {
lock.unlock();
}
}
}

运行结果

1
2
3
21:58:48.257 c.Test03 [main] - execute method1
21:58:48.259 c.Test03 [main] - execute method2
21:58:48.259 c.Test03 [main] - execute method3

从运行结果可以看出,当前线程在执行时多次获取锁, 并不会被锁挡住, 而是正常运行

注意:加锁与解锁是必须匹配的,只有当解锁次数等于加锁次数时,锁才会被正确释放。

四、可打断

4.1 概述

可打断是指, 当前线程在等待锁的时候, 可以被其他的线程使用 interrupt() 方法打断。synchronized是不可中断锁,而ReentrantLock则提供了中断功能。

方法说明

  • **lock.lockInterruptibly()**:尝试获取锁,如果获取不到锁,进入等待;等待过程中可以被打断。
  • lock.lock():等待锁的过程是不可以被打断的。

4.2 代码示例

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
36
37
38
39
40
41
42
43
44
45
import lombok.extern.slf4j.Slf4j;

import java.util.concurrent.locks.ReentrantLock;

import static site.weiyikai.thread.utils.Sleeper.sleep;

/**
* @author xiaowei
* @date 2022-10-25
* @description 打断ReentrantLock锁
**/
@Slf4j(topic = "c.Test04")
public class Test04 {
public static void main(String[] args) {
ReentrantLock lock = new ReentrantLock();
Thread t1 = new Thread(() -> {
log.debug("start...");

try {
lock.lockInterruptibly();
} catch (InterruptedException e) {
e.printStackTrace();
log.debug("等锁的过程中被打断");
return;
}

try {
log.debug("获得了锁");
}finally {
lock.unlock();
}
},"t1");

lock.lock();
log.debug("获得了锁");
t1.start();
try {
sleep(1);
t1.interrupt();
log.debug("执行打断");
} finally {
lock.unlock();
}
}
}

运行结果

1
2
3
4
5
6
7
8
9
10
22:21:02.967 c.Test04 [main] - 获得了锁
22:21:02.969 c.Test04 [t1] - start...
22:21:03.984 c.Test04 [main] - 执行打断
java.lang.InterruptedException
at java.util.concurrent.locks.AbstractQueuedSynchronizer.doAcquireInterruptibly(AbstractQueuedSynchronizer.java:898)
at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquireInterruptibly(AbstractQueuedSynchronizer.java:1222)
at java.util.concurrent.locks.ReentrantLock.lockInterruptibly(ReentrantLock.java:335)
at com.lilinchao.thread.demo06.Test04.lambda$main$0(Test04.java:22)
at java.lang.Thread.run(Thread.java:748)
22:21:03.985 c.Test04 [t1] - 等锁的过程中被打断

说明

main线程首先获得锁,因此被创建出的线程t1启动后无法获得锁,之后,main线程打断线程t1,使得线程t1结束等待。

4.3 不可中断模式

注意如果是不可中断模式,那么即使使用了 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
31
32
33
34
35
36
37
38
39
40
import lombok.extern.slf4j.Slf4j;

import java.util.concurrent.locks.ReentrantLock;

import static site.weiyikai.thread.utils.Sleeper.sleep;

/**
* @author xiaowei
* @date 2022-10-25
* @description 不可中断模式
**/
@Slf4j(topic = "c.Test05")
public class Test05 {
public static void main(String[] args) {
ReentrantLock lock = new ReentrantLock();
Thread t1 = new Thread(() -> {
log.debug("start...");

lock.lock();
try {
log.debug("获得了锁");
}finally {
lock.unlock();
}
},"t1");

lock.lock();
log.debug("获得了锁");
t1.start();
try {
sleep(1);
t1.interrupt();
log.debug("执行打断");
sleep(1);
} finally {
log.debug("释放了锁");
lock.unlock();
}
}
}

运行结果

1
2
3
4
5
22:24:52.756 c.Test05 [main] - 获得了锁
22:24:52.758 c.Test05 [t1] - start...
22:24:53.771 c.Test05 [main] - 执行打断
22:24:54.777 c.Test05 [main] - 释放了锁
22:24:54.777 c.Test05 [t1] - 获得了锁

五、锁超时

5.1 概述

可打断,是一种被动的打断,需要其他的线程来进行打断。

而锁超时可以通过主动方式,来解决线程无限制的等待下去。如果当前线程在等待了一段时间之后,还没有获取锁,将不在继续等待,继续向下执行。

通过设置获得锁的等待时间,当不能在等待时间内获得锁的时候释放锁,就能够避免死锁的问题。

5.2 设置超时时间API

ReetrantLock提供了两个获取锁并快速返回的方法,不会一直等待,无论成功失败都将立即返回。

5.3 代码示例

  • 无参tryLock()方法
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
36
37
38
39
import lombok.extern.slf4j.Slf4j;

import java.util.concurrent.locks.ReentrantLock;

import static site.weiyikai.thread.utils.Sleeper.sleep;

/**
* @author xioawei
* @date 2022-10-25
* @description 锁超时 - 立刻失败
**/
@Slf4j(topic = "c.Test06")
public class Test06 {
public static void main(String[] args) {
ReentrantLock lock = new ReentrantLock();
Thread t1 = new Thread(() -> {
log.debug("start...");
if (!lock.tryLock()) {
log.debug("获取立即失败,返回");
return;
}

try {
log.debug("获得了锁");
}finally {
lock.unlock();
}
},"t1");

lock.lock();
log.debug("获得了锁");
t1.start();
try {
sleep(2);
}finally {
lock.unlock();
}
}
}

运行结果

1
2
3
22:40:21.990 c.Test06 [main] - 获得了锁
22:40:21.992 c.Test06 [t1] - start...
22:40:21.992 c.Test06 [t1] - 获取立即失败,返回
  • 带参tryLock方法
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
36
37
38
39
40
41
42
43
44
45
import lombok.extern.slf4j.Slf4j;

import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;

import static site.weiyikai.thread.utils.Sleeper.sleep;

/**
* @author xiaowei
* @date 2022-10-25
* @description 超时失败
**/
@Slf4j(topic = "c.Test07")
public class Test07 {
public static void main(String[] args) {
ReentrantLock lock = new ReentrantLock();
Thread t1 = new Thread(() -> {
log.debug("start...");
try {
if (!lock.tryLock(1, TimeUnit.SECONDS)) {
log.debug("获取等待 1s 后失败,返回");
return;
}
} catch (InterruptedException e) {
e.printStackTrace();
}

try {
log.debug("获得了锁");
}finally {
lock.unlock();
}
},"t1");

lock.lock();
log.debug("获得了锁");
t1.start();
try {
//主线程等待2s
sleep(2);
}finally {
lock.unlock();
}
}
}

运行结果

1
2
3
22:42:41.537 c.Test07 [main] - 获得了锁
22:42:41.539 c.Test07 [t1] - start...
22:42:42.544 c.Test07 [t1] - 获取等待 1s 后失败,返回

说明

代码执行时,main线程先获得了锁,进入到2s的睡眠当中,此时t1线程启动,执行到lock.tryLock(1, TimeUnit.SECONDS),等待获取到lock锁后继续向下执行。1s后t1线程未获得所,将放弃继续获取锁,t1线程退出。

5.4 锁超时解决哲学家就餐问题

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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
import site.weiyikai.thread.utils.Sleeper;
import lombok.extern.slf4j.Slf4j;

import java.util.concurrent.locks.ReentrantLock;

/**
* @author xiaowei
* @date 2022-10-25
* @description
**/
public class Test08 {
public static void main(String[] args) {
Chopstick2 c1 = new Chopstick2("1");
Chopstick2 c2 = new Chopstick2("2");
Chopstick2 c3 = new Chopstick2("3");
Chopstick2 c4 = new Chopstick2("4");
Chopstick2 c5 = new Chopstick2("5");
new Philosopher2("苏格拉底", c1, c2).start();
new Philosopher2("柏拉图", c2, c3).start();
new Philosopher2("亚里士多德", c3, c4).start();
new Philosopher2("赫拉克利特", c4, c5).start();
new Philosopher2("阿基米德", c5, c1).start();
}
}

@Slf4j(topic = "c.Philosopher2")
class Philosopher2 extends Thread {
Chopstick2 left;
Chopstick2 right;
public Philosopher2(String name, Chopstick2 left, Chopstick2 right) {
super(name);
this.left = left;
this.right = right;
}
@Override
public void run() {
while (true) {
// 尝试获得左手筷子
if (left.tryLock()) {
try {
// 尝试获得右手筷子
if (right.tryLock()) {
try {
eat();
} finally {
right.unlock();
}
}
} finally {
// 如果没有获得右手的筷子,则释放自己手里的筷子,破坏了死锁的请求和保持条件
left.unlock();
}
}
}
}
private void eat() {
log.debug("eating...");
Sleeper.sleep(1);
}
}

class Chopstick2 extends ReentrantLock {
String name;
public Chopstick2(String name) {
this.name = name;
}
@Override
public String toString() {
return "筷子{" + name + '}';
}
}

运行结果

1
2
3
4
5
6
7
8
9
10
22:56:17.080 c.Philosopher2 [亚里士多德] - eating...
22:56:17.080 c.Philosopher2 [苏格拉底] - eating...
22:56:18.093 c.Philosopher2 [柏拉图] - eating...
22:56:18.093 c.Philosopher2 [赫拉克利特] - eating...
22:56:19.093 c.Philosopher2 [苏格拉底] - eating...
22:56:19.093 c.Philosopher2 [亚里士多德] - eating...
22:56:20.101 c.Philosopher2 [亚里士多德] - eating...
22:56:20.101 c.Philosopher2 [阿基米德] - eating...
...........
程序会一直向下执行,不会产生死锁

说明

可以看到,需要使用 tryLock 方法去获取左筷子和右筷子, 如果获取失败直接结束, 另外在成功获取锁后,要在 finally 里释放锁。

六、公平锁

6.1 概念

  • 公平锁:是指多个线程按照申请锁的顺序来获取锁,通过队列FIFO,先进先出,类似排队打饭,先来后到。
  • 非公平锁:是指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取锁。**

6.2 语法

ReentrantLock默认是非公平锁

1
2
3
4
5
6
7
ReentrantLock lock = new ReentrantLock(true);  // true:公平锁
lock.lock();
try {
// todo
} finally {
lock.unlock();
}

说明

  • 初始化构造函数入参,选择是否为初始化公平锁。
  • 其实一般情况下并不需要公平锁,除非你的场景中需要保证顺序性。
  • 使用 ReentrantLock 切记需要在 finally 中关闭,lock.unlock()。
公平锁和非公平锁的选择

一点源码

1
2
3
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
  • 构造函数中选择公平锁(FairSync)、非公平锁(NonfairSync)。

6.3 代码示例

  • 非公平锁
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
import java.util.concurrent.locks.ReentrantLock;

/**
* Created by xiaowei
* Date 2022/10/25
* Description 公平锁/非公平锁
*/
public class FairLockDemo {
public static void main(String[] args) throws InterruptedException {
ReentrantLock lock = new ReentrantLock(false);
lock.lock();
for (int i = 0; i < 500; i++) {
new Thread(() -> {
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + " running...");
} finally {
lock.unlock();
}
}, "t" + i).start();
}
// 1s 之后去争抢锁
Thread.sleep(1000);
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + " start...");
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + " running...");
} finally {
lock.unlock();
}
}, "强行插入").start();
lock.unlock();
}
}

强行插入,有机会在中间输出

运行结果

image-20230328200858114

改为公平锁后

1
ReentrantLock lock = new ReentrantLock(true);

强行插入,总是在最后输出

image-20230328200957557

说明

开启公平锁后,所有的线程在entrylist中按照开始的时间顺序执行,不会出现插队现象,所以公平锁能够解决饥饿现象。不开启公平锁,当上一个线程结束后,随机从entrylist中执行一个线程。

6.4 总结

  • 公平锁 : 锁被释放之后,先申请的线程先得到锁。性能较差一些,因为公平锁为了保证时间上的绝对顺序,上下文切换更频繁。
  • 非公平锁:锁被释放之后,后申请的线程可能会先获取到锁,是随机或者按照其他优先级排序的。性能更好,但可能会导致某些线程永远无法获取到锁。

七、条件变量

7.1 概述

  • 关键字synchronized中也有条件变量,就是waitSet,可以理解为条件不满足时进入waitSet等待,一个synchronized只能对应一个waitSet。
  • ReentrantLock可以支持多个条件变量,因此可以将不同条件的线程放入等待集合中,以便于后续进行专门的唤醒。在ReentrantLock中使用条件变量需要使用await()方法。

ReentrantLock的条件变量比synchronized强大之处在于,它支持多个条件变量(对象)。

使用流程
  • 使用ReentrantLock对象创建条件变量condition;
  • 执行condition.await()前需要先获取锁;
  • 执行condition.await()后,线程会释放锁,并进入conditionObject中等待;
  • 其它线程执行condition.signal()或者condition.signalAll()唤醒conditionObject中等待的线程;
  • 被唤醒后会重新竞争锁
  • 竞争锁成功后,会从await()后的代码处开始执行

函数await()调用方式

image-20230328202644798

7.2 代码示例

  • t1需要等待烟过来, 否则就一直等待
  • t2需要等待早餐, 否则就一直等待
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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
import lombok.extern.slf4j.Slf4j;

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;

import static site.weiyikai.concurrent.utils.Sleeper.sleep;

/**
* Created by xiaowei
* Date 2022/10/25
* Description 条件变量
*/
@Slf4j(topic = "c.ConditionTest")
public class ConditionTest {
static ReentrantLock lock = new ReentrantLock();
static Condition waitCigaretteQueue = lock.newCondition();
static Condition waitbreakfastQueue = lock.newCondition();
static volatile boolean hasCigrette = false;
static volatile boolean hasBreakfast = false;
public static void main(String[] args) {
new Thread(() -> {
try {
// 如果没有拿到锁的话, 线程就会阻塞在这, 不会向下执行
lock.lock();
while (!hasCigrette) {
// 不满足条件就到对应的 waitSet 等待
try {
waitCigaretteQueue.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
log.debug("等到了它的烟");
} finally {
lock.unlock();
}
},"t1").start();
new Thread(() -> {
try {
lock.lock();
while (!hasBreakfast) {
try {
waitbreakfastQueue.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
log.debug("等到了它的早餐");
} finally {
lock.unlock();
}
},"t2").start();
sleep(1);
sendBreakfast();
sleep(1);
sendCigarette();
}
private static void sendCigarette() {
lock.lock();
try {
log.debug("送烟来了");
hasCigrette = true;
waitCigaretteQueue.signal();
} finally {
lock.unlock();
}
}
private static void sendBreakfast() {
lock.lock();
try {
log.debug("送早餐来了");
hasBreakfast = true;
waitbreakfastQueue.signal();
} finally {
lock.unlock();
}
}
}

运行结果

1
2
3
4
23:52:23.927 c.ConditionTest [main] - 送早餐来了
23:52:23.930 c.ConditionTest [t2] - 等到了它的早餐
23:52:24.931 c.ConditionTest [main] - 送烟来了
23:52:24.931 c.ConditionTest [t1] - 等到了它的烟

ReentrantLock
http://example.com/2023/03/28/ReentrantLock/
作者
程序员小魏
发布于
2023年3月28日
许可协议