unlock java_Java并发原理层面:ReentrantLock中lock()、unlock()全解析

一、前言

Java线程同步两种方式,synchronized关键字和Lock锁机制,其中,AQS队列就是Lock锁实现公平加锁的底层支持。

二、AQS源码对于lock.lock()的实现

2.1 AQS类 + 内部Node类

2.1.1 AQS类结构示意图

首先我们要看看AQS的结构的类图

10d33e281dc1d417af50cc3301ea079f.png

从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指针,具体如下图:

b1d127af904ea7ba66cde61e5f9f229d.png

问题: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),如下图所示:

06462635595f5582b75c9bae309618ad.png

对于上图的解释,看这张图的正确姿势是:

上图中涉及的方法包括 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队列中有两个线程的情况,如下图所示:

eb1b2ccfbb197e096a4479d3d85d2217.png

上图注意两点:

流程:两个线程的运行,左边是线程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队列如下图所示:

92d0b1516230d59206d77f3024d52866.png

对于当前的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保证安全

65c46871ccc47828ccb6cb69dee89ea7.png

第三,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队列如下图:

20d46f96a45da9c2ba66a66c78a01d72.png

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队列如下图:

21d47c8241c77caaba5db4422aa302a1.png

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方法中有三个线程情况,如下图:

14c8d079d8fdfc85a9228f784869b870.png

三个线程和两个线程的情况是差不多的,即加锁成功的节点永远是头结点的下一个节点中的线程加锁成功,因为是公平锁。

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 解锁整体流程图

caedc063fc60e13a908b0c658908c5f0.png

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队列如下图所示:

92d0b1516230d59206d77f3024d52866.png

对于当前的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保证安全

65c46871ccc47828ccb6cb69dee89ea7.png

第三,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队列如下图:

20d46f96a45da9c2ba66a66c78a01d72.png

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队列如下图:

21d47c8241c77caaba5db4422aa302a1.png

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() 全解析,完成了。

天天打码,天天进步!!!

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值