一、前言

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

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

2.1 AQS类 + 内部Node类

2.1.1 AQS类结构示意图

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


从AQS类的类结构示意图可以知道,

  1. AbstractQueuedSynchronizer的父类是AbstractOwnableSynchronizer;
  2. 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:

  1. SIGNAL=-1:这个节点的后继被(或即将)阻塞(通过park),因此当前节点在释放或取消时必须释放它的后继。为了避免竞争,acquire方法必须首先表明它们需要一个信号,然后重试原子获取,当失败时,阻塞。
  2. CANCELLED=1:由于超时或中断,该节点被取消。节点不会离开这个状态。特别是,取消节点的线程不会再次阻塞。
  3. CONDITION=-2:此节点当前处于条件队列中。在传输之前,它不会被用作同步队列节点,此时状态将被设置为0。
  4. PROPAGATE=-3:释放的共享应该传播到其他节点。在doReleaseShared中设置这个(仅针对头节点),以确保传播继续,即使其他操作已经干预。
  5. 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节点的六个属性(共享/独占算一个),下面看源码就轻松些

  1. 处在同步队列中使用到的属性(本文用:加锁、解锁)包括:next prev thread waitStatus,所以同步队列是双向非循环链表,涉及的类变量AbstractQueuedSynchronizer类中的head和tail,分别指向同步队列中的头结点和尾节点。
  2. 处在等待队列中使用到的属性(下一篇博文用:阻塞、唤醒)包括:nextWaiter thread waitStatus,所以等待队列是单向非循环链表,涉及的类变量ConditionObject类中的firstWaiter和lastWaiter,分别指向等待队列中的头结点和尾节点。
  3. AQS队列是工作队列、同步队列,是非循环双向队列:当使用到head tail的时候,就说AQS队列建立起来了,单个线程不使用到head tail,所以AQS队列没有建立起来;
  4. 等待队列,是非循环单向队列:当使用firstWaiter lastWaiter的时候,就说等待队列建立起来了。
  5. lock()和unlock()就是操作同步队列:lock()将线程封装到节点里面(此时,节点使用到的属性是thread nextWaiter waitStatus),放到同步队列,即AQS队列中,unlock()将存放线程的节点从同步队列中拿出来,表示这个线程工作完成。
  6. 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),如下图所示:

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

  1. 上图中涉及的方法包括 acquire() 、 tryAcquire() 、 hasQueuedPredecessors()方法;
  2. 对于acquire() 方法,就是直接调用 tryAcquire() 方法,实参传入1。
  3. 对于tryAcquire()方法,由于只有一个线程A,第一次进入的时候要CAS加锁(if判断后半段)并将自己设置为独占线程(if代码段),所以两行代码,以后就是重入锁了,直接进入,不用CAS操作和设置独占锁了,直接通过setState()修改state值,每次加一即可,因为实参acquires为1。
  4. 对于hasQueuedPredecessors()方法,该方法就是判断AQS工作队列是否建立起来了,这里h==t,所以,没有建立起来,返回false。
  5. 总体流程,对于同一线程A,对于图中唯一一个菱形判断框,第一次进入的时候要CAS加锁(if判断后半段)并将自己设置为独占线程(if代码段);第2~n次进入就是重入锁,直接进入,不用CAS操作和设置独占锁了,直接通过setState()修改state值,每次加一即可,这就是整体流程,线程A的整体流程,分为两种球情况而已。

公平锁加锁流程(只有一个线程的时候):

  1. 直接调用tryAcquire,然后判断state的是不是等于0
  2. 对于A线程第1次加锁,等于0,证明是第一次加锁,第一次加锁hasQueuedPredecessors()一定通过(返回为false取反为true),通过CAS操作将state的值改成1,然后true,返回true表示加锁成功,就完成了加锁。
  3. 对于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()源码解析?
回答:

  1. 如果h == t成立,h和t均为null或是同一个具体的节点,无后继节点,方法返回false,表示要通过获取锁tryLock()加锁,这种情况是第一个节点,类变量tail == head == null。
    key:如果类变量head==tail,表示没有节点或只有一个节点,所以一定是没有前驱节点的,方法直接返回false,不用多说,注意,后面的,head!=tail至少两个节点

  2. 如果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。

  3. 如果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()方法是用来干什么的?
