JUC:java.util.concurrent的缩写,Java的并发编程类都在这个包下面。
AQS:AbstractQueuedSynchronizer抽象类的缩写,Lock、Condition的具体实现都在这个类,很重要的一个类。
主要类结构:
AbstractOwnableSynchronizer的主要结构:
public abstract class AbstractOwnableSynchronizer
implements java.io.Serializable {
// 当前持有锁的线程
private transient Thread exclusiveOwnerThread;
}
AQS(AbstractQueuedSynchronizer) 的主要结构:
public abstract class AbstractQueuedSynchronizer
extends AbstractOwnableSynchronizer
implements java.io.Serializable {
/**
* 队列头
*/
private transient volatile Node head;
/**
* 队列尾
*/
private transient volatile Node tail;
/**
* 锁状态:加锁成功则为1,重入+1,解锁则为0
*/
private volatile int state;
}
Node节点类的主要结构:
里面封装了上一个节点、下一个节点和线程,某种意义上Node就等于一个线程。
public abstract class AbstractQueuedSynchronizer
extends AbstractOwnableSynchronizer
implements java.io.Serializable {
static final class Node {
volatile int waitStatus;
volatile Node prev;
volatile Node next;
volatile Thread thread;
Node nextWaiter;
}
}
ReentrantLock类的主要结构:
public class ReentrantLock implements Lock, java.io.Serializable {
private final Sync sync;
abstract static class Sync extends AbstractQueuedSynchronizer {
public void lock() {
sync.lock();
}
public boolean tryLock() {
return sync.nonfairTryAcquire(1);
}
public Condition newCondition() {
return sync.newCondition();
}
public void unlock() {
sync.release(1);
}
}
ReentrantLock.Sync的继承关系:
AQS中的队列关系图
查看源码了解加锁流程
现在我们来查看源码,了解加锁的过程。
构造方法
日常开发使用锁的代码示例:
ReentrantLock类提供了无参和有参两种构造方式,不传默认等于false,非公平锁。为true表示公平锁。
对应源码:
public ReentrantLock() {
sync = new NonfairSync();
}
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
我们先以公平锁加锁为入口来看看加锁的代码实现。
lock()
final void lock() {
acquire(1);
}
“1”标识加锁成功之后改变的值,对应AQS的state,int类型默认为0。
acquire()
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
acquire()方法首先会调用tryAcquire方法,注意tryAcquire的结果做了取反。
tryAcquire(arg)尝试加锁,如果加锁失败则会调用acquireQueued()方法加入队列去排队,如果加锁成功则不会调用,此流程结束。
tryAcquire()尝试加锁
/*
*如果c不等于0,而且当前线程不等于拥有锁的线程则不会进else if 直接返回false,加锁失败
*如果c不等于0,但是当前线程等于拥有锁的线程则表示这是一次重入,那么直接把状态+1表示重入次数+1
*那么这里也侧面说明了reentrantlock是可以重入的,因为如果是重入也返回true,也能lock成功
*/
protected final boolean tryAcquire(int acquires) {
//获取当前线程
final Thread current = Thread.currentThread();
//获取lock对象的上锁状态,如果锁是自由状态则=0,如果被上锁则为1,大于1表示重入
int c = getState();
if (c == 0) {//没人占用锁--->我要去上锁
//hasQueuedPredecessors()判断自己是否需要排队,如果不需要排队则进行cas尝试加锁,如果加锁成功则把当前线程设置为拥有锁的线程,继而返回true
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;
}
“tryAcquire(arg)”尝试拿锁,第一次进来拿锁成功,修改AQS的属性,CAS修改state=1,exclusiveOwnerThread=currentThread,然后返回true。
if (!tryAcquire(arg)…… 这里取反后if条件为false,此流程结束。
hasQueuedPredecessors()判断自己是否需要排队
public final boolean hasQueuedPredecessors() {
Node t = tail;
Node h = head;
Node s;
/**
* 不需要排队有三种情况
* 一:队列没有初始化,不需要排队。
* 直接去加锁,但是可能会失败;为什么会失败呢?
* 假设两个线程同时来lock,都看到队列没有初始化,都认为不需要排队,都去进行CAS修改计数器;有一个必然失败
* 比如t1先拿到锁,那么另外一个t2则会CAS失败,这个时候t2就会去初始化队列acquireQueued(),并排队
*
* 二:队列被初始化了,但是tc过来加锁,发觉队列当中第一个排队的就是自己;比如重入;
*
* 三:持有的锁正好被释放,自己是队列中第一个排队的,正在排队时会先去尝试获取一下锁,因为有可能这个时候持有锁锁的那个线程可能释放了锁;
* 如果释放了就直接获取锁执行。但是如果没有释放他就会去排队,
* 所以这里的不需要排队,不是真的不需要排队
*
* h != t 判断首不等于尾这里要分三种情况
* 1、队列没有初始化,也就是第一个线程tf来加锁的时候那么这个时候队列没有初始化,
* h和t都是null,那么这个时候判断不等于则不成立(false)那么由于是&&运算后面的就不会走了,
* 直接返回false表示不需要排队,而前面又是取反(if (!hasQueuedPredecessors()),所以会直接去cas加锁。
*
* 2、队列被初始化了,后面会分析队列初始化的流程,如果队列被初始化那么h!=t则成立;(不绝对,还有第3中情况)
* 大于1个数据则成立;继续判断把h.next赋值给s;s有是对头的下一个Node,
* 这个时候s则表示他是队列当中参与排队的线程而且是排在最前面的;
* 为什么是s最前面不是h嘛?诚然h是队列里面的第一个,但是不是排队的第一个;下文有详细解释
* 因为h也就是对头对应的Node对象或者线程他是持有锁的,但是不参与排队;
* 这个很好理解,比如你去买车票,你如果是第一个这个时候售票员已经在给你服务了,你不算排队,你后面的才算排队;
* 队列里面的h是不参与排队的这点一定要明白;参考下面关于队列初始化的解释;
* 因为h要么是虚拟出来的节点,要么是持有锁的节点;什么时候是虚拟的呢?什么时候是持有锁的节点呢?下文分析
* 然后判断s是否等于空,其实就是判断队列里面是否只有一个数据;
* 假设队列大于1个,那么肯定不成立(s==null---->false),因为大于一个Node的时候h.next肯定不为空;
* 由于是||运算如果返回false,还要判断s.thread != Thread.currentThread();这里又分为两种情况
* 2.1 s.thread != Thread.currentThread() 返回true,就是当前线程不等于在排队的第一个线程s;
* 那么这个时候整体结果就是h!=t:true; (s==null false || s.thread != Thread.currentThread() true 最后true)
* 结果: true && true 方法最终放回true,所以需要去排队
* 其实这样符合情理,试想一下买火车票,队列不为空,有人在排队;
* 而且第一个排队的人和现在来参与竞争的人不是同一个,那么你就乖乖去排队
* 2.2 s.thread != Thread.currentThread() 返回false 表示当前来参与竞争锁的线程和第一个排队的线程是同一个线程
* 这个时候整体结果就是h!=t---->true; (s==null false || s.thread != Thread.currentThread() false-----> 最后false)
* 结果:true && false 方法最终放回false,所以不需要去排队
* 不需要排队则调用 compareAndSetState(0, acquires) 去改变计数器尝试上锁;
* 这里又分为两种情况(日了狗了这一行代码;有同学课后反应说子路老师老师老是说这个AQS难,
* 你现在仔细看看这一行代码的意义,真的不简单的)
* 2.2.1 第一种情况加锁成功?有人会问为什么会成功啊,如这个时候h也就是持有锁的那个线程执行完了
* 释放锁了,那么肯定成功啊;成功则执行 setExclusiveOwnerThread(current); 然后返回true 自己看代码
* 2.2.2 第二种情况加锁失败?有人会问为什么会失败啊。假如这个时候h也就是持有锁的那个线程没执行完
* 没释放锁,那么肯定失败啊;失败则直接返回false,不会进else if(else if是相对于 if (c == 0)的)
* 那么如果失败怎么办呢?后面分析;
*
*----------第二种情况总结,如果队列被初始化了,而且至少有一个人在排队那么自己也去排队;但是有个插曲;
* ----------他会去看看那个第一个排队的人是不是自己,如果是自己那么他就去尝试加锁;尝试看看锁有没有释放
*----------也合情合理,好比你去买票,如果有人排队,那么你乖乖排队,但是你会去看第一个排队的人是不是你女朋友;
*----------如果是你女朋友就相当于是你自己(这里实在想不出现实世界关于重入的例子,只能用男女朋友来替代);
* --------- 你就叫你女朋友看看售票员有没有搞完,有没有轮到你女朋友,因为你女朋友是第一个排队的
* 疑问:比如如果在在排队,那么他是park状态,如果是park状态,自己怎么还可能重入啊。
* 希望有同学可以想出来为什么和我讨论一下,作为一个菜逼,希望有人教教我
*
*
* 3、队列被初始化了,但是里面只有一个数据;什么情况下才会出现这种情况呢?ts加锁的时候里面就只有一个数据?
* 其实不是,因为队列初始化的时候会虚拟一个h作为头结点,tc=ts作为第一个排队的节点;tf为持有锁的节点
* 为什么这么做呢?因为AQS认为h永远是不排队的,假设你不虚拟节点出来那么ts就是h,
* 而ts其实需要排队的,因为这个时候tf可能没有执行完,还持有着锁,ts得不到锁,故而他需要排队;
* 那么为什么要虚拟为什么ts不直接排在tf之后呢,上面已经时说明白了,tf来上锁的时候队列都没有,他不进队列,
* 故而ts无法排在tf之后,只能虚拟一个thread=null的节点出来(Node对象当中的thread为null);
* 那么问题来了;究竟什么时候会出现队列当中只有一个数据呢?假设原队列里面有5个人在排队,当前面4个都执行完了
* 轮到第五个线程得到锁的时候;他会把自己设置成为头部,而尾部又没有,故而队列当中只有一个h就是第五个
* 至于为什么需要把自己设置成头部;其实已经解释了,因为这个时候五个线程已经不排队了,他拿到锁了;
* 所以他不参与排队,故而需要设置成为h;即头部;所以这个时间内,队列当中只有一个节点
* 关于加锁成功后把自己设置成为头部的源码,后面会解析到;继续第三种情况的代码分析
* 记得这个时候队列已经初始化了,但是只有一个数据,并且这个数据所代表的线程是持有锁
* h != t false 由于后面是&&运算,故而返回false可以不参与运算,整个方法返回false;不需要排队
*
*
*-------------第三种情况总结:如果队列当中只有一个节点,而这种情况我们分析了,
*-------------这个节点就是当前持有锁的那个节点,故而我不需要排队,进行cas;尝试加锁
*-------------这是AQS的设计原理,他会判断你入队之前,队列里面有没有人排队;
*-------------有没有人排队分两种情况;队列没有初始化,不需要排队
*--------------队列初始化了,按时只有一个节点,也是没人排队,自己先也不排队
*--------------只要认定自己不需要排队,则先尝试加锁;加锁失败之后再排队;
*--------------再一次解释了不需要排队这个词的歧义性
*-------------如果加锁失败了,在去park,下文有详细解释这样设计源码和原因
*-------------如果持有锁的线程释放了锁,那么我能成功上锁
*
**/
return h != t &&
((s = h.next) == null || s.thread != Thread.currentThread());
}
参考此图理解:
非公平锁的拿锁流程:
获取锁的差异,如下图:
)
跟公平锁比,多了红色框框的代码,即先抢一次,抢不到再执行acquire(1)走常规流程。
公平锁的上锁是必须判断自己是不是需要排队;而非公平锁是直接进行CAS修改计数器看能不能加锁成功;如果加锁不成功则乖乖排队(调用acquire);所以不管公平还是不公平;只要进到了AQS队列当中那么他就会排队;一朝排队,永远排队。
流程大概过了一遍,从流程中看出来的设计思路:
1、如果线程是交替执行的,那么AQS队列不会初始化,减少不必要的开销;(示例一)
2、线程之间有资源竞争,但时间相隔很短,队列会初始化,拿不到锁的线程会入队,但不会阻塞,会自旋尝试拿锁,减少park、unpark;(示例二第一种情况)
3、线程之间有资源竞争,但时间相隔很长,队列会初始化,拿不到锁的线程会入队,且阻塞等待唤醒;(示例二第二种情况)
Lock和AQS实现代码还是有点复杂的,因为Doug Lea会考虑多种情况且兼顾性能。比如队列的初始化,排队前再次尝试拿锁等等。文章还会继续补充,我也在边写这篇文章边消化。
参考文章:
https://blog.csdn.net/java_lyvee/article/details/98966684