JUC锁之ReentrantLock

前面,我们介绍了Java中非常重要的同步器框架AQS,本文将介绍Java类库中基于AQS实现的一个同步器ReentrantLock。同样地,我们还是循着:它是什么?它有什么用?它底层怎么实现的?怎么使用它?这些问题,来深入了解ReentrantLock。

ReentrantLock概述

ReentrantLock,翻译成中文就是可重入锁,它是Java类库中基于AQS框架实现的一个可重入锁同步器。在介绍ReentrantLock之前,先了解下可重复锁相关的知识。

可重入锁

什么是可重入锁?

可重入锁,顾名思义就是可以重复申请,指一个线程可以多次获取一把锁。比如:一个线程在执行一个带锁的方法,该方法中又调用了另一个需要相同锁的方法,这是该线程可以直接调用该方法,而无需重新获得锁,这样就能避免死锁现象。

Java中的ReentrantLock和synchronized都是可重入锁。

可重入锁有什么用?

最大的作用就是避免死锁。

怎么实现可重入锁?

通常情况下为每个锁关联一个获取计数器和一个所有者线程,当计数值为0的时候,这个锁就没有被任何线程持有。

什么场景下需要用它?

在很多情况下,线程需要多次进入锁内执行任务。

场景一: 递归调用 。

场景二: 此线程调用同一对象其它synchronized或者有同步锁函数。

ReentrantLock类结构

ReentrantLock实现了Lock接口,Lock接口中定义了lock与unlock相关操作,并且还存在newCondition方法,表示生成一个条件。

1
public class ReentrantLock implements Lock, java.io.Serializable

ReentrantLock总共有三个内部类,并且三个内部类是紧密相关的,下图展示了三个类的关系:

其中Sync类继承自AQS抽象类,NonfairSync和FairSync都继承自Sync类。从名字就可以看出,ReentrantLock同时支持公平锁和非公平锁,两者分别基于NonfairSync和FairSync实现的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* 默认构造方法,非公平锁
*/
public ReentrantLock() {
sync = new NonfairSync();
}

/**
* true公平锁,false非公平锁
* @param fair
*/
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}

ReentrantLock实现原理

Sync类

Sync类的源码如下:

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
79
80
81
82
83
84
85
86
87
abstract static class Sync extends AbstractQueuedSynchronizer {
// 序列号
private static final long serialVersionUID = -5179523762034025860L;

// 获取锁
abstract void lock();

// 非公平方式获取
final boolean nonfairTryAcquire(int acquires) {
// 当前线程
final Thread current = Thread.currentThread();
// 获取状态
int c = getState();
if (c == 0) { // 表示没有线程正在竞争该锁
if (compareAndSetState(0, acquires)) { // 比较并设置状态成功,状态0表示锁没有被占用
// 设置当前线程独占
setExclusiveOwnerThread(current);
return true; // 成功
}
}
else if (current == getExclusiveOwnerThread()) { // 当前线程拥有该锁
int nextc = c + acquires; // 增加重入次数
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
// 设置状态
setState(nextc);
// 成功
return true;
}
// 失败
return false;
}

// 试图在共享模式下获取对象状态,此方法应该查询是否允许它在共享模式下获取对象状态,如果允许,则获取它
protected final boolean tryRelease(int releases) {
int c = getState() - releases;
if (Thread.currentThread() != getExclusiveOwnerThread()) // 当前线程不为独占线程
throw new IllegalMonitorStateException(); // 抛出异常
// 释放标识
boolean free = false;
if (c == 0) {
free = true;
// 已经释放,清空独占
setExclusiveOwnerThread(null);
}
// 设置标识
setState(c);
return free;
}

// 判断资源是否被当前线程占有
protected final boolean isHeldExclusively() {
// While we must in general read state before owner,
// we don't need to do so to check if current thread is owner
return getExclusiveOwnerThread() == Thread.currentThread();
}

// 新生一个条件
final ConditionObject newCondition() {
return new ConditionObject();
}

// Methods relayed from outer class
// 返回资源的占用线程
final Thread getOwner() {
return getState() == 0 ? null : getExclusiveOwnerThread();
}
// 返回状态
final int getHoldCount() {
return isHeldExclusively() ? getState() : 0;
}

// 资源是否被占用
final boolean isLocked() {
return getState() != 0;
}

/**
* Reconstitutes the instance from a stream (that is, deserializes it).
*/
// 自定义反序列化逻辑
private void readObject(java.io.ObjectInputStream s)
throws java.io.IOException, ClassNotFoundException {
s.defaultReadObject();
setState(0); // reset to unlocked state
}
}

NonfairSync类