回答:

  1. hasQueuedPredecessors()方法的意义:该方法表示对加入了同步队列中当前节点是否有前驱节点的判断,如果该方法返回true,则表示有线程比当前线程更早地请求获取锁,因此需要等待前驱线程获取并释放锁之后才能继续获取锁,因为是公平锁,要按照队列来。
  2. hasQueuedPredecessors()方法的调用以及返回的意义:hasQueuedPredecessors()方法只在tryAcquire()方法里面被调用执行过,hasQueuedPredecessors()返回false表示要尝试通过获取锁的方式加锁,没有前驱节点请求获取锁,返回为true表示不需要通过获取锁的方式加锁,已经有前驱节点请求获取了。

问题:如何进入hasQueuedPredecessors()方法?
回答:lock() -> acquire() -> tryAcquire() -> hasQueuedPredecessors()

2.2.2 lock方法中有两个线程的情况

2.2.2.1 整体路程图(线程A加锁 + 线程B加锁)

lock方法实现的公平锁AQS队列中有两个线程的情况,如下图所示:

上图注意两点:

  1. 流程:两个线程的运行,左边是线程A先获得锁,然后线程B再去尝试获得锁,但是线程B一定要等到线程A释放锁之后,才能获得锁成功,在这之前只能阻塞,即源码中for循环;
  2. 仔细对比两个图,和上一次的改变是:线程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队列解释:

  1. 这时AQS队列新建起来了
  2. head节点中thread=null表示没有存放任何线程,waitStatus=0表示什么都不是
  3. 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。

  1. 第一次进入shouldParkAfterFailedAcquire()方法会将线程B节点的上一个节点的waitStatus的值改成-1,然后返回false。此时的AQS工作队列:head节点thread==null表示没有存放线程,waitStatus=-1表示后面节点的线程需要释放;tail节点thread=线程B表示存放的是线程B,waitStatus=0表示什么都不是。

  2. 第二次进入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()区别就好)

非公平锁加锁流程:

  1. 非公平锁会走来直接尝试加锁;
  2. 如果加锁成功,直接执行线程中的代码;
  3. 如果加锁不成功,直接走公平锁的逻辑。

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()大同小异)

  1. tryLock(long timeout, TimeUnit unit)方法,加了一个获取锁的时间,如果这个时间内没有获取到锁,直接返回false,表示加锁失败;如果在这个时间内调用tryAcquire(arg)获得到锁,表示加锁成功,tryAcquireNanos(int arg, long nanosTimeout)方法返回值直接为true,即tryLock(long timeout, TimeUnit unit)方法返回值为true。

  2. 如果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的区别:

  1. lockInterruptibly是会立即响应中断的:并且在park中的线程也会interruptibly唤醒的,因为这个时候返回true,直接抛出异常,响应对应的中断。
  2. 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;
}

线程解锁的三种情况:

  1. 当前线程不在AQS队列中,执行tryRelease()方法。
    如果当前线程不是重入锁(即满足 if(c==0)为true),直接将当前的线程独占标识去除掉,然后将state的值通过CAS的操作改成0;
    如果当前线程加的是重入锁(即满足 if(c!=0)为false),解锁一次,state的值减1,如果state的值是等于0的时候,返回true。表示解锁成功。

  2. AQS队列中只有一个头结点,这个时候tryRelease()返回的结果和上面的情况是一样的。这个时候返回的true,会进当前的if中去,然后判断头结点是不是为null和头结点中waitStatus的值是不是等于0。这个时候head不等于null,但是waitState是等于0,if判断不成立,不会执行unpark的方法。会直接返回true。表示解锁成功。

  3. 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) ?
回答:

  1. 第一种情况,没有AQS队列,head一定为null,就是return true;
  2. 第二种情况,有AQS队列但是同步队列中只有一个节点,head不为null,但是唯一的一个节点既是头结点也是尾节点,所有h.waitStatus == 0 ,不满足条件,就是return true;
  3. 第三种情况,有AQS队列但是同步队列中超过一个节点,head不为null,头结点和尾节点是不同节点,所以h.waitStatus==-1,满足第二层if判断,就是 unparkSuccessor(h); 然后 return true;

小结:虽然都是返回为true,解锁成功,但是内部逻辑是不同的。

