ReentrantLock源码学习

学习ReentrantLock之前,先了解一下可重入锁的概念。何为可重入锁,顾名思义,就是可重入的。真是听君一席话,胜听一席话啊。

请添加图片描述

正经点,可重入锁就是能够支持同一个线程对资源的重复加锁。注意两个关键字:同一线程和重复。

像synchronized关键字也实现了可重入。用synchronized修饰的方法,在进行递归调用时,执行线程在获取了锁之后仍然能够连续多次获得该锁,并不会出现阻塞的情况。

再比如说,这篇文章要学习的ReentrantLock,也实现了可重入锁。并且ReentrantLock还支持公平锁和非公平锁(默认是非公平锁)。

1、ReentrantLock源码学习

1.1 构造方法

ReentrantLock的源码比较简单,并且它也是基于AQS实现的。先看看它的构造函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
java复制代码    /**
* Creates an instance of {@code ReentrantLock}.
* This is equivalent to using {@code ReentrantLock(false)}.
*/
public ReentrantLock() {
sync = new NonfairSync();
}

/**
* Creates an instance of {@code ReentrantLock} with the
* given fairness policy.
*
* @param fair {@code true} if this lock should use a fair ordering policy
*/
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}

默认就是非公平锁。

1.2 锁的释放

Sync类就是继承自AQS的,FairSync类和NonfairSync类又是继承自Sync。对于公平锁和非公平锁,其释放锁的逻辑都是一样的,所以在Sync类中实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
java复制代码    abstract static class Sync extends AbstractQueuedSynchronizer {
protected final boolean tryRelease(int releases) {
int c = getState() - releases;
// 判断当前线程是不是占有锁的线程,如果不是,抛异常
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
// 同步变量state的值为0时,才释放锁,返回true
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
// 设置同步变量的值
setState(c);
return free;
}
}

可以发现可重入锁的释放逻辑,对于占有锁的线程来说,只有在同步变量state的值为0的时候,才算是释放了锁。

1.3 锁的获取

