Java中的公平锁和非公平锁实现详解
在Java中实现锁的方式有两种,一种是使用Java自带的关键字
synchronized
对相应的类或者方法以及代码块进行加锁,另一种是
ReentrantLock
,前者只能是非公平锁,而后者是默认非公平但可实现公平的一把锁。
ReentrantLock
的实现是基于其内部类
FairSync
(公平锁)
和
NonFairSync
(非公平锁)
实现的。 其可重入性是基于
Thread.currentThread()
实现的: 如果当前线程已经获得了执行序列中的锁, 那执行序列之后的所有方法都可以获得这个锁。
在本文将集中对ReentrantLock以及它实现公平锁和非公平锁的方式进行讲解:
首先我们来看一段ReentrantLock的源代码,有助于我们等会对非公平和公平锁的理解:
public class ReentrantLockExample {
int a=0;
ReentrantLock lock= new ReentrantLock();
public void write() {
lock.lock(); // 获取锁
try {
a++;
}finally {
lock.unlock(); // 释放锁
}
}
public void reader() {
lock.lock(); // 获取锁
try {
int i=a;
...
} finally {
lock.unlock(); // 释放锁
}
}
}
ReentrantLock的实现依赖于Java同步器框架AbstractQueuedSynchronizer(AQS)。AQS使用一个整形的volatile变量state来维护同步状态,这个volatile变量是实现ReentrantLock的关键。我再看一下下面ReentrantLock的类图
公平锁:
公平和非公平锁的队列都基于锁内部维护的一个双向链表,表结点Node的值就是每一个请求当前锁的线程。公平锁则在于每次都是依次从队首取值。
锁的实现方式是基于如下几点:
表结点Node和状态
state
的volatile关键字。
sum.misc.Unsafe.compareAndSet的原子操作。
非公平锁:
在等待锁的过程中, 如果有任意新的线程妄图获取锁,都是有很大的几率直接获取到锁的。
ReentrantLock锁都不会使得线程中断,除非开发者自己设置了中断位。
ReentrantLock获取锁里面有看似自旋的代码,但是它不是自旋锁。
ReentrantLock公平与非公平锁都是属于排它锁。
ReentrantLock的可重入性分析
这里有一篇对锁介绍甚为详细的文章
朱小厮的博客-Java中的锁
.
synchronized的可重入性
参考这篇文章:
Java内置锁synchronized的可重入性
。
不过考虑到基础知识不一,如果对锁的概念不太清晰的也可参考我这篇博客
https://blog.csdn.net/weixin_42504145/article/details/84995202
jvm—静态方法加锁和非静态方法加锁的区别
ReentrantLock的可重入性
前言里面提到,ReentrantLock重入性是基于Thread.currentThread()实现的: 如果当前线程已经获得了锁, 那该线程下的所有方法都可以获得这个锁。ReentrantLock的锁依赖只有 NonfairSync和FairSync两个实现类, 他们的锁获取方式大同小异。
可重入性的实现基于下面代码片段的
else if
语句)(如果想要真正理解ReentrantLock的课重入性要仔细理解上面那片博客,以及下面给出的else if 语句)
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState(); // 获取锁的开始,首先读volatile变量
if (c == 0) { // 这个if条件是没有其他线程获取该对象的锁,我们用CAS尝试获取锁
if(ifFirst(current) && compareAndSetState(0,acquires)){
setExclusiveOwnerThread(current);
return(true) // 这个方法在cas成功后,将对象的Mark Word里的锁标志改成该线程
}
}
else if (current == getExclusiveOwnerThread()) {
// 是当前线程,直接获取到锁。实现可重入性。
int nextc = c + acquires; // acquires默认传入1,相当于让nextc自增
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc); // 将标志位改成自增后的值
return true;
}
return false;
}
此处有两个值需要关心:
/**
*
The current owner of exclusive mode synchronization.
* 持有该锁的当前线程
*/
private transient Thread exclusiveOwnerThread;—————–两个值不在同一个类—————-
/**
* The synchronization state.
* 0: 初始状态-无任何线程得到了锁
* > 0: 被线程持有, 具体值表示被当前线程持有的执行次数
*
* 这个字段在解锁的时候也需要用到。
* 注意这个字段的修饰词:
volatile
*/
private volatile int state;
公平锁FairSync
公平锁的实现机理在于每次有线程来抢占锁的时候,都会检查一遍有没有等待队列,如果有, 当前线程会执行如下步骤:
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires))
{ setExclusiveOwnerThread(current);
return true; }
其中
hasQueuedPredecessors
是用于检查是否有等待队列的。
public final boolean hasQueuedPredecessors() {
Node t = tail; // Read fields in reverse initialization order
Node h = head;
Node s;
return h != t &&
((s = h.next) == null || s.thread != Thread.currentThread());
}
非公平锁NonfairSync
非公平锁在实现的时候多次强调随机抢占:
if (c == 0) {
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true; }
}
与公平锁的区别在于新晋获取锁的进程会有多次机会去抢占锁。如果被加入了等待队列后则跟公平锁没有区别。
ReentrantLock锁的释放
ReentrantLock锁的释放是逐级释放的,也就是说在 可重入性 场景中,必须要等到场景内所有的加锁的方法都释放了锁, 当前线程持有的锁才会被释放!
释放的方式很简单, state字段减一即可:
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;
}