引言

公平锁实现了先进先出的公平性,但是由于来一个线程就加入队列中,往往都需要阻塞,再由阻塞变为运行,这种上下文切换非常资源。
非公平锁由于允许插队,所以上下文切换少,能保证的大的吞吐量,但是容易出现饥饿问题。

查看ReentrantLock源码,其内部有三个重要的内部类:
Sync继承自AbstractQueueSynchronizer同步器,这个锁的同步操作由这个Sync来保证。
NofairSync非公平同步器,继承Sync,在同步的基础上,保证线程获取锁是非公平的。
FairSync公平同步器,继承Sync,公平锁的体现。

ReentrantLock默认初始化为非公平锁。

在执行ReentrantLock的Lock方法的时候,会调用Sync的Lock方法,最后由NofairSync或者FairSync实现。

1
2
3
4
5
6
7
8
9
 public void lock() {
sync.lock();
}
/**
* Performs {@link Lock#lock}. The main reason for subclassing
* is to allow fast path for nonfair version.
*/
abstract void lock();

首先看NonfairSync

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* Performs lock. Try immediate barge, backing up to normal
* acquire on failure.
* 执行获取锁的操作,尝试获取锁,如果失败的话,进入同步队列等待。
*/
final void lock() {
//CAS尝试获取锁。
if (compareAndSetState(0, 1))
//如果获取锁成功的话,将自己放在当前持有锁的线程位置。
setExclusiveOwnerThread(Thread.currentThread());
else
//获取锁失败,直接入同步队列,这块就和公平锁一样了。
acquire(1);
}

非公平锁在执行获取锁操作的时候,会先尝试的获取一下锁,即和同步队列中等待的线程竞争,如果获取失败的时候才会入同步队列,
这步操作即减少了入队、阻塞、出队的操作,能增大锁处理的吞吐量,但是也有可能同步队列中的等待线程迟迟不能获取到锁,因为都被新来的抢占了,发生饥饿现象。
源码中,当前获取锁失败之后会调用AQS中定义的acquire方法,最后又会回调本类中的tryAcquire(int acquires)方法:

1
2
3
4
5
6
7
8
9
/**
* 由父类回调,尝试获取锁
* @param acquires
* @return
*/
protected final boolean tryAcquire(int acquires) {
//非公平方式获取
return nonfairTryAcquire(acquires);
}

而nonfairTryAcquire方法的逻辑功能因为ReentrantLock的tryLock()方法与非公平同步器都需要使用,所以提到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
/**
* Performs non-fair tryLock. tryAcquire is implemented in
* subclasses, but both need nonfair try for trylock method.
* 执行非公共方式获取锁,按理说应该子类实现,但是ReentrantLock的tryLock()方法也需要这块的逻辑,所以提到了上层处理。
*/
final boolean nonfairTryAcquire(int acquires) {
//获取当前线程
final Thread current = Thread.currentThread();
//获取当前锁状态
int c = getState();
//如果锁状态为0,即没有线程持有当前锁
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的可重入性,在当前线程再次获取锁的时候,发现获取失败了,而且当前持有锁的线程正是自己,
那就将锁的重入次数增加,减少了再次获取锁的消耗。

再来看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
/**
* 获取锁
*/
final void lock() {
//直接调用AQS的方法,最后会回调tryAcquire方法
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();
//获取当前锁的状态,即AQS中的state属性
int c = getState();
//c==0即锁没有被占用
if (c == 0) {

//首先检查自己是不是处于头节点的后继节点,即队列中有没有排在我前面的节点
//之后将state设置为1
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
//上面两步都成功之后,将AbstractOwnableSynchronizer的exclusiveOwnerThread设置为当前线程,就此获取锁成功。
setExclusiveOwnerThread(current);
//获取锁成功
return true;
}
}
//如果当前的线程持有这个锁
else if (current == getExclusiveOwnerThread()) {
//记录锁重入次数
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
//获取锁失败
return false;
}

源码中,没有了非公平锁的一进来就和同步队列中的等待线程竞争,而是直接进入同步队列,接下来就是AQS的问题了,以前分析过就不写了。

直接将锁的释放也分析一下:

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
/**
* 释放锁执行操作,因为想要释放锁,肯定是持有锁,所以此方法内部不需要同步方法。
* @param releases 释放次数
* @return
*/
protected final boolean tryRelease(int releases) {
//获取释放后当前锁被重入次数
int c = getState() - releases;
//为什么还要判断一下当前线程是不是获取锁的线程呢?
//可能是:如果有线程没有调用Lock但是先效用Unlock的时候,做一个判断。
//为什么不把这句判断放到 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;
}

这里面有两个疑问,目前只能瞎猜。