两种重要问题:

  1. 问题:为什么需要同步队列AQS?
    回答:同步队列是lock公平锁的内部数据结构,非公平锁不需要同步队列。
  2. 问题:为什么同步队列是非循环双链表,非循环单链表不行吗?
    回答:公平锁中,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节点的六个属性(共享/独占算一个),下面看源码就轻松些

  1. 处在同步队列中使用到的属性(本文用:加锁、解锁)包括:next prev thread waitStatus,所以同步队列是双向非循环链表,涉及的类变量AbstractQueuedSynchronizer类中的head和tail,分别指向同步队列中的头结点和尾节点。
  2. 处在等待队列中使用到的属性(下一篇博文用:阻塞、唤醒)包括:nextWaiter thread waitStatus,所以等待队列是单向非循环链表,涉及的类变量ConditionObject类中的firstWaiter和lastWaiter,分别指向等待队列中的头结点和尾节点。
  3. AQS队列是工作队列、同步队列,是非循环双向队列:当使用到head tail的时候,就说AQS队列建立起来了,单个线程不使用到head tail,所以AQS队列没有建立起来;
  4. 等待队列,是非循环单向队列:当使用firstWaiter lastWaiter的时候,就说等待队列建立起来了。
  5. lock()和unlock()就是操作同步队列:lock()将线程封装到节点里面(此时,节点使用到的属性是thread nextWaiter waitStatus),放到同步队列,即AQS队列中,unlock()将存放线程的节点从同步队列中拿出来,表示这个线程工作完成。
  6. await()和signal()就是操作等待队列:await()将线程封装到节点里面(此时,节点使用到的属性是thread prev next waitStatus),放到等待队列里面,signal()从等待队列中拿出元素。

问题:为什么负责同步队列的head和tail在AbstractQueuedSynchronizer类中,但是负责等待队列的firstWaiter和lastWaiter在ConditionObject类中?

回答:

  1. 线程同步互斥是直接通过ReentrantLock类对象 lock.lock() lock.unlock()实现的,而ReentrantLock类对象是调用AQS类实现加锁解锁的,所以负责同步队列的head和tail在AbstractQueuedSynchronizer类中;
  2. 线程阻塞和唤醒是通过ReentrantLock类对象lock.newCondition()得到一个对应,condition引用指向这个对象,然后condition.await() condition.signal()实现的,所以负责等待队列的firstWaiter和lastWaiter在ConditionObject类中。

4.2 公平锁加锁

4.2.1 单线程加锁过程

4.2.1.1 整体路程图(第1次加锁 + 第2~n次加锁)

公平锁加锁流程(只有一个线程的时候):

  1. 直接调用tryAcquire,然后判断state的是不是等于0
  2. 对于A线程第1次加锁,等于0,证明是第一次加锁,第一次加锁hasQueuedPredecessors()一定通过(返回为false取反为true),通过CAS操作将state的值改成1,然后true,返回true表示加锁成功,就完成了加锁。
  3. 对于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()源码解析?
回答:

  1. 如果h == t成立,h和t均为null或是同一个具体的节点,无后继节点,方法返回false,表示要通过获取锁tryLock()加锁,这种情况是第一个节点,类变量tail == head == null。
    key:如果类变量head==tail,表示没有节点或只有一个节点,所以一定是没有前驱节点的,方法直接返回false,不用多说,注意,后面的,head!=tail至少两个节点

  2. 如果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。

  3. 如果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()方法是用来干什么的?
回答:

  1. hasQueuedPredecessors()方法的意义:该方法表示对加入了同步队列中当前节点是否有前驱节点的判断,如果该方法返回true,则表示有线程比当前线程更早地请求获取锁,因此需要等待前驱线程获取并释放锁之后才能继续获取锁,因为是公平锁,要按照队列来。
  2. 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队列解释:

  1. 这时AQS队列新建起来了
  2. head节点中thread=null表示没有存放任何线程,waitStatus=0表示什么都不是
  3. 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。

  1. 第一次进入shouldParkAfterFailedAcquire()方法会将线程B节点的上一个节点的waitStatus的值改成-1,然后返回false。此时的AQS工作队列:head节点thread==null表示没有存放线程,waitStatus=-1表示后面节点的线程需要释放;tail节点thread=线程B表示存放的是线程B,waitStatus=0表示什么都不是。

  2. 第二次进入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()解锁过程

