ReentrantLock那些事儿(二)

ReentrantLock那些事儿(二)

薛定谔的汪

接着上篇写,了解了什么是公平锁和非公平锁的概念后,继续分析 ReentrantLock,它使用一个静态抽象类 Sync来管理公平锁和非公平锁:

1
2
3
abstract static class Sync extends AbstractQueuedSynchronizer {
...
}

Sync 有两个子类,一个是FairSync,实现了公平锁,另一个是NonfairSync,它是非公平锁的实现方式。

加锁

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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
static final class FairSync extends Sync {
private static final long serialVersionUID = -3000897897090466540L;

//获取锁
final void lock() {
//从AbstractQueuedSynchronizer继承而来的方法
acquire(1);
}

//从AbstractQueuedSynchronizer继承而来,放到这里好理解
public final void acquire(int arg) {
/* 先调用了tryAcquire(1),也就是FairSync重写的tryAcquire(int acquires)尝试获取下锁,
* 如果获取成功,返回true,acquireQueued(addWaiter(Node.EXCLUSIVE), arg))不回去执行、
* 程序结束。
*/
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) //尝试获取锁失败,则把线程放入阻塞队列
//根据返回值判断是否打断当前线程
selfInterrupt();
}

/**
* 重写了从AbstractQueuedSynchronizer的tryAcquire(...)方法
*/
protected final boolean tryAcquire(int acquires) {
//当前线程
final Thread current = Thread.currentThread();
int c = getState();
//c==0,表示锁空闲,可以获取锁
if (c == 0) {
/* 因为是公平锁,所以要判断阻塞队列中是否有复合条件获取锁的线程,
* 没有的话自己通过 CAS 机制获取锁并更新state
* 用 CAS 更新是有可能刚判断完当前线程可以去获得锁,还没真正获得锁的时候,
* 锁刚好被其他线程占用了
*/
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
//标记是当前线程取到了锁
setExclusiveOwnerThread(current);
return true;
}
}
/*
* 判断拥有锁的线程是否是当前线程,是的话就标识重入,对state+1
*/
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
//如果 if-新获取锁 和 else if-锁重入 都没返回 true,就返回false
return false;
}
}

我画了一张公平锁获取锁的流程图:

NoFairSync

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
static final class NonfairSync extends Sync {
private static final long serialVersionUID = 7316153563782823691L;

/**
* 获取锁
*/
final void lock() {
//CAS获取锁
if (compareAndSetState(0, 1))
//设置当前线程获取锁
setExclusiveOwnerThread(Thread.currentThread());
else
// CAS获取锁失败,acquire(1)内调用tryAcquire,
// 非公平锁的tryAcquire调用从父类Sync继承而来的nonfairTryAcquire
acquire(1);
}

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

nonfairTryAcquire是其父类 Sync 的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
final boolean nonfairTryAcquire(int acquires) {
//当前线程
final Thread current = Thread.currentThread();
int c = getState();
//非公平锁不用排队直接去竞争
if (c == 0) {
if (compareAndSetState(0, acquires)) {
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;
}

我们发现非公平锁的源码更容易理解,公平锁在获取锁时还要判断有没有轮到自己获得锁,非公平锁去掉这部判断直接去竞争锁。

注:非公平锁可能会造成线程饥饿,但它的吞吐量要远大于公平锁,所以 ReentrantLock 默认使用非公平锁,其构造方法可以传入一个布尔值来标识是否使用公平锁:

1
2
3
4
5
6
7
public ReentrantLock() {
sync = new NonfairSync();
}

public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}

CAS 方法

1
2
3
4
protected final boolean compareAndSetState(int expect, int update) {
// See below for intrinsics setup to support this
return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}

compareAndSetState是从AQS 继承而来,使用的是unsafe 的 CAS 方法,后面还会分析 Atomic包下的原子类,其实现原子操作都是基于 unsafe的 CAS 方法。

CAS (Compare And Swap)是并发系统中,实现原子操作和锁(乐观锁)的常见手段。

比较并交换,通过传入旧值与新值,保证只有在待修改值与旧值相等时才执行赋值为新值,只要保证了整个过程的原子性,则使用者可以返回值判断并重试的方式保证并发环境下的安全性。

扩展

CAS存在ABA 问题:

线程1准备用CAS将变量的值由A替换为B,在此之前,线程2将变量的值由A替换为C,又由C替换为A,然后线程1执行CAS时发现变量的值仍然为A,CAS成功。但实际上这时的A 已经不是原来的 A 了,可能会引发一些问题。

ABA 解决办法:

每次更新记录版本号。

解锁

1
2
3
public void unlock() {
sync.release(1);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
protected final boolean tryRelease(int releases) {
//当前state-1 因为锁可重入,解锁一次-1
int c = getState() - releases;
//当前线程持有锁才可以解锁,否则抛出IllegalMonitorStateException异常,前面提到过
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) {
//c==0时才真正释放锁
free = true;
//设置锁为空闲
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}

总结

两个线程 t1 和 t2,t1和t2并发去执行lock()方法获得锁,假设t1先获取锁(state=0),state加1,如果t1在执行的过程中又执行了lock()方法,t1会直接获取锁,这就是锁重入,state再加1,ReentrantLock判断锁重入是根据当前线程是否是当前获取锁的线程,如果是则重入。

t2去获得锁时,如果t1释放锁,如果是公平锁则需要检查同步阻塞队列中是否有符合获取锁条件的其他线程,如果有则继续阻塞,如果没有则获取锁;如果是非公平锁则直接 CAS 获取锁。

如果t1还没释放锁,则将t2线程封装到 Node 节点插入同步阻塞队列的尾部,并阻塞t2,等待唤醒,当t1释放后,开始从同步等待队列的头节点开始唤醒阻塞线程t2,t2获取锁。

线程每次执行unlock()方法,state减1,直至 state等于0时锁才真正释放。

  • Title: ReentrantLock那些事儿(二)
  • Author: 薛定谔的汪
  • Created at : 2018-08-15 18:01:54
  • Updated at : 2023-11-17 19:37:37
  • Link: https://www.zhengyk.cn/2018/08/15/java/ReentrantLock-2/
  • License: This work is licensed under CC BY-NC-SA 4.0.
On this page
ReentrantLock那些事儿(二)