锁的获取分公平锁和非公平锁。非公平锁的获取逻辑实现在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
java复制代码abstract static class Sync extends AbstractQueuedSynchronizer {
/**
* Performs non-fair tryLock. tryAcquire is implemented in
* subclasses, but both need nonfair try for trylock method.
* 非公平锁,获取锁
*/
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
// 同步变量为0,说明没有线程占用锁
if (c == 0) {
// CAS获取锁,注意这里并没有判断该线程是不是同步队列的队头
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
// 判断当前线程是不是占有锁的线程
else if (current == getExclusiveOwnerThread()) {
// 增加同步变量state的值
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
}

可以发现Sync类似并没有重写AQS的tryAcquire方法,而是放到了它的子类FairSync类和NonfairSync类中去实现的。

看看NonfairSync类的源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
java复制代码    // 非公平锁
static final class NonfairSync extends Sync {
private static final long serialVersionUID = 7316153563782823691L;

// 上来先CAS获取一下锁,如果获取失败,再调用AQS的acquire方法
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
// 调用Sync类的nonfairTryAcquire方法
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
}

看看FairSync类的源码

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
java复制代码    // 公平锁
static final class FairSync extends Sync {
private static final long serialVersionUID = -3000897897090466540L;
// 直接调用AQS的acquire方法
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();
// 同步变量为0,说明没有线程占有锁
if (c == 0) {
/**
* 判断同步队列中当前节点是否有前驱节点,也就是只有当前节点是头结点并且CAS成功的情况下,当
* 前线程才能占有锁
*/
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
// 当前线程已经占有锁,则增加state变量的值
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
}

可以发现公平锁和非公平锁在获取锁的时候,唯一的差别就是公平锁判断了当前节点是不是头结点,只有是头结点的情况下才可能获取到锁。非公平锁就不一样了,上来就直接CAS。

上面的方法都是在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
java复制代码// 获取锁,在等待获取锁的过程中休眠并禁止一切线程调度
public void lock() {
sync.lock();
}

// 在等待获取锁的过程中可被中断
public void lockInterruptibly() throws InterruptedException {
sync.acquireInterruptibly(1);
}

// 尝试获取锁,获取到锁并返回true;获取不到并返回false
public boolean tryLock() {
return sync.nonfairTryAcquire(1);
}

// 在指定时间内等待获取锁;过程中可被中断
public boolean tryLock(long timeout, TimeUnit unit)
throws InterruptedException {
return sync.tryAcquireNanos(1, unit.toNanos(timeout));
}

// 释放锁
public void unlock() {
sync.release(1);
}

2、测试

测试一下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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
java复制代码import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.locks.ReentrantLock;

public class FairAndUnfairTest {

private static Sync fairLock = new Sync(true);
private static Sync noFairLock = new Sync(false);

public static void testLock(Sync lock) {
// 开启5个线程
for (int i = 0; i < 5; i++) {
new Thread(new Job(lock), String.valueOf(i)).start();
}
}

private static class Job extends Thread {
private Sync lock;
public Job(Sync lock) {
this.lock = lock;
}

public void run() {
for (int i = 0; i < 2; i++) {
lock.lock();
System.out.println("locked by " + currentThread().getName() + ", waiting by " + lock.getQueueThreads());
lock.unlock();
}
}
}

private static class Sync extends ReentrantLock {
public Sync(boolean fair) {
super(fair);
}

/**
* 获取等待队列
* @return
*/
public List<String> getQueueThreads() {
List<Thread> arrayList = new ArrayList<Thread>(super. getQueuedThreads());
Collections.reverse(arrayList);
List<String> list = new ArrayList<>();
arrayList.forEach(el -> {
list.add(el.getName());
});
return list;
}
}

public static void main(String[] args) {
//testLock(fairLock);
testLock(noFairLock);
}
}

公平锁输出如下

1
2
3
4
5
6
7
8
9
10
java复制代码locked by 0, waiting by [1, 2]
locked by 1, waiting by [2, 4, 3, 0]
locked by 2, waiting by [4, 3, 0, 1]
locked by 4, waiting by [3, 0, 1, 2]
locked by 3, waiting by [0, 1, 2, 4]
locked by 0, waiting by [1, 2, 4, 3]
locked by 1, waiting by [2, 4, 3]
locked by 2, waiting by [4, 3]
locked by 4, waiting by [3]
locked by 3, waiting by []

非公平锁输出如下

1
2
3
4
5
6
7
8
9
10
java复制代码locked by 0, waiting by [2]
locked by 0, waiting by [2, 3, 1, 4]
locked by 2, waiting by [3, 1, 4]
locked by 2, waiting by [3, 1, 4]
locked by 3, waiting by [1, 4]
locked by 3, waiting by [1, 4]
locked by 1, waiting by [4]
locked by 1, waiting by [4]
locked by 4, waiting by []
locked by 4, waiting by []

可以发现公平锁总是按照顺序来依次获取锁。而非公平锁却是连续获取。回顾nonfairTryAcquire(int acquires)方法,当一 个线程请求锁时,只要获取了同步状态即成功获取锁。在这个前提下,刚释放锁的线程再次获取同步状态的几率会非常大,使得其他线程只能在同步队列中等待。

非公平锁可能会出现线程饥饿的情况,当竞争的线程很多时,后面的线程可能一直都获取不到锁。那为啥ReentrantLock默认是非公平锁呢?经过上面的测试可以发现,在公平锁的情况下,线程进行了10次上下文切换,非公平锁情况下只进行了5次。

线程上下文切换是一个耗费时间和资源的操作,所以在线程竞争激烈的情况下,非公平锁无疑能够节省很多的资源。

总结一下就是:公平性锁保证了锁的获取按照FIFO原则,而代价是进行大量的线程切换。非公平性锁虽然可能造成线程“饥饿”,但极少的线程切换,保证了其更大的吞吐量。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

0%