加锁与解锁,同步队列之精彩

Posted 毛奇志

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了加锁与解锁,同步队列之精彩相关的知识,希望对你有一定的参考价值。

文章目录

一、前言

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;

    // 默认值0,什么都不是
    volatile int waitStatus;   
    
    //上一个节点
    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、thread、nextWaiter、waitStatus、SHARED|EXCLUSIVE。其中,prev、next 两个Node类型,表示做指针;thread 存放节点的值,因为AQS队列的节点就是存放线程的,所以这个值类型就是Thread;最后,nextWaiter也是Node类型,表示下一个等待节点, waitStatus表示当前节点等待状态,SHARED|EXCLUSIVE 表示是独占还是共享。如下:

volatile Node prev;       //上一个节点
volatile Node next;         //下一个节点
volatile Thread thread;         //节点中的值
Node nextWaiter;        //下一个等待节点
volatile int waitStatus;   //当前节点等待状态
 //指示节点共享还是独占,默认初始是共享
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),如下图所示:

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

第一步:看到上图中涉及的方法包括 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,表示加锁成功,就完成了加锁。需要注意的是,这个时候AQS的队列没有创建出来。

源码一般命名优美,可以从命名上来看,帮助理清思路,例如 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,加锁失败**

if (!hasQueuedPredecessors() && 表示公平锁只有在同步队列中,前面没有元素的时候,各个线程才会抢占锁,队列前面有元素,各个线程不会自由抢占,会认认真真地排队。非公平锁永远是自由抢占,没有 if (!hasQueuedPredecessors() && 这一句。

所以说,公平锁和非公平锁的本质就是在于tryAcquire方法的不同。

为什么会有重入?嵌套代码会有重入,如下:


释放锁的线程一定是占有锁的线程,所以是释放锁的tryRelease方法是线程安全的。

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());

这个方法很简单,最重要的就是它的返回值 h != t && ((s = h.next) == null || s.thread != Thread.currentThread()); ,这里分为三种情况来看其返回值,如下:

情况1:如果h == t成立,此时要么h和t均为null表示零个节点,要么h和t为同一个具体的节点表示只有一个节点,不管怎样,都是无后继节点,方法直接返回false,表示需要通过获取锁tryLock()加锁,要么零个节点,要么一个节点,当前节点就是第一个节点。

如果类变量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。此时,头结点设置为新建节点,所以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,表示要通过获取锁的方式加锁;如果不是当前线程(true && (false || true))方法返回true,表示正在运行的线程不是当前的这个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队列中有两个线程的情况,如下图所示:

上图注意两点:
(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()方法

非公平锁加锁流程:

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

2.4 其他加锁方法

2.4.1 ReentrantLock类的tryLock()方法

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.4.2 ReentrantLock类的tryLock(long timeout, TimeUnit unit)方法

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.4.3 其他加锁方法:ReentrantLock类的lockInterruptibly()方法

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())
             

以上是关于加锁与解锁,同步队列之精彩的主要内容,如果未能解决你的问题,请参考以下文章

AQS (Abstract Queued Synchronizer)源码解析 -- 独占锁与共享锁的加锁与解锁

Redis分布式锁的正确加锁与解锁方式

golang加锁与解锁

Synchronized&Lock&AQS详解

java虚拟机之线程同步

synchronized实现可见性