线程解锁的三种情况:

  1. 当前线程不在AQS队列中,执行tryRelease()方法。
    如果当前线程不是重入锁(即满足 if(c==0)为true),直接将当前的线程独占标识去除掉,然后将state的值通过CAS的操作改成0;
    如果当前线程加的是重入锁(即满足 if(c!=0)为false),解锁一次,state的值减1,如果state的值是等于0的时候,返回true。表示解锁成功。

  2. AQS队列中只有一个头结点,这个时候tryRelease()返回的结果和上面的情况是一样的。这个时候返回的true,会进当前的if中去,然后判断头结点是不是为null和头结点中waitStatus的值是不是等于0。这个时候head不等于null,但是waitState是等于0,if判断不成立,不会执行unpark的方法。会直接返回true。表示解锁成功。

  3. AQS队列中不止一个头结点,还有其他节点,这个时候tryRelease()返回的结果和上面的情况是一样的。这个时候返回的true,会进当前的if中去,然后判断头结点是不是为null和头结点中waitStatus的值是不是等于0。这个时候head不等于null,但是waitState是等于-1,if判断成立,会执行unpark的方法。unpark方法中会unpark头结点的下一个节点,然后如果当前的节点的状态是取消的状态,会从最后一个节点开始找,找到当前节点的下一个不是取消状态的节点进行unpark。这个时候也会直接返回true。表示解锁成功。

问题:第二个if是如何判断三情况的 if (h != null && h.waitStatus != 0) ?
回答:

  1. 第一种情况,没有AQS队列,head一定为null,就是return true;
  2. 第二种情况,有AQS队列但是同步队列中只有一个节点,head不为null,但是唯一的一个节点既是头结点也是尾节点,所有h.waitStatus == 0 ,不满足条件,就是return true;
  3. 第三种情况,有AQS队列但是同步队列中超过一个节点,head不为null,头结点和尾节点是不同节点,所以h.waitStatus==-1,满足第二层if判断,就是 unparkSuccessor(h); 然后 return true;

小结:虽然都是返回为true,解锁成功,但是内部逻辑是不同的。

两种重要问题:

  1. 问题:为什么需要同步队列AQS?
    回答:同步队列是lock公平锁的内部数据结构,非公平锁不需要同步队列。
  2. 问题:为什么同步队列是非循环双链表,非循环单链表不行吗?
    回答:公平锁中,lock.lock()使用尾插法插入,但是,在调用lock.unlock()方法的时候,由于头节点是成功获取到同步状态的节点,而头节点的线程释放了同步状态后,将会唤醒其他后续节点s,后继节点的线程被唤醒后需要检查自己的前驱节点是否是头节点,如果是则尝试获取同步状态。所以为了能让后继节点获取到其前驱节点,同步队列便设置为双向链表,而等待队列没有这样的需求,就为单链表。

五、小结

ReentrantLock中lock()、unlock() 全解析,完成了。

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

最新文章

  1. oracle case when
  2. (转) 一步一步学习ASP.NET 5 (五)- TypeScript
  3. android StringBuffer类的使用
  4. Spring事务传播、隔离等级
  5. shell脚本步骤调试
  6. HDU 2042 不容易系列之二 [补6.24] 分类: ACM 2015-06-26 20:40 9人阅读 评论(0) 收藏
  7. WPF多语言化的实现
  8. Java中的面向接口编程
  9. iOS开发——新特性OC篇&amp;Objective新特性
  10. C函数数组元素初始化
  11. Varnish缓存服务
  12. Skewed Sorting
  13. ZABBIX自定义用户KEY与参数USERPARAMETERS监控脚本输出
  14. 一个苹果证书怎么多次使用——导出p12文件
  15. Spring mvc 数据验证
  16. java程序测试之字符流
  17. hi3531 SDK 编译 kernel, 修改 参数
  18. win10子系统 (linux for windows)打造python, pytorch开发环境
  19. Win10下 usart驱动PL2303无法安装的问题
  20. Android手机上浏览器不支持带端口号wss解决方案

热门文章

  1. Mac根据端口找进程id
  2. 使用『jQuery』『原生js』制作一个选项卡盒子 —— { }
  3. 《网页设计基础——CSS的四种引入方式详解》
  4. kali2020.1修改root密码,以最高权限登录系统
  5. Redisson多策略注解限流
  6. redis cluster 6.2集群
  7. 使用growpart扩容CentOS虚拟机磁盘
  8. 论文解读(FedGAT)《Federated Graph Attention Network for Rumor Detection》
  9. WPF绘制圆形调色盘
  10. sg函数入门理解