JUC高并发-公平锁与非公平锁

  1. 1. 锁的公平性和非公平性
    1. 1.1. 1、公平锁FairSync:
    2. 1.2. 2、非公平锁:
  2. 2. 源码分析:
    1. 2.1. 对比公平锁与非公平锁源码:
    2. 2.2. ReentrantLock锁的释放

​ ReentrantLock的实现是基于其内部类 FairSync(公平锁)和 NonFairSync(非公平锁)实现的。

​ ReentrantLock公平锁与非公平锁的实现原理区别就是抽象方法 tryAcquire 的实现不同。

锁的公平性和非公平性

​ 在并发环境中,多个线程需要对同一资源进行访问,同一时刻只能有一个线程能够获取到锁并进行资源访问,那么剩下的这些线程怎么办呢?

1、公平锁FairSync:

公平锁:多个线程按照申请锁的顺序来获取锁,采用先来后到,先来先服务的原则。

​ 公平锁的优点是等待锁的线程不会饿死,(效率高)。

​ 缺点是整体吞吐效率相对非公平锁要低,等待队列中除第一个线程以外的所有线程都会阻塞,CPU唤醒阻塞线程的开销比非公平锁大。

2、非公平锁:

​ 非公平锁是多个线程加锁时直接尝试获取锁,获取不到才会到等待队列的队尾等待。但如果此时锁刚好可用,那么这个线程可以无需阻塞直接获取到锁,所以非公平锁有可能出现后申请锁的线程先获取锁的场景。(如插队现象)

非公平锁的优点是可以减少唤起线程的开销,整体的吞吐效率高,因为线程有几率不阻塞直接获得锁,CPU不必唤醒所有线程。

缺点是处于等待队列中的线程可能会饿死,或者等很久才会获得锁。(效率低)

源码分析:

ReentrantLock 我们可以查看它的源码,无参构造函数默认使用的是非公平锁。也可以指定构造函数的boolean类型来得到公平锁或者非公平锁。

​ 例如我们创建了一个 ReetrantLock,并给构造函数传了一个 true,我们可以查看 ReetrantLock 的构造函数。

如果是 true 的话,那么就会创建一个 ReentrantLock 的公平锁,然后并创建一个 FairSync ,FairSync 其实是一个 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
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) {
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
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;
}
}

Sync 是 ReentrantLock 中的内部类,Sync 继承 AbstractQueuedSynchronizer 类,AbstractQueuedSynchronizer 就是我们常说的 AQS ,它是 JUC(java.util.concurrent) 中最重要的一个类,通过它来实现独占锁和共享锁。

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

此处有两个值需要关心:

1
2
3
4
5
6
7
8
9
10
11
//持有该锁的当前线程
private transient Thread exclusiveOwnerThread;
-----------------两个值不在同一个类----------------
/**
* 同步状态
* 0: 初始状态-无任何线程得到了锁
* > 0: 被线程持有, 具体值表示被当前线程持有的执行次数
* 这个字段在解锁的时候也需要用到。
* 注意这个字段的修饰词: volatile
*/
private volatile int state;

对比公平锁与非公平锁源码:

​ 我们可以明显的看出公平锁与非公平锁的lock()方法唯一的区别就在于公平锁在获取同步状态时多了一个限制条件:**hasQueuedPredecessors()**。

公平锁的实现机理在于每次有线程来抢占锁的时候,都会检查一遍有没有等待队列,如果有, 当前线程会执行如下步骤:

1
2
3
4
if (!hasQueuedPredecessors() && compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}

而 hasQueuedPredecessors() 是 AQS 中的方法,它主要是用于检查是否有等待队列的。。

ReentrantLock 的公平锁和非公平锁都委托了 AbstractQueuedSynchronizer#acquire 去请求获取。

  • tryAcquire 是一个抽象方法,是公平与非公平的实现原理所在。
  • addWaiter 是将当前线程结点加入等待队列之中。公平锁在锁释放后会严格按照等到队列去取后续值,而非公平锁在对于新晋线程有很大优势。
  • acquireQueued 在多次循环中尝试获取到锁或者将当前线程阻塞。
  • selfInterrupt 如果线程在阻塞期间发生了中断,调用 Thread.currentThread().interrupt() 中断当前线程。
  • ReentrantLock 对线程的阻塞是基于 LockSupport.park(this); (AbstractQueuedSynchronizer#parkAndCheckInterrupt,先决条件是当前节点有限次尝试获取锁失败。)

ReentrantLock锁的释放

​ ReentrantLock锁的释放是逐级释放的,也就是说在可重入性场景中,必须要等到场景内所有的加锁的方法都释放了锁, 当前线程持有的锁才会被释放
释放的方式很简单, state字段减一即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
protected final boolean tryRelease(int releases) {
// releases = 1
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;
}