NonfairSync类继承了Sync类,表示采用非公平策略获取锁,其实现了Sync类中抽象的lock方法,源码如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 非公平锁
static final class NonfairSync extends Sync {
// 版本号
private static final long serialVersionUID = 7316153563782823691L;

// 获得锁
final void lock() {
if (compareAndSetState(0, 1)) // 比较并设置状态成功,状态0表示锁没有被占用
// 把当前线程设置独占了锁
setExclusiveOwnerThread(Thread.currentThread());
else // 锁已经被占用,或者set失败
// 以独占模式获取对象,忽略中断
acquire(1);
}

protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
}

NonfairSync的非公平性体现在哪里?

从lock方法可以看出, 每一次都尝试获取锁 , 而并不会按照公平等待的原则进行等待,让等待时间最久的线程获得锁。

FairSyn类

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
// 公平锁
static final class FairSync extends Sync {
// 版本序列化
private static final long serialVersionUID = -3000897897090466540L;
final void lock() {
// 以独占模式获取对象,忽略中断
acquire(1);
}

/**
* Fair version of tryAcquire. Don't grant access unless
* recursive call or no waiters or is first.
*/
// 尝试公平获取锁
protected final boolean tryAcquire(int acquires) {
// 获取当前线程
final Thread current = Thread.currentThread();
// 获取状态
int c = getState();
if (c == 0) { // 状态为0
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) { // 不存在已经等待更久的线程并且比较并且设置状态成功
// 设置当前线程独占
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) { // 状态不为0,即资源已经被线程占据
// 下一个状态
int nextc = c + acquires;
if (nextc < 0) // 超过了int的表示范围
throw new Error("Maximum lock count exceeded");
// 设置状态
setState(nextc);
return true;
}
return false;
}
}

跟踪lock方法的源码可知,当资源空闲时,它总是会先判断sync队列(AbstractQueuedSynchronizer中的数据结构)是否有等待时间更长的线程,如果存在,则将该线程加入到等待队列的尾部,实现了公平获取原则。

总结

通过分析前文的源代码,我们可以发现ReentrantLock可重入的关键在于:FairSync和NonfairSync中tryAcquire和tryRelease方法的实现。

  • tryAcquire()方法

    每次申请锁时,如果锁被占用了,那么不会直接退出,而是判断当前占有所的线程是不是自己,如果是自己就执行int nextc = c + acquires;setState(nextc); 实现锁的可重入。

  • tryRelease()方法

    释放锁的时候不是直接将锁状态置为0,而是执行int c = getState() - releases;setState(c); ,相当于释放部分资源量。

ReentrantLock的应用

ReentrantLock作为公平锁示例

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
package com.hust.grid.leesf.abstractqueuedsynchronizer;

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

class MyThread extends Thread {
private Lock lock;
public MyThread(String name, Lock lock) {
super(name);
this.lock = lock;
}

public void run () {
lock.lock();
try {
System.out.println(Thread.currentThread() + " running");
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
} finally {
lock.unlock();
}
}
}

public class AbstractQueuedSynchonizerDemo {
public static void main(String[] args) throws InterruptedException {
Lock lock = new ReentrantLock(true);

MyThread t1 = new MyThread("t1", lock);
MyThread t2 = new MyThread("t2", lock);
MyThread t3 = new MyThread("t3", lock);
t1.start();
t2.start();
t3.start();
}
}

ReentrantLock v.s Synchronized

  • 可重入锁

    两者支持可重入,Synchronized是本地方法C++实现的,而ReentrantLock是JUC中的类,基于AQS实现。

  • 实现方式

    • ReentrantLock是轻量级锁,基于AQS,即cas+volatile管理线程,无需线程切换,属于乐观锁。
    • Synchronized是重量级锁,锁被占用是,阻塞当前线程, 需要将线程从内核态和用户态来回切换,属于悲观锁。
  • 使用方式

    • ReentrantLock 可以修饰实例方法,静态方法,代码块。自动释放锁。
    • Synchronized 一般需要try catch finally语句,在try中获取锁,在finally释放锁。需要手动释放锁。
  • 公平性

    • ReentrantLock 支持公平锁和非公平锁两种,默认是非公平的。
    • Synchronized 只有非公平锁。
  • 中断

    • Synchronized是不可中断的。
    • ReentrantLock提供可中断和不可中断两种方式。其中lockInterruptibly方法表示可中断,lock方法表示不可中断。
  • 条件队列

    • Synchronized只有一个等待队列。
    • ReentrantLock中一把锁可以对应多个条件队列。通过newCondition表示。

参考资料

  1. https://www.cnblogs.com/leesf456/p/5383609.html
  2. https://pdai.tech/md/java/thread/java-thread-x-lock-ReentrantLock.html
  3. https://segmentfault.com/a/1190000039091031
打赏
  • 版权声明: 本博客所有文章除特别声明外,著作权归作者所有。转载请注明出处!
  • Copyrights © 2021-2022 Yin Peng
  • 引擎: Hexo   |  主题:修改自 Ayer
  • 访问人数: | 浏览次数:

请我喝杯咖啡吧~

支付宝
微信