说明
在了解AQS后,那应该怎么了解AQS的最佳实践那,我想再也没有Java官方的实践更加优秀了,这次我打算重新拿出系统源代码,并将其总结成一系列文章,以供将来查看.
本次准备分六篇文章用来分析基于AQS实现的类
- 第一篇(Java-ReentrantLock-非公平锁源码分析)
- 第二篇(Java-ReentrantLock-公平锁源码分析)
- 第三篇(Java-并发工具-CountDownLatch源码分析)
- 第四篇(Java-并发工具-Semaphore源码分析)
- 第五篇(Java-ReentrantReadWriteLock-读锁分析)
- 第六篇(Java-ReentrantReadWriteLock-写锁分析)
介绍
本篇文章为系列文章的第二篇,本篇文章介绍ReentrantLock(可重入锁)公平锁代码实现,ReentrantLock是一个可重入的互斥锁Lock,它具有与使用synchronized方法和语句所访问的隐式监视器锁相同的一些基本行为和语义,但功能更强大。
什么是可重入锁
可重入锁指的是线程可以重复获取同一把锁.
概述
首先,我们从总体过程入手,了解ReentrantLock公平锁的执行逻辑,然后逐步深入分析源代码。
-
我们知道AQS内部是对阻塞线程队列的维护.因此,框架不支持基于优先级的同步,同时也表明加入队列的线程是按照顺序获取执行时间的,所以只有我们保证获取锁线程顺序的加入阻塞队列,我们AQS实现的锁就是公平锁,这就像我们生活中排队结账,如果保证结账的公平性,那就是有序的排队,不允许插队。
-
首先ReentrantLock公平锁当前线程获取状态后,不会立即尝试设置state,而是先检查是否有线程等待获取的时间长于当前线程,如果不存在才会尝试设置state(获取锁),如果存在则当前线程会执行排队.不会参与竞争锁.
源码分析
基于上面提到的过程,让我们来看看源代码实现逻辑.首先,让我们看看如果创建公平锁。
//默认构造器 构建非公平锁
public ReentrantLock() {
sync = new NonfairSync();
}
//传值 构建 true 构建 公平锁,false 构建非公平锁
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
由上面代码可知,ReentrantLock提供了两个构造方法,默认无参构造器构建非公平锁,我们通过有参构造器传入true创建公平锁
// 公平锁为一个内部类 ,他继承 Sync 内部类
static final class FairSync extends Sync {
private static final long serialVersionUID = -3000897897090466540L;
//不做任何操作 直接使用AQS acquire方法
final void lock() {
acquire(1);
}
//重写 tryAcquire
protected final boolean tryAcquire(int acquires) {
//获取当前线程
final Thread current = Thread.currentThread();
//获取当前状态
int c = getState();
//如果状态为0 则表示没有线程获取锁
if (c == 0) {
//与非公平锁不同的是,此处多了hasQueuedPredecessors()判断,该方法是实现公平锁的关键。
//如果hasQueuedPredecessors返回true,表示有其他线程先于当前线程等待获取锁,
//此时为了实现公平,保证等待时间最长的线程先获取到锁,不能执行CAS。
//CAS可能会破坏公平性。反之,如果hasQueuedPredecessors返回false,则可以执行CAS更新同步状态尝试获取锁。
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;
}
//如果以上都没有设置成功
//则返回失败 执行AQS排队机制
return false;
}
}
由上面代码可知,FairSync继承SYNC,公平锁和非公平锁最大的区别就是在设置同步状态的时候,增加了一个校验hasQueuedPredecessors,该方法保证不会存在插队现象。
//查询是否有线程等待获取的时间长于当前线程。
public final boolean hasQueuedPredecessors() {
Node t = tail;
Node h = head;
Node s;
return h != t &&
((s = h.next) == null || s.thread != Thread.currentThread());
}
通过以上分析可以知道hasQueuedPredecessors返回true代表有别的线程在CLH队列中排了当前线程之前,返回false代表当前线程处于CLH队列的第一个线程。
- 分析h != t返回false的情况。此时hasQueuedPredecessors返回false。
- 当h和t都为null,返回false。此时说明队列为空,还从来没有Node入过队。
- 当h和t都指向同一个Node,也返回false。此时说明队列中只有一个dummy node,(参考AQS enq代码)那说明没有线程在队列中或者还没有线程排队成功。
- 分析h != t返回true 同时(s = h.next) == null 为true 此时hasQueuedPredecessors返回为true。
- 既然h != t返回true,说明h和t不相等,先考虑特殊情况(上面讲到的出现"head不为null,tail为null"的情况,此时head是空node,next成员肯定为null),那么说明有一个线程正在执行enq,且它正好执行到if (compareAndSetHead(new Node()))到tail = head;的间隙。但这个线程肯定不是当前线程,所以不用判断后面短路的s.thread != Thread.currentThread()了,因为当前线程连enq都没开始执行,但另一个线程都开始执行enq了,那不就是说明当前线程排在别人后面了,别的线程马上就要入队了。
- 既然h != t返回true,说明h和t不相等,再考虑二者都不为null。那此时队列中已经至少有一个等待中的线程了,那说明当前线程肯定排在别人后面了。
思考 hasQueuedPredecessors 结合 enq 源码思考
- Node t = tail; Node h = head; tail与head的赋值顺序的意义
tail 先赋值head后赋值保证了后续节点 h.next 不会为空
- tail 为null 则 head 不为null
发生在t == null场景内, compareAndSetHead(new Node()) 刚为head赋值完成,tail 还没有被head赋值
- tail 为null 则 head 为 null
发生在t == null场景内,compareAndSetHead(new Node()) 还没有为head赋值完成
- tail 不为null 则 head 不为null
发生在t == null场景内,tail 被head赋值完
- tail 不为null 则 head 为null (这种情况100%不会出现)
该场景不会发生,tail 如果不为null则,则head必然不为nul
private Node enq(final Node node) {
for (;;) {
Node t = tail;
if (t == null) { // Must initialize
if (compareAndSetHead(new Node()))
tail = head;
} else {
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
总结
以上是ReentrantLock-公平锁的获取和释放源码分析,公平锁的实现也很简单,这些都因为它基于AQS实现,AQS已经帮我们实现了大多数功能,了解ReentrantLock源码实现能够让我们更加深入的了解AQS设计思想。