一、前言
Java线程同步两种方式,synchronized关键字和Lock锁机制,其中,AQS队列就是Lock锁实现公平加锁的底层支持。
二、AQS源码对于lock.lock()的实现
2.1 AQS类 + 内部Node类
2.1.1 AQS类结构示意图
首先我们要看看AQS的结构的类图
从AQS类的类结构示意图可以知道,
- AbstractQueuedSynchronizer的父类是AbstractOwnableSynchronizer;
- AbstractQueuedSynchronizer的子类是Sync,然后又通过继承Sync得到了FairSync公平锁和UnfaiSync非公平锁,所以,AQS是Lock锁机制实现公平锁的底层支持。
2.1.2 内部Node类
通过上面的结构图,我们可以知道该类维护了一个Node内部类,于是我们查看Node的源码如下,主要是用来实现上面的我们提到的队列。
static final class Node {
//指示节点正在共享模式下等待的标记
static final Node SHARED = new Node();
//指示节点正在以独占模式等待的标记
static final Node EXCLUSIVE = null;
//waitStatus值,指示线程已取消 cancel
// 这个节点已经被取消 canceled 这样可读性强
static final int CANCELLED = 1;
//waitStatus值,指示后续线程需要释放 signal
// 这个节点的后继被阻塞,因此当前节点在取消必须释放它的后继
static final int SIGNAL = -1;
//waitStatus值,指示线程正在等待条件 condition
// 这个节点在条件队列里面
static final int CONDITION = -2;
//waitStatus值,表示下一个被默认的应该无条件传播的等待状态值 propagate
static final int PROPAGATE = -3;
/*
* SIGNAL:这个节点的后继被(或即将)阻塞(通过park),因此当前节点在释放或取消时必须释放它的后继。为了避免竞争,acquire方法必须首先表明它们需要一个信号,然后重试原子获取,当失败时,阻塞。
*
* CANCELLED:由于超时或中断,该节点被取消。节点不会离开这个状态。特别是,取消节点的线程不会再次阻塞。
*
* CONDITION:此节点当前处于条件队列中。在传输之前,它不会被用作同步队列节点,此时状态将被设置为0。
*
* PROPAGATE:释放的共享应该传播到其他节点。在doReleaseShared中设置这个(仅针对头节点),以确保传播继续,即使其他操作已经干预。
*
* 0:以上都不是
*/
volatile int waitStatus; // 默认值0,什么都不是
//上一个节点
volatile Node prev;
//下一个节点
volatile Node next;
//节点中的值
volatile Thread thread;
//下一个等待节点
Node nextWaiter;
//判断是否是共享的节点
final boolean isShared() {
return nextWaiter == SHARED;
}
//返回当前的节点前置节点
final Node predecessor() throws NullPointerException {
Node p = prev;
if (p == null)
throw new NullPointerException();
else
return p;
}
//用于建立初始标头或SHARED标记
Node() {
}
//addWaiter时候调用
Node(Thread thread, Node mode) {
this.nextWaiter = mode;
this.thread = thread;
}
//Condition时候调用
Node(Thread thread, int waitStatus) {
this.waitStatus = waitStatus;
this.thread = thread;
}
}
复制代码
关于waitStatus:
- SIGNAL=-1:这个节点的后继被(或即将)阻塞(通过park),因此当前节点在释放或取消时必须释放它的后继。为了避免竞争,acquire方法必须首先表明它们需要一个信号,然后重试原子获取,当失败时,阻塞。
- CANCELLED=1:由于超时或中断,该节点被取消。节点不会离开这个状态。特别是,取消节点的线程不会再次阻塞。
- CONDITION=-2:此节点当前处于条件队列中。在传输之前,它不会被用作同步队列节点,此时状态将被设置为0。
- PROPAGATE=-3:释放的共享应该传播到其他节点。在doReleaseShared中设置这个(仅针对头节点),以确保传播继续,即使其他操作已经干预。
- waitStatus=0:以上都不是
根据上面代码,知道AQS队列是一个双链表实现的队列,每个节点包含prev指针和next指针,具体如下图:
问题:AQS内部类Node
回答:AQS本质是一个非循环的双向链表(也可以称为队列),所以它是由一个个节点构成的,就是Node,后面的lock() unlock() await() signal()/signalAll()都是以Node为基本元素操作的。
问题:AQS类中的Node内部类中需要保存什么信息呢?
回答:一个六个,其中,prev、next 两个Node类型,表示做指针,thread 存放节点的值,因为AQS队列的节点就是存放线程的,所以这个值类型就是Thread,最后,nextWaiter也是Node类型,表示下一个等待节点, waitStatus表示当前节点等待状态,SHARED|EXCLUSIVE 表示是独占还是共享。
volatile int waitStatus; //当前节点等待状态
volatile Node prev; //上一个节点
volatile Node next; //下一个节点
volatile Thread thread; //节点中的值
Node nextWaiter; //下一个等待节点
//指示节点共享还是独占,默认初始是共享
static final Node SHARED = new Node();
static final Node EXCLUSIVE = null;
复制代码
记住一个Node节点的六个属性(共享/独占算一个),下面看源码就轻松些
- 处在同步队列中使用到的属性(本文用:加锁、解锁)包括:next prev thread waitStatus,所以同步队列是双向非循环链表,涉及的类变量AbstractQueuedSynchronizer类中的head和tail,分别指向同步队列中的头结点和尾节点。
- 处在等待队列中使用到的属性(下一篇博文用:阻塞、唤醒)包括:nextWaiter thread waitStatus,所以等待队列是单向非循环链表,涉及的类变量ConditionObject类中的firstWaiter和lastWaiter,分别指向等待队列中的头结点和尾节点。
- AQS队列是工作队列、同步队列,是非循环双向队列:当使用到head tail的时候,就说AQS队列建立起来了,单个线程不使用到head tail,所以AQS队列没有建立起来;
- 等待队列,是非循环单向队列:当使用firstWaiter lastWaiter的时候,就说等待队列建立起来了。
- lock()和unlock()就是操作同步队列:lock()将线程封装到节点里面(此时,节点使用到的属性是thread nextWaiter waitStatus),放到同步队列,即AQS队列中,unlock()将存放线程的节点从同步队列中拿出来,表示这个线程工作完成。
- await()和signal()就是操作等待队列:await()将线程封装到节点里面(此时,节点使用到的属性是thread prev next waitStatus),放到等待队列里面,signal()从等待队列中拿出元素。
2.2 公平锁加锁需要工作队列:FairSync中的lock()方法(重点)
2.2.1 lock方法只有一个线程的情况
2.2.1.1 整体路程图(第1次加锁 + 第2~n次加锁)
lock方法只有一个线程的情况(ps:此时还没有AQS队列,head==tail),如下图所示:
对于上图的解释,看这张图的正确姿势是:
- 上图中涉及的方法包括 acquire() 、 tryAcquire() 、 hasQueuedPredecessors()方法;
- 对于acquire() 方法,就是直接调用 tryAcquire() 方法,实参传入1。
- 对于tryAcquire()方法,由于只有一个线程A,第一次进入的时候要CAS加锁(if判断后半段)并将自己设置为独占线程(if代码段),所以两行代码,以后就是重入锁了,直接进入,不用CAS操作和设置独占锁了,直接通过setState()修改state值,每次加一即可,因为实参acquires为1。
- 对于hasQueuedPredecessors()方法,该方法就是判断AQS工作队列是否建立起来了,这里h==t,所以,没有建立起来,返回false。
- 总体流程,对于同一线程A,对于图中唯一一个菱形判断框,第一次进入的时候要CAS加锁(if判断后半段)并将自己设置为独占线程(if代码段);第2~n次进入就是重入锁,直接进入,不用CAS操作和设置独占锁了,直接通过setState()修改state值,每次加一即可,这就是整体流程,线程A的整体流程,分为两种球情况而已。
公平锁加锁流程(只有一个线程的时候):
- 直接调用tryAcquire,然后判断state的是不是等于0
- 对于A线程第1次加锁,等于0,证明是第一次加锁,第一次加锁hasQueuedPredecessors()一定通过(返回为false取反为true),通过CAS操作将state的值改成1,然后true,返回true表示加锁成功,就完成了加锁。
- 对于A线程第2~n次加锁,不等于0,表示不是第一次加锁,这个锁是重入锁,这个时候将原来的state值继续通过CAS操作加上1,再次返回true,表示加锁成功,就完成了加锁。
tip1:需要注意的是,这个时候AQS的队列没有创建出来。
tip2:setExclusiveOwnerThread(current); // 这里是设置当前节点为独占 记住上面六个属性
tip3:看源码的时候,知道自己在看什么,这里是看FairSync的lock()方法实现
tip4:源码一般命名优美,可以从命名上来看,帮助理清思路,例如 lock()是加锁、acquire()是去获得tryAcquire() 是尝试加锁、acquireQueued()是获得队列
2.2.1.2 重要方法:tryAcquire()源码解析
对于只有一个线程A使用lock.lock();加锁,最重要的方法就是tryAcquire,就是这个方法,将线程A区分对待,第一次加锁和非第一次加锁,源码拿出来讲一讲:
protected final boolean tryAcquire(int acquires) { // **1、tryAcquire是去获取,2、返回为true就是使用获取的方式加锁成功(可以第一次,也可以是重入锁)**
final Thread current = Thread.currentThread();
int c = getState(); // 当前状态
if (c == 0) { // 当前状态为0,就是默认状态
if (!hasQueuedPredecessors() && // **1、hasQueuedPredecessors这个方法重要,下面解释**
compareAndSetState(0, acquires)) { // **1、只要上面那个hasQueuedPredecessors()返回为false,取反为true,这个cas一定是可以通过的,只是自旋等一下罢了**
setExclusiveOwnerThread(current); // **1、设置当前线程为独占线程,因为当前线程已经加锁成功了,所以设置当前线程为互斥资源的独占线程**
//**2、为什么说当前线程加锁成功了,因为这里返回true啊**
return true;
}
}
else if (current == getExclusiveOwnerThread()) { // 这句表示当前线程为独占线程
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true; // **1、因为当前线程是独占线程,所以一定是加锁成功,这里返回true就好
// 2、既然已经是独占线程,就没有必要再次设置当前线程为独占线程了,直接返回true**
}
return false; // **1、如果既不是第一次,也不是重入锁,就不能通过获取的方式去加锁,要自己加锁,这里返回false,加锁失败**
}
复制代码
2.2.1.3 重要方法: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());
}
复制代码
问题:hasQueuedPredecessors()源码解析?
回答:
-
如果h == t成立,h和t均为null或是同一个具体的节点,无后继节点,方法返回false,表示要通过获取锁tryLock()加锁,这种情况是第一个节点,类变量tail == head == null。
key:如果类变量head==tail,表示没有节点或只有一个节点,所以一定是没有前驱节点的,方法直接返回false,不用多说,注意,后面的,head!=tail至少两个节点 -
如果h!=t成立(至少两个节点),而且 head.next == 为null,方法返回true。什么情况下h!=t的同时h.next==null??有其他线程第一次正在入队时,可能会出现。见AQS的enq方法,compareAndSetHead(node)完成,还没执行tail=head语句时,此时tail=null,head=newNode,head.next=null。
key:此时,头结点设置为新建节点,所以head=newNode
但是,还未将头结点设置为尾节点,所以tail=null,为默认值
同时,这是第一次执行enq()方法,没有设置 node.prev = t; 和 t.next = node;,所以head.next=null。 -
如果h!=t成立(当前类变量head 和 tail不是同一个,至少两个节点),head.next != null,(true && (false || ))则判断head.next是否是当前线程,如果是当前线程(true && (false || false)),方法返回false,key:表示要通过获取锁的方式加锁;如果不是当前线程(true && (false || true))返回true,key:表示正在运行的线程不是当前的这个current,需要等待。
(head节点是获取到锁的节点,但是任意时刻head节点可能占用着锁,也可能释放了锁(unlock()),未被阻塞的head.next节点对应的线程在任意时刻都是有必要去尝试获取锁)
实际上,hasQueuedPredecessors返回为true不通过,只需要等一段时间罢了(上面关于hasQueuedPredecessors方法的意义:如果该方法返回true,则表示有线程比当前线程更早地请求获取锁,因此需要等待前驱线程获取并释放锁之后才能继续获取锁)
问题:hasQueuedPredecessors()方法是用来干什么的?
回答:
- hasQueuedPredecessors()方法的意义:该方法表示对加入了同步队列中当前节点是否有前驱节点的判断,如果该方法返回true,则表示有线程比当前线程更早地请求获取锁,因此需要等待前驱线程获取并释放锁之后才能继续获取锁,因为是公平锁,要按照队列来。
- hasQueuedPredecessors()方法的调用以及返回的意义:hasQueuedPredecessors()方法只在tryAcquire()方法里面被调用执行过,hasQueuedPredecessors()返回false表示要尝试通过获取锁的方式加锁,没有前驱节点请求获取锁,返回为true表示不需要通过获取锁的方式加锁,已经有前驱节点请求获取了。
问题:如何进入hasQueuedPredecessors()方法?
回答:lock() -> acquire() -> tryAcquire() -> hasQueuedPredecessors()
2.2.2 lock方法中有两个线程的情况
2.2.2.1 整体路程图(线程A加锁 + 线程B加锁)
lock方法实现的公平锁AQS队列中有两个线程的情况,如下图所示:
上图注意两点:
- 流程:两个线程的运行,左边是线程A先获得锁,然后线程B再去尝试获得锁,但是线程B一定要等到线程A释放锁之后,才能获得锁成功,在这之前只能阻塞,即源码中for循环;
- 仔细对比两个图,和上一次的改变是:线程B节点的上一个节点的waitStatus的值从0修改成-1。
对于上图的解释:
先是线程A加锁,然后线程B加锁,线程A加锁没必要说,和上面一个线程的情况一样,线程B加锁分为两种情况:线程A还没有解锁,线程B加锁失败;线程A已经解锁,线程B CAS操作加锁成功。
2.2.2.2 第一种情况:当线程B进来的时候,死循环中线程A没有解锁
第一,建立AQS队列
我们假设线程A直接获取到了锁(获取锁的过程和上面单线程一样,不再赘言),但是线程A还没有解锁,这个时候线程B来进行加锁,走来会执行tryAcquire()方法,这个时候线程A没有解锁,所以这个tryAcquire()方法会直接返回false(state!=0,也不是重入锁),然后会调用addWaiter(Node.EXCLUSIVE)方法(addWaiter()是新方法:上面一个线程的时候没有涉及到,这里要重点分析),这个时候会在这个方法中的enq(node)的方法中初始化AQS队列,也会利用尾插法将当前的节点插入到AQS队列中去。AQS队列如下图所示:
对于当前的AQS队列解释:
- 这时AQS队列新建起来了
- head节点中thread=null表示没有存放任何线程,waitStatus=0表示什么都不是
- tail节点中thread=线程B表示存放的是线程B,waitStatus=0表示什么都不是
第二,addWaiter()方法中调用enq()方法,新建AQS队列
完成AQS队列的方法是addWaiter()中调用的enq()方法,且看addWaiter()方法和enq()方法
private Node addWaiter(Node mode) { // **1、实际参数是Node.EXCLUSIVE,就是当前独占节点,表示下一个等待节点就是正在独占的那个线程的节点,因为它释放锁就要到插入了,所以这个方法称为addWaiter,意为添加下一个等待节点**
Node node = new Node(Thread.currentThread(), mode); // 新建一个节点,存放当前线程,当前线程为内容,实参为下一个等待节点nextWaiter
Node pred = tail; // 当前尾节点赋值,当前tail==null
if (pred != null) {
node.prev = pred; // 如果不为空,进来,新建节点的前一个是之前的尾节点,就是尾插法
if (compareAndSetTail(pred, node)) { // 设置新的尾节点,从之前的尾节点pred到现在的node
pred.next = node; // 之前尾节点的next设置为这个节点
// **由此可知,尾插法三步骤:设置新节点的prev为之前尾节点、重新设置tail类变量的指向、设置之前尾节点的next为新建节点(就是三个Node类型指针而已,很简单)**
return node; // 返回新建节点
}
}
enq(node); // 返回值没有接收者,但是队列新建好了
return node; // 返回这个新建的节点
}
private Node enq(final Node node) {
for ( ; ; ) { // **1、死循环,不创建好AQS队列不退出**
Node t = tail;
if (t == null) { // Must initialize **1、第一次进入,必须初始化,这里表示连尾节点都没有**
if (compareAndSetHead(new Node())) // **for+if(cas)就是线程同步**
tail = head; // **1、新建一个节点,设置为头结点,因为只有一个节点,所以尾节点也是这个节点**
} else {
node.prev = t; // **1、这是时候,第二次循环,因为head tail都是新节点,第二次循环中使用 Node t = tail;将t设置为这个新节点**
if (compareAndSetTail(t, node)) { // 方法名是compareAndSetTail,表示设置尾节点,自旋,知道设置成功 for+if(cas)就是线程同步,设置tail类变量,将tail从t变为node,所以传入参数node是尾节点
t.next = node; // 尾节点指向参数node,头结点还是指向t
// **由此可知,尾插法三步骤:设置参数节点的prev为之前尾节点t、重新设置tail类变量的指向从之前的t到参数节点node、设置之前尾节点t的next为参数节点node(就是三个Node类型指针而已,很简单),最后队列两个元素 t 和 node**
return t; // 返回头结点t
}
}
}
}
复制代码
tip:head和tail是类变量,类似指针,指向其他节点
compareAndSetTail(t, node) // 设置tail类变量,将tail从t变为node,所以传入参数node是尾节点
compareAndSetHead(t, node) // 设置tail类变量,将head从t变为node,所以传入参数node是头节点
compareAndSetState(0, acquires) // 设置state类变量,从0到1,cas保证安全
第三,acquireQueued()方法接收addWaiter()返回值,调用shouldParkAfterFailedAcquire()方法,修改AQS队列中节点的waitStatus值从0到1
方法addWaiter()返回当前的节点,然后调用acquireQueued(addWaiter(Node.EXCLUSIVE), arg))方法(tip:返回的刚刚在addWaiter()方法中新建的最尾巴的节点作为acquireQueued方法的参数,arg参数是1,传递过来的)。这个方法中是一个死循环,由于线程A没有释放锁(tryAcquire()方法会直接返回false(state!=0,也不是重入锁)),会执行shouldParkAfterFailedAcquire(p, node)(p表示线程B节点的上一个节点,p = node.predecessor();就是最尾巴节点上一个,node表示线程B的节点,就是addWaiter()方法中新建的最尾巴的节点)第一次进这个方法会将线程B节点的上一个节点的waitStatus的值改成-1(执行最后一个else compareAndSetWaitStatus(pred, ws, Node.SIGNAL);),然后返回false,这个时候的AQS队列如下图:
tip:仔细对比两个图,和上一次的改变是:线程B节点的上一个节点的waitStatus的值从0修改成-1。
-
第一次进入shouldParkAfterFailedAcquire()方法会将线程B节点的上一个节点的waitStatus的值改成-1,然后返回false。此时的AQS工作队列:head节点thread==null表示没有存放线程,waitStatus=-1表示后面节点的线程需要释放;tail节点thread=线程B表示存放的是线程B,waitStatus=0表示什么都不是。
-
第二次进入shouldParkAfterFailedAcquire()方法的时候,会返回true( if (ws == Node.SIGNAL) return true;),会执行后面的方法parkAndCheckInterrupt()( LockSupport.park(this);),这个时候线程B就会被park在这。(1、直到线程A解锁了,第二种情况可以当做第一个情况后面的执行来看)
2.2.2.3 第二种情况:死循环中线程A已经解锁了
上面的情况都是在线程A没有解锁的时候,如果在死循环中线程A已经解锁了。这个时候判断线程B节点的上一个节点是不是头结点,如果是的话,直接执行tryAcquire(),将当前线程B设置成独占线程,同时将state的值通过CAS操作设置成1,如果成功的话,直接返回true。表示加锁成功。这个时候会执行这个if判断中代码。执行setHead(node),这个时候AQS队列如下图:
if (p == head && tryAcquire(arg)) {
setHead(node); // node就是addWaiter的尾巴节点,
p.next = null; // help GC 看Java四种引用就知道 前面那个节点的next设置为null
failed = false; // 局部变量failed初始为true,要下去执行cancelAcquire,这里设置为false,不执行cancelAcquire了
return interrupted; // false
}
private void setHead(Node node) {
head = node; // 类变量head指向addWaiter的尾巴节点
node.thread = null; // 这个节点thread=null
node.prev = null; // 这个节点prev==null 因为要变成头结点,非循环双向链表,所以前驱指针为null
}
复制代码
这个时候原来的线程B节点出队列(因为B节点要去执行了),然后永远会维护一个头结点中thread为null的AQS队列。
2.2.3 lock方法中有三个线程的情况
lock方法中有三个线程情况,如下图:
三个线程和两个线程的情况是差不多的,即加锁成功的节点永远是头结点的下一个节点中的线程加锁成功,因为是公平锁。
2.3 非公平锁加锁不需要队列:NonfairSync类的lock()方法(了解即可,搞懂了公平锁的lock(),这个就好懂了,然后只要知道公平锁lock()和非公平锁lock()区别就好)
非公平锁加锁流程:
- 非公平锁会走来直接尝试加锁;
- 如果加锁成功,直接执行线程中的代码;
- 如果加锁不成功,直接走公平锁的逻辑。
2.4 其他加锁方法:ReentrantLock类的tryLock()方法(了解即可,和上面公平锁lock()大同小异)
tryLock()方法和lock()方法是差不多,tryLock方法,尝试加锁不成功后就直接返回false,具体的代码如下:
public boolean tryLock() {
return sync.nonfairTryAcquire(1);
}
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
复制代码
2.5 其他加锁方法:ReentrantLock类的tryLock(long timeout, TimeUnit unit)方法(了解即可,和上面公平锁lock()大同小异)
-
tryLock(long timeout, TimeUnit unit)方法,加了一个获取锁的时间,如果这个时间内没有获取到锁,直接返回false,表示加锁失败;如果在这个时间内调用tryAcquire(arg)获得到锁,表示加锁成功,tryAcquireNanos(int arg, long nanosTimeout)方法返回值直接为true,即tryLock(long timeout, TimeUnit unit)方法返回值为true。
-
如果tryAcquire(arg)返回为false,会执行doAcquireNanos(arg, nanosTimeout)方法,走来先将当前节点用尾插法的方式插入到AQS队列中去,如果AQS队列没有初始化,直接初始化,将当前的节点放入到尾结点中去。然后进入死循环,这个时候判断当前节点的上一个节点是不是头结点,再次尝试加锁,如果成功直接返回true,如果失败将当前的节点的线程直接park指定的时间,当时间到了直接唤醒。再次尝试获取锁,如果成功直接返回true,如果失败直接返回false,这个方法中是可以直接响应中断的。
详细:
public boolean tryLock(long timeout, TimeUnit unit)
throws InterruptedException {
return sync.tryAcquireNanos(1, unit.toNanos(timeout));
}
public final boolean tryAcquireNanos(int arg, long nanosTimeout)
throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException(); // 这里会立即响应中断
return tryAcquire(arg) ||
doAcquireNanos(arg, nanosTimeout);
}
private boolean doAcquireNanos(int arg, long nanosTimeout)
throws InterruptedException {
if (nanosTimeout <= 0L)
return false;
final long deadline = System.nanoTime() + nanosTimeout;
final Node node = addWaiter(Node.EXCLUSIVE);
boolean failed = true;
try {
for (;;) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return true;
}
nanosTimeout = deadline - System.nanoTime();
if (nanosTimeout <= 0L)
return false;
if (shouldParkAfterFailedAcquire(p, node) &&
nanosTimeout > spinForTimeoutThreshold)
LockSupport.parkNanos(this, nanosTimeout); // park指定的时间
if (Thread.interrupted())
throw new InterruptedException(); // 有中断立即响应
}
} finally {
if (failed)
cancelAcquire(node);
}
}
复制代码
2.6 其他加锁方法:ReentrantLock类的lockInterruptibly()方法(了解即可,和上面公平锁lock()大同小异)
lockInterruptibly和lock的区别:
- lockInterruptibly是会立即响应中断的:并且在park中的线程也会interruptibly唤醒的,因为这个时候返回true,直接抛出异常,响应对应的中断。
- lock是要等线程执行完才会响应中断:是因为park中线程被中断唤醒后,没有抛出异常,只是将中断的标志设置成了true,等到获取到锁,执行完,才会响应对象的中断。
lockInterruptibly是会立即响应中断的(源码解释)
public void lockInterruptibly() throws InterruptedException {
sync.acquireInterruptibly(1);
}
public final void acquireInterruptibly(int arg)
throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
if (!tryAcquire(arg))
doAcquireInterruptibly(arg);
}
private void doAcquireInterruptibly(int arg)
throws InterruptedException {
final Node node = addWaiter(Node.EXCLUSIVE);
boolean failed = true;
try {
for (;;) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return;
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
throw new InterruptedException(); // 有中断立即响应
}
} finally {
if (failed)
cancelAcquire(node);
}
}
复制代码
lock是要等线程执行完才会响应中断(源码解释)
final void lock() {
acquire(1);
}
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
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; // 没有立即响应中断,仅仅设置一个标志位interrupt=true
}
} finally {
if (failed)
cancelAcquire(node);
}
}
复制代码
三、AQS源码对于lock.unlock()的实现
讲完了加锁的过程,我们再来看看解锁的过程,即ReentrantLock类的unlock()方法。
3.1 解锁整体流程图
3.2 解锁涉及的函数:unlock()->release()->tryRelease() + 解锁三种情况
解锁涉及的函数:unlock()->release()->tryRelease()
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;
}
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;
}
复制代码
线程解锁的三种情况:
-
当前线程不在AQS队列中,执行tryRelease()方法。
如果当前线程不是重入锁(即满足 if(c==0)为true),直接将当前的线程独占标识去除掉,然后将state的值通过CAS的操作改成0;
如果当前线程加的是重入锁(即满足 if(c!=0)为false),解锁一次,state的值减1,如果state的值是等于0的时候,返回true。表示解锁成功。 -
AQS队列中只有一个头结点,这个时候tryRelease()返回的结果和上面的情况是一样的。这个时候返回的true,会进当前的if中去,然后判断头结点是不是为null和头结点中waitStatus的值是不是等于0。这个时候head不等于null,但是waitState是等于0,if判断不成立,不会执行unpark的方法。会直接返回true。表示解锁成功。
-
AQS队列中不止一个头结点,还有其他节点,这个时候tryRelease()返回的结果和上面的情况是一样的。这个时候返回的true,会进当前的if中去,然后判断头结点是不是为null和头结点中waitStatus的值是不是等于0。这个时候head不等于null,但是waitState是等于-1,if判断成立,会执行unpark的方法。unpark方法中会unpark头结点的下一个节点,然后如果当前的节点的状态是取消的状态,会从最后一个节点开始找,找到当前节点的下一个不是取消状态的节点进行unpark。这个时候也会直接返回true。表示解锁成功。
3.3 重要函数:unparkSuccessor()(在release()中被调用)
private void unparkSuccessor(Node node) { // 同步队列中的头结点head传递过来
int ws = node.waitStatus; // 为 -1
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0); // 设置状态 node的ws变为0
Node s = node.next; // 找到工作队列的head的后面一个节点
if (s == null || s.waitStatus > 0) { // head后面这个节点为空,或者waitStatus大于0
s = null; // 如果是因为waitStatus大于0而进入这个if,设置head后面的这个节点为null
for (Node t = tail; t != null && t != node; t = t.prev) // 从尾巴开始遍历,布局变量为t 没有遍历完或没找就继续找
if (t.waitStatus <= 0) // 如果t.waitStatus <= 0,将这个t记录到s,后面unpark用 找到当前节点的下一个不是取消状态的节点进行unpark
s = t;
}
if (s != null) // head后面的节点不为空,直接对head后面这个节点unpark 毕竟公平锁
LockSupport.unpark(s.thread);
}
复制代码
问题:第二个if是如何判断三情况的 if (h != null && h.waitStatus != 0) ?
回答:
- 第一种情况,没有AQS队列,head一定为null,就是return true;
- 第二种情况,有AQS队列但是同步队列中只有一个节点,head不为null,但是唯一的一个节点既是头结点也是尾节点,所有h.waitStatus == 0 ,不满足条件,就是return true;
- 第三种情况,有AQS队列但是同步队列中超过一个节点,head不为null,头结点和尾节点是不同节点,所以h.waitStatus==-1,满足第二层if判断,就是 unparkSuccessor(h); 然后 return true;
小结:虽然都是返回为true,解锁成功,但是内部逻辑是不同的。
两种重要问题:
- 问题:为什么需要同步队列AQS?
回答:同步队列是lock公平锁的内部数据结构,非公平锁不需要同步队列。 - 问题:为什么同步队列是非循环双链表,非循环单链表不行吗?
回答:公平锁中,lock.lock()使用尾插法插入,但是,在调用lock.unlock()方法的时候,由于头节点是成功获取到同步状态的节点,而头节点的线程释放了同步状态后,将会唤醒其他后续节点s,后继节点的线程被唤醒后需要检查自己的前驱节点是否是头节点,如果是则尝试获取同步状态。所以为了能让后继节点获取到其前驱节点,同步队列便设置为双向链表,而等待队列没有这样的需求,就为单链表。
四、面试金手指(ReentrantLock中lock()与unlock(),即AQS加锁解锁)
面试问题:lock机制是如何实现公平锁的加锁和解锁的(因为synchronized无法实现公平锁)
回答:下面4.1 4.2 4.3
4.1 先搞懂AQS类中的基本元素Node节点
问题:AQS内部类Node
回答:AQS本质是一个非循环的双向链表(也可以称为队列),所以它是由一个个节点构成的,就是Node,后面的lock() unlock() await() signal()/signalAll()都是以Node为基本元素操作的。
问题:AQS类中的Node内部类中需要保存什么信息呢?
回答:一个六个,其中,prev、next 两个Node类型,表示做指针,thread 存放节点的值,因为AQS队列的节点就是存放线程的,所以这个值类型就是Thread,最后,nextWaiter也是Node类型,表示下一个等待节点, waitStatus表示当前节点等待状态,SHARED|EXCLUSIVE 表示是独占还是共享。
volatile int waitStatus; //当前节点等待状态
volatile Node prev; //上一个节点
volatile Node next; //下一个节点
volatile Thread thread; //节点中的值
Node nextWaiter; //下一个等待节点
//指示节点共享还是独占,默认初始是共享
static final Node SHARED = new Node();
static final Node EXCLUSIVE = null;
复制代码
记住一个Node节点的六个属性(共享/独占算一个),下面看源码就轻松些
- 处在同步队列中使用到的属性(本文用:加锁、解锁)包括:next prev thread waitStatus,所以同步队列是双向非循环链表,涉及的类变量AbstractQueuedSynchronizer类中的head和tail,分别指向同步队列中的头结点和尾节点。
- 处在等待队列中使用到的属性(下一篇博文用:阻塞、唤醒)包括:nextWaiter thread waitStatus,所以等待队列是单向非循环链表,涉及的类变量ConditionObject类中的firstWaiter和lastWaiter,分别指向等待队列中的头结点和尾节点。
- AQS队列是工作队列、同步队列,是非循环双向队列:当使用到head tail的时候,就说AQS队列建立起来了,单个线程不使用到head tail,所以AQS队列没有建立起来;
- 等待队列,是非循环单向队列:当使用firstWaiter lastWaiter的时候,就说等待队列建立起来了。
- lock()和unlock()就是操作同步队列:lock()将线程封装到节点里面(此时,节点使用到的属性是thread nextWaiter waitStatus),放到同步队列,即AQS队列中,unlock()将存放线程的节点从同步队列中拿出来,表示这个线程工作完成。
- await()和signal()就是操作等待队列:await()将线程封装到节点里面(此时,节点使用到的属性是thread prev next waitStatus),放到等待队列里面,signal()从等待队列中拿出元素。
问题:为什么负责同步队列的head和tail在AbstractQueuedSynchronizer类中,但是负责等待队列的firstWaiter和lastWaiter在ConditionObject类中?
回答:
- 线程同步互斥是直接通过ReentrantLock类对象 lock.lock() lock.unlock()实现的,而ReentrantLock类对象是调用AQS类实现加锁解锁的,所以负责同步队列的head和tail在AbstractQueuedSynchronizer类中;
- 线程阻塞和唤醒是通过ReentrantLock类对象lock.newCondition()得到一个对应,condition引用指向这个对象,然后condition.await() condition.signal()实现的,所以负责等待队列的firstWaiter和lastWaiter在ConditionObject类中。
4.2 公平锁加锁
4.2.1 单线程加锁过程
4.2.1.1 整体路程图(第1次加锁 + 第2~n次加锁)
公平锁加锁流程(只有一个线程的时候):
- 直接调用tryAcquire,然后判断state的是不是等于0
- 对于A线程第1次加锁,等于0,证明是第一次加锁,第一次加锁hasQueuedPredecessors()一定通过(返回为false取反为true),通过CAS操作将state的值改成1,然后true,返回true表示加锁成功,就完成了加锁。
- 对于A线程第2~n次加锁,不等于0,表示不是第一次加锁,这个锁是重入锁,这个时候将原来的state值继续通过CAS操作加上1,再次返回true,表示加锁成功,就完成了加锁。
tip1:需要注意的是,这个时候AQS的队列没有创建出来。
tip2:setExclusiveOwnerThread(current); // 这里是设置当前节点为独占
tip3:看源码的时候,知道自己在看什么,这里是看FairSync的lock()方法实现
tip4:源码一般命名优美,可以从命名上来看,帮助理清思路,例如 lock()是加锁、acquire()是去获得tryAcquire() 是尝试加锁、acquireQueued()是获得队列
4.2.1.2 重要方法:tryAcquire()源码解析
对于只有一个线程A使用lock.lock();加锁,最重要的方法就是tryAcquire,就是这个方法,将线程A区分对待,第一次加锁和非第一次加锁,源码拿出来讲一讲:
protected final boolean tryAcquire(int acquires) { // **1、tryAcquire是去获取,2、返回为true就是使用获取的方式加锁成功(可以第一次,也可以是重入锁)**
final Thread current = Thread.currentThread();
int c = getState(); // 当前状态
if (c == 0) { // 当前状态为0,就是默认状态
if (!hasQueuedPredecessors() && // **1、hasQueuedPredecessors这个方法重要,下面解释**
compareAndSetState(0, acquires)) { // **1、只要上面那个hasQueuedPredecessors()返回为false,取反为true,这个cas一定是可以通过的,只是自旋等一下罢了**
setExclusiveOwnerThread(current); // **1、设置当前线程为独占线程,因为当前线程已经加锁成功了,所以设置当前线程为互斥资源的独占线程**
//**2、为什么说当前线程加锁成功了,因为这里返回true啊**
return true;
}
}
else if (current == getExclusiveOwnerThread()) { // 这句表示当前线程为独占线程
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true; // **1、因为当前线程是独占线程,所以一定是加锁成功,这里返回true就好
// 2、既然已经是独占线程,就没有必要再次设置当前线程为独占线程了,直接返回true**
}
return false; // **1、如果既不是第一次,也不是重入锁,就不能通过获取的方式去加锁,要自己加锁,这里返回false,加锁失败**
}
复制代码
4.2.1.3 重要方法:hasQueuedPredecessors()源码解析
问题:hasQueuedPredecessors()源码解析?
回答:
-
如果h == t成立,h和t均为null或是同一个具体的节点,无后继节点,方法返回false,表示要通过获取锁tryLock()加锁,这种情况是第一个节点,类变量tail == head == null。
key:如果类变量head==tail,表示没有节点或只有一个节点,所以一定是没有前驱节点的,方法直接返回false,不用多说,注意,后面的,head!=tail至少两个节点 -
如果h!=t成立(至少两个节点),而且 head.next == 为null,方法返回true。什么情况下h!=t的同时h.next==null??有其他线程第一次正在入队时,可能会出现。见AQS的enq方法,compareAndSetHead(node)完成,还没执行tail=head语句时,此时tail=null,head=newNode,head.next=null。
key:此时,头结点设置为新建节点,所以head=newNode
但是,还未将头结点设置为尾节点,所以tail=null,为默认值
同时,这是第一次执行enq()方法,没有设置 node.prev = t; 和 t.next = node;,所以head.next=null。 -
如果h!=t成立(当前类变量head 和 tail不是同一个,至少两个节点),head.next != null,(true && (false || ))则判断head.next是否是当前线程,如果是当前线程(true && (false || false)),方法返回false,key:表示要通过获取锁的方式加锁;如果不是当前线程(true && (false || true))返回true,key:表示正在运行的线程不是当前的这个current,需要等待。
(head节点是获取到锁的节点,但是任意时刻head节点可能占用着锁,也可能释放了锁(unlock()),未被阻塞的head.next节点对应的线程在任意时刻都是有必要去尝试获取锁)
实际上,hasQueuedPredecessors返回为true不通过,只需要等一段时间罢了(上面关于hasQueuedPredecessors方法的意义:如果该方法返回true,则表示有线程比当前线程更早地请求获取锁,因此需要等待前驱线程获取并释放锁之后才能继续获取锁)
问题:hasQueuedPredecessors()方法是用来干什么的?
回答:
- hasQueuedPredecessors()方法的意义:该方法表示对加入了同步队列中当前节点是否有前驱节点的判断,如果该方法返回true,则表示有线程比当前线程更早地请求获取锁,因此需要等待前驱线程获取并释放锁之后才能继续获取锁,因为是公平锁,要按照队列来。
- hasQueuedPredecessors()方法的调用以及返回的意义:hasQueuedPredecessors()方法只在tryAcquire()方法里面被调用执行过,hasQueuedPredecessors()返回false表示要尝试通过获取锁的方式加锁,没有前驱节点请求获取锁,返回为true表示不需要通过获取锁的方式加锁,已经有前驱节点请求获取了。
问题:如何进入hasQueuedPredecessors()方法?
回答:lock() -> acquire() -> tryAcquire() -> hasQueuedPredecessors()
4.2.2 两个线程加锁过程(A线程 B线程)
4.2.2.1 第一种情况:当线程B进来的时候,死循环中线程A没有解锁
第一,建立AQS队列
我们假设线程A直接获取到了锁(获取锁的过程和上面单线程一样,不再赘言),但是线程A还没有解锁,这个时候线程B来进行加锁,走来会执行tryAcquire()方法,这个时候线程A没有解锁,所以这个tryAcquire()方法会直接返回false(state!=0,也不是重入锁),然后会调用addWaiter(Node.EXCLUSIVE)方法(addWaiter()是新方法:上面一个线程的时候没有涉及到,这里要重点分析),这个时候会在这个方法中的enq(node)的方法中初始化AQS队列,也会利用尾插法将当前的节点插入到AQS队列中去。AQS队列如下图所示:
对于当前的AQS队列解释:
- 这时AQS队列新建起来了
- head节点中thread=null表示没有存放任何线程,waitStatus=0表示什么都不是
- tail节点中thread=线程B表示存放的是线程B,waitStatus=0表示什么都不是
第二,addWaiter()方法中调用enq()方法,新建AQS队列
完成AQS队列的方法是addWaiter()中调用的enq()方法,且看addWaiter()方法和enq()方法
private Node addWaiter(Node mode) { // **1、实际参数是Node.EXCLUSIVE,就是当前独占节点,表示下一个等待节点就是正在独占的那个线程的节点,因为它释放锁就要到插入了,所以这个方法称为addWaiter,意为添加下一个等待节点**
Node node = new Node(Thread.currentThread(), mode); // 新建一个节点,存放当前线程,当前线程为内容,实参为下一个等待节点nextWaiter
Node pred = tail; // 当前尾节点赋值,当前tail==null
if (pred != null) {
node.prev = pred; // 如果不为空,进来,新建节点的前一个是之前的尾节点,就是尾插法
if (compareAndSetTail(pred, node)) { // 设置新的尾节点,从之前的尾节点pred到现在的node
pred.next = node; // 之前尾节点的next设置为这个节点
// **由此可知,尾插法三步骤:设置新节点的prev为之前尾节点、重新设置tail类变量的指向、设置之前尾节点的next为新建节点(就是三个Node类型指针而已,很简单)**
return node; // 返回新建节点
}
}
enq(node); // 返回值没有接收者,但是队列新建好了
return node; // 返回这个新建的节点
}
private Node enq(final Node node) {
for ( ; ; ) { // **1、死循环,不创建好AQS队列不退出**
Node t = tail;
if (t == null) { // Must initialize **1、第一次进入,必须初始化,这里表示连尾节点都没有**
if (compareAndSetHead(new Node())) // **for+if(cas)就是线程同步**
tail = head; // **1、新建一个节点,设置为头结点,因为只有一个节点,所以尾节点也是这个节点**
} else {
node.prev = t; // **1、这是时候,第二次循环,因为head tail都是新节点,第二次循环中使用 Node t = tail;将t设置为这个新节点**
if (compareAndSetTail(t, node)) { // 方法名是compareAndSetTail,表示设置尾节点,自旋,知道设置成功 for+if(cas)就是线程同步,设置tail类变量,将tail从t变为node,所以传入参数node是尾节点
t.next = node; // 尾节点指向参数node,头结点还是指向t
// **由此可知,尾插法三步骤:设置参数节点的prev为之前尾节点t、重新设置tail类变量的指向从之前的t到参数节点node、设置之前尾节点t的next为参数节点node(就是三个Node类型指针而已,很简单),最后队列两个元素 t 和 node**
return t; // 返回头结点t
}
}
}
}
复制代码
tip:head和tail是类变量,类似指针,指向其他节点
compareAndSetTail(t, node) // 设置tail类变量,将tail从t变为node,所以传入参数node是尾节点
compareAndSetHead(t, node) // 设置tail类变量,将head从t变为node,所以传入参数node是头节点
compareAndSetState(0, acquires) // 设置state类变量,从0到1,cas保证安全
第三,acquireQueued()方法接收addWaiter()返回值,调用shouldParkAfterFailedAcquire()方法,修改AQS队列中节点的waitStatus值从0到1
方法addWaiter()返回当前的节点,然后调用acquireQueued(addWaiter(Node.EXCLUSIVE), arg))方法(tip:返回的刚刚在addWaiter()方法中新建的最尾巴的节点作为acquireQueued方法的参数,arg参数是1,传递过来的)。这个方法中是一个死循环,由于线程A没有释放锁(tryAcquire()方法会直接返回false(state!=0,也不是重入锁)),会执行shouldParkAfterFailedAcquire(p, node)(p表示线程B节点的上一个节点,p = node.predecessor();就是最尾巴节点上一个,node表示线程B的节点,就是addWaiter()方法中新建的最尾巴的节点)第一次进这个方法会将线程B节点的上一个节点的waitStatus的值改成-1(执行最后一个else compareAndSetWaitStatus(pred, ws, Node.SIGNAL);),然后返回false,这个时候的AQS队列如下图:
tip:仔细对比两个图,和上一次的改变是:线程B节点的上一个节点的waitStatus的值从0修改成-1。
-
第一次进入shouldParkAfterFailedAcquire()方法会将线程B节点的上一个节点的waitStatus的值改成-1,然后返回false。此时的AQS工作队列:head节点thread==null表示没有存放线程,waitStatus=-1表示后面节点的线程需要释放;tail节点thread=线程B表示存放的是线程B,waitStatus=0表示什么都不是。
-
第二次进入shouldParkAfterFailedAcquire()方法的时候,会返回true( if (ws == Node.SIGNAL) return true;),会执行后面的方法parkAndCheckInterrupt()( LockSupport.park(this);),这个时候线程B就会被park在这。(1、直到线程A解锁了,第二种情况可以当做第一个情况后面的执行来看)
4.2.2.2 第二种情况:当线程B进来的时候,死循环中线程A已经解锁
上面的情况都是在线程A没有解锁的时候,如果在死循环中线程A已经解锁了。这个时候判断线程B节点的上一个节点是不是头结点,如果是的话,直接执行tryAcquire(),将当前线程B设置成独占线程,同时将state的值通过CAS操作设置成1,如果成功的话,直接返回true。表示加锁成功。这个时候会执行这个if判断中代码。执行setHead(node),这个时候AQS队列如下图:
if (p == head && tryAcquire(arg)) {
setHead(node); // node就是addWaiter的尾巴节点,
p.next = null; // help GC 看Java四种引用就知道 前面那个节点的next设置为null
failed = false; // 局部变量failed初始为true,要下去执行cancelAcquire,这里设置为false,不执行cancelAcquire了
return interrupted; // false
}
private void setHead(Node node) {
head = node; // 类变量head指向addWaiter的尾巴节点
node.thread = null; // 这个节点thread=null
node.prev = null; // 这个节点prev==null 因为要变成头结点,非循环双向链表,所以前驱指针为null
}
复制代码
这个时候原来的线程B节点出队列(因为B节点要去执行了),然后永远会维护一个头结点中thread为null的AQS队列。
4.3 lock.unlock()解锁过程
线程解锁的三种情况:
-
当前线程不在AQS队列中,执行tryRelease()方法。
如果当前线程不是重入锁(即满足 if(c==0)为true),直接将当前的线程独占标识去除掉,然后将state的值通过CAS的操作改成0;
如果当前线程加的是重入锁(即满足 if(c!=0)为false),解锁一次,state的值减1,如果state的值是等于0的时候,返回true。表示解锁成功。 -
AQS队列中只有一个头结点,这个时候tryRelease()返回的结果和上面的情况是一样的。这个时候返回的true,会进当前的if中去,然后判断头结点是不是为null和头结点中waitStatus的值是不是等于0。这个时候head不等于null,但是waitState是等于0,if判断不成立,不会执行unpark的方法。会直接返回true。表示解锁成功。
-
AQS队列中不止一个头结点,还有其他节点,这个时候tryRelease()返回的结果和上面的情况是一样的。这个时候返回的true,会进当前的if中去,然后判断头结点是不是为null和头结点中waitStatus的值是不是等于0。这个时候head不等于null,但是waitState是等于-1,if判断成立,会执行unpark的方法。unpark方法中会unpark头结点的下一个节点,然后如果当前的节点的状态是取消的状态,会从最后一个节点开始找,找到当前节点的下一个不是取消状态的节点进行unpark。这个时候也会直接返回true。表示解锁成功。
问题:第二个if是如何判断三情况的 if (h != null && h.waitStatus != 0) ?
回答:
- 第一种情况,没有AQS队列,head一定为null,就是return true;
- 第二种情况,有AQS队列但是同步队列中只有一个节点,head不为null,但是唯一的一个节点既是头结点也是尾节点,所有h.waitStatus == 0 ,不满足条件,就是return true;
- 第三种情况,有AQS队列但是同步队列中超过一个节点,head不为null,头结点和尾节点是不同节点,所以h.waitStatus==-1,满足第二层if判断,就是 unparkSuccessor(h); 然后 return true;
小结:虽然都是返回为true,解锁成功,但是内部逻辑是不同的。
两种重要问题:
- 问题:为什么需要同步队列AQS?
回答:同步队列是lock公平锁的内部数据结构,非公平锁不需要同步队列。 - 问题:为什么同步队列是非循环双链表,非循环单链表不行吗?
回答:公平锁中,lock.lock()使用尾插法插入,但是,在调用lock.unlock()方法的时候,由于头节点是成功获取到同步状态的节点,而头节点的线程释放了同步状态后,将会唤醒其他后续节点s,后继节点的线程被唤醒后需要检查自己的前驱节点是否是头节点,如果是则尝试获取同步状态。所以为了能让后继节点获取到其前驱节点,同步队列便设置为双向链表,而等待队列没有这样的需求,就为单链表。
五、小结
ReentrantLock中lock()、unlock() 全解析,完成了。
天天打码,天天进步!!!
作者:毛毛的学习笔记
链接:https://juejin.im/post/6893438433826439182
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。