并发学习(十) ReentrantLock源码分析

ReentrantLock简介

重入锁,顾名思义,就是支持重进入的锁,它表示锁能够支持一个县城对资源的重复加锁。除此之外,该锁还支持获取锁的时公平和非公平性选择。ReentrantLock功能上与synchronized关键字类似。

ReentrantLock是基于AbstractQueuedSynchronizer(AQS)实现的。

ReentrantLock与synchronized关键字的比较

  • synchronized是在JVM层面的(monitor),而ReentrantLock是Java类
  • synchronized不支持响应中断,ReentrantLock可以响应中断
  • synchronized不支持超时等待,ReentrantLock支持超时等待
  • synchronized只支持非公平锁,ReentrantLock都支持
  • synchronized不支持尝试加锁,ReentrantLock支持尝试加锁(tryLock())
  • synchronized自动获取、释放锁,ReentrantLock需要手动获取、释放
  • synchronized在返回或抛出异常时释放锁,ReentrantLock需要在finally释放锁

实现可重入

重进入是指任意线程在获取到锁之后能够再次获取该锁而不会被锁阻塞,该特性的实现需要解决以下两个问题:

  • 线程再次获取锁。锁需要识别获取锁的线程是否为当前占据锁的线程,如果是,则再次成功获取。
  • 锁的最终释放。线程重复n次获取了锁,随后在第n次释放该锁后,其他线程能够获取到该锁。锁的最终释放要求所对于获取进行计数自增,计数表示当前锁被重复获取的次数,而锁被释放时,计数自减,当计数等于0时表示锁已经成功释放。

公平与非公平获取锁的区别

公平性是针对获取锁而言的,如果一个锁是公平的,那么锁的获取顺序就应该符合请求的绝对时间顺序,也就是FIFO。
在非公平模式下,线程会通过“插队”的方式去抢占锁,抢不到的则进入同步队列进行排队。默认情况下,ReentrantLock 使用的是非公平模式获取锁,而不是公平模式。不过我们也可通过 ReentrantLock 构造方法ReentrantLock(boolean fair)调整加锁的模式。

为什么默认设置为非公平锁

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

源码分析

ReentrantLock是内部类Sync继承AbstractQueuedSynchronizer,并重写抽象方法。

ReentrantLock分为公平锁以及非公平锁,而公平性是在构造方法ReentrantLock(boolean fair)中设定的

而ReentrantLock根据公平性分为了FairSync和NonfairSync两个继承了sync的内部类,所以lock实现也不同,下面根据公平性逐个分析。

公平锁

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
final void lock() {
acquire(1);
}


public final void acquire(int arg) {
//AQS的方法,这里不再赘述
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}


//复写AQS的tryAcquire方法
protected final boolean tryAcquire(int acquires) {
//获取当前线程
final Thread current = Thread.currentThread();
//获取同步状态
int c = getState();
//如果同步状态为0,说明没有线程获取锁
if (c == 0) {
//查看是否有前驱结点,如果有,让前面的线程先获取锁(公平锁FIFO)
//如果没有,用CAS将同步状态设为1
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
// 将当前线程设置为独占持有
setExclusiveOwnerThread(current);
return true;
}
}
//如果c不为0,并且当前线程是获取锁的线程
else if (current == getExclusiveOwnerThread()) {
//锁的可重入
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
//设置同步状态
setState(nextc);
return true;
}
return false;
}

public final boolean hasQueuedPredecessors() {
// The correctness of this depends on head being initialized
// before tail and on head.next being accurate if the current
// thread is first in queue.
Node t = tail; // Read fields in reverse initialization order
Node h = head;
Node s;
// h != t 表示队列非空,并且当前线程的结点前面还有线程 返回true
return h != t &&
((s = h.next) == null || s.thread != Thread.currentThread());
}

非公平锁

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
final void lock() {
//先尝试通过CAS修改同步状态来抢占锁,一开始就体现了非公平性的“插队”
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}

public final void acquire(int arg) {
//AQS的方法,这里不再赘述
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}

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

final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
//与公平锁不同的是,并没有判断是否有前驱结点
// 调用 CAS 加锁,如果失败,则说明有其他线程在竞争获取锁
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
//如果c不为0,并且当前线程是获取锁的线程,则可重入
else if (current == getExclusiveOwnerThread()) {
// 计算重入后的同步状态值,acquires 一般为1
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}

公平锁与非公平锁代码差异

1
2
3
4
5
6
7
8
9
10
11
12
// 公平锁
acquire(1);

if (!hasQueuedPredecessors() && compareAndSetState(0, acquires))

// 非公平锁
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);

if (compareAndSetState(0, acquires))

通过代码可知道公平性体现两点,第一是在最开始的lock方法中非公平锁直接尝试CAS修改同步状态,如果成功则直接获取锁,第二是在是否检查有前驱结点,公平锁如果发现有前驱结点根据FIFO原则应该让前驱结点先获取同步状态,而非公平锁不用判断直接用CAS设置了同步状态,也体现了非公平锁的“插队”特性。

释放锁

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
public void unlock() {
sync.release(1);
}

public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}

protected final boolean tryRelease(int releases) {
//用当前同步状态减去释放量,因为之前lock方法中是加上,与之相反
int c = getState() - releases;
//如果当前线程没有获取锁,抛出异常
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
//到这里表示当前线程是获取锁的线程
boolean free = false;
//c=0 表示锁已经完全释放了
if (c == 0) {
//当前无锁
free = true;
//设置成无锁状态
setExclusiveOwnerThread(null);
}
//设置同步状态
setState(c);
return free;
}

总结

首先,在学完了AQS来看ReentrantLock真的很好理解,主要ReentrantLock也不是很难,主要是要理解公平性体现在代码的什么地方,如何实现可重入,并且释放的具体操作,最后就是公平性与非公平性的优缺点,在实际开发中没有好坏之分,要根据实际情况来选择公平性。

参考资料

  • Java并发编程的艺术