在工作中,"多线程"一直出现在我们耳边,大多数人对于多线程的印象就是能够提高代码的性能,满足多个请求,就比方说是一块蛋糕用一个刀切和用多个刀同时一起切,区别还是很大的,但是很多小伙伴又对于多线程的使用很困难,线程之间的争抢会增加编码的复杂度,所以说多线程真是可爱又可恨。
在我刚接触多线程时,我一直有很多疑惑,比如 线程之间是怎么保证顺序的,多个线程去访问同一个代码块时,如果在保证请求顺序的前提下,那么并行不还是最终变成串行,实际的性能提高并不明显,比方说 用多个线程去累计1到100之和,多个线程的争抢,导致最终的结果必然会出现问题,这些疑问,希望小伙伴们和我一起慢慢的解开…、
提到多线程,那么不得不说下AQS(AbstractQueuedSynchronizer),这是JUC包中的核心框架,大多数的锁都是基于AQS来实现的,那么AQS在锁中到底是扮演一个什么角色呢? 我们可以用一个场景来解释,最近国内疫情又开始泛滥了,社区都在组织核酸监测,来做核酸监测的人数众多,需要一个监察员来组织群众排队,维持现场持续,那么这个监察员的角色就可以相当于AQS了,在JUC中,AQS也是负责维护各个请求,保证请求可以得到有序的执行,不会发生混乱。
JUC下面有很多的锁,比如ReentrantLock ,CountDonwLatch 都使用到了AQS,下面我们以ReentrantLock为例,来看下AQS是怎么和锁进行协同的(单独讲AQS有点枯燥,就结合ReetrantLock来说了)
ReetrantLock
上图为ReetrantLock的结构图,其内部组合了Sync对象,Sync对象继承了AQS抽象类
我们现在看看,但我们创建一个ReetrantLock,并加上lock锁时,代码内部会发生一些什么状况
ReentrantLock reentrantLock=new ReentrantLock();
reentrantLock.lock();
我们来分析一下reentrantLock的lock方法
public void lock() {
sync.lock();
}
首先它内部会调用内部类Sync的lock方法,继续往下看
我们看到Sync有两个实现类分别为公平的和不公平的,这里就涉及到了公平锁和非公平锁,后面会详细讲到二者的区别以及在ReetrantLock中的体现。
这里我们要注意到在创建ReentrantLock对象是,是可以传入参数来标识当前新建锁是否公平,以及在其内部创建Sync时也会进行处理,来看看Sync的初始化过程就一目了然了,go
好了下面我们就来看看Lock方法的具体实现吧
lock方法中有个if else分支,if中的compareAndSetState是个CAS方法,关于CAS后面我们也会讲到,这里的意思就是查看当前线程有没有锁,如果没有,直接把state状态由0改为1
如果当前线程已经获取到锁了就会进入到acquire方法中,这里的acquire就是重头戏了,其实这里还涉及到锁重入的知识,不要着急,后续我都会有详细的讲解。
我们来看看acquire方法:
这里就不放图片了,直接上源码了
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
这里的tryAcquire方法注意,它是AQS中的钩子方法,不清楚什么是钩子方法的小伙伴可以自己去看看模板设计模式,了解一下,在reentrantLock中有相应的tryAcquire具体实现,主要是用来判断当前线程是否能够成功获取到锁,我们来看看源码的逻辑
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;
}
注:这里有一点不明,在cas中已经将0变为1,后续在acquire中的tryAcquire方法中还进行了一次state==0的判断有点不解。
首先会获取当前的线程有没有持有锁,若没有还有进一步判断当前线程需不需要排队,也就是上述中的hasQueuedPredecessors(),关于这个方法大家可以自己去看看其中的判断逻辑。
在这个方法中会引出一个Node对象,这个对象很关键,它是AQS的线程队列中的管理对象,我们来看看这个Node对象中存放着什么信息
waitStatus:当前线程在队列中的等待状态 有一下几种
- CANCELLED:值为1,表示线程的获锁请求已经“取消”
- SIGNAL:值为-1,表示该线程一切都准备好了,就等待锁空闲出来给我
- CONDITION:值为-2,表示线程等待某一个条件(Condition)被满足
- PROPAGATE:值为-3,当线程处在“SHARED”模式时,该字段才会被使用上(在后续讲共享锁的时候再细聊)
prev:当前节点的前驱节点
next:当前节点的后继节点
thread:表示当前节点中的等待线程
nextWaiter: 表示一个Node节点,该节点为等待Condition条件的节点
当tryAcquire获取线程失败时,就会进入到acquireQueued(addWaiter(Node.EXCLUSIVE), arg))方法,这个方法的意思就是把当前线程加入到AQS中的线程等待队列中,我们看看这个方法
private Node addWaiter(Node mode) {
Node node = new Node(Thread.currentThread(), mode);
// Try the fast path of enq; backup to full enq on failure
Node pred = tail;
if (pred != null) {
node.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
enq(node);
return node;
}
首先是addWaiter(Node mode) 方法,这里用的时尾插法,在队尾将当前的新建节点加入到等待队列,并通过compareAndSetTail设置新加入节点为尾节点,enq()方法时用来保证当等待队列为空时,用来进行队列的初始化,并将当前节点加入。
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
再看看acquireQueued方法,上面我们通过addWaiter将当前线程加入到了等待队列,而这个acquireQueued方法是用来将加入等待队列中的线程不断的获取锁的操作,首先我们先获取当前节点的前驱节点,如果当前的节点的前驱节点为头节点,那么我们将这个头节点 删除,并且将当前节点设置为头节点,关于等待队列的头节点,希望大家记住,头节点的Node对象是没有存任何信息的,相当于是一个空对象,这里在setHead()方法中有具体的体现,大家可以看一下。
shouldParkAfterFailedAcquire这里的这个方法很重要,大家想象一下,如果当前线程一直没有成功获取到锁,那么就会一直在for循环进行获取锁的操作,从而造成cpu飙升,而这个shouldParkAfterFailedAcquire方法就是为了避免这一操作设计的,它会将获取锁失败的线程进行阻塞操作,具体的方法我们来一起看一下
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;
if (ws == Node.SIGNAL)
/*
* This node has already set status asking a release
* to signal it, so it can safely park.
*/
return true;
if (ws > 0) {
/*
* Predecessor was cancelled. Skip over predecessors and
* indicate retry.
*/
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
/*
* waitStatus must be 0 or PROPAGATE. Indicate that we
* need a signal, but don't park yet. Caller will need to
* retry to make sure it cannot acquire before parking.
*/
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
首先我们判断当前的节点的前驱节点的状态,如果为SIGNAL,意思是前驱节点就绪完毕正在等待线程,那么节点应该进行阻塞,如果前驱节点的状态大于0,以为着已经被取消,那么应该将前驱节点为取消状态的全部移除,如果不是大于零,我们应该将将前驱节点设置为SIGNAL状态,这样第二次循环就会将当前节点进行阻塞。
前面我们详细的说了lock加锁操作,现在我们再来看看解锁操作,相较于加锁操作来说,解锁操作就稍微来说简单一点了。
public void unlock() {
sync.release(1);
}
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
解锁其实也是调用了Sync中的release方法,我们来看看其中的tryRelease方法,它和tryAcquire方法一样,用来判断当前线程是否可以进行锁的释放
protected final boolean tryRelease(int releases) {
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;
}
这个方法应该很清晰吧,如果是重入锁,就将当前的重入减1,如果不是重入锁,就将当前所释放,本质就是设置当前持有锁的线程为空,当判断锁释放成功之后,我们就要进行下一步操作,去唤醒在等待队列里的线程来持有锁,具体方法为unparkSuccessor()。
private void unparkSuccessor(Node node) {
/*
* If status is negative (i.e., possibly needing signal) try
* to clear in anticipation of signalling. It is OK if this
* fails or if status is changed by waiting thread.
*/
int ws = node.waitStatus;
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
/*
* Thread to unpark is held in successor, which is normally
* just the next node. But if cancelled or apparently null,
* traverse backwards from tail to find the actual
* non-cancelled successor.
*/
Node s = node.next;
if (s == null || s.waitStatus > 0) {
s = null;
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
if (s != null)
LockSupport.unpark(s.thread);
}
这里记住在Node节点初始化时,waitStatus的值默认是为0。获取头节点的后继节点,因为头节点是不存放数据信息的,如果后继节点为空或者已经为取消状态的,那么就从尾节点往前进行遍历查找,找到一个不是canceled状态的线程并进行唤醒操作;
以上就是解锁的相关操作了,相对来说比较友好一点。
以上就就是AQS进行加锁和解锁的详细的操作已经对源码的解读,可能看完还是有点懵逼,这个需要对应源码慢慢研读才会有体会,当然在解读过程中,有些相关知识我并没有继续进行深入的介绍,比如ReedtrantLock公平锁和非公平锁的实现,其实大家比较NonfairSync和FairSync的lock方法就应该可以看出区别,这个后续我们专门详细介绍,关于CAS的操作原理也很重要,JUC中很多工具类都用到了这一方法,如 atomic包,hashmap(1.8版本),这个CAS我也会有详细介绍,关于钩子方法和模板设计模式这点就需要大家去学习了(有时间我也会介绍,优先级不是最高),还有关于数据接口的知识这里我是默认大家已经熟悉了,希望大家可以看看这方面的知识,这里在AQS的等待队列添加Node节点时,需要大家仔细研读,节点时如何添加的,后面有空我会补上这一部分。
下期我会和大家介绍 reentrantLock和synchorized的区别已经相关的用法,这个就比较偏实战,没有AQS那么枯燥了哈哈 。