IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> Java知识库 -> 加锁与解锁,同步队列之精彩 -> 正文阅读

[Java知识库]加锁与解锁,同步队列之精彩

文章目录

一、前言

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())
                interrupted = true;    // 没有立即响应中断,仅仅设置一个标志位interrupt=true
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

AcquireQueue方法 if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt())表示 上一个if tryAcquire抢占不到,所以 看是否可以阻塞,如果可以阻塞,LockSupport.park() 阻塞。

三、源码解析lock.unlock()

讲完了加锁的过程,我们再来看看解锁的过程,即ReentrantLock类的unlock()方法。

3.1 解锁整体流程图

在这里插入图片描述

3.2 解锁操作

解锁涉及的函数调用链路是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队列中

(1) 如果当前线程不是重入锁(即满足 if(c==0)为true),直接将当前的线程独占标识去除掉,然后将state的值通过CAS的操作改成0;
(2) 如果当前线程加的是重入锁(即满足 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()源码解析

unparkSuccessor()方法在release中被调用,如下:

private void unparkSuccessor(Node node) {  // 同步队列中的头结点head传递过来
     int ws = node.waitStatus;  // 为 -1 
     if (ws < 0)   // 当ws小于0,通过CAS操作设置状态node的ws变为0
         compareAndSetWaitStatus(node, ws, 0); 
     Node s = node.next;   // 找到工作队列的head的后面一个节点
     // 情况1:head后面这个节点为空,或者waitStatus大于0
     if (s == null || s.waitStatus > 0) {  
         s = null;  // 如果是因为waitStatus大于0而进入这个if,设置head后面的这个节点为null
         // 从尾巴开始遍历,局部变量为t,没有遍历完或没找到node就继续找
         for (Node t = tail; t != null && t != node; t = t.prev)  
             // 如果t.waitStatus <= 0,将这个t记录到s,用于后面unpark   
             // 找到当前节点的下一个不是取消状态的节点进行unpark(unpark即唤醒)
             if (t.waitStatus <= 0)                    
                 s = t;   
     }
     // 情况2:head后面的节点不为空,直接对head后面这个节点unpark(unpark即唤醒)  毕竟公平锁
     if (s != null)   
         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,解锁成功,但是内部逻辑是不同的。

问题1:为什么需要同步队列AQS?
回答1:同步队列是lock公平锁的内部数据结构,非公平锁不需要同步队列。

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

四、面试金手指

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()解锁过程

线程解锁的三种情况:

第一种情况,若当前线程不在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,解锁成功,但是内部逻辑是不同的。

两种重要问题:

问题1:为什么需要同步队列AQS?
回答1:同步队列是lock公平锁的内部数据结构,公平锁的唤醒一定是同步队列中的下一个元素,非公平锁不需要同步队列。

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

五、尾声

加锁与解锁,同步队列之精彩,完成了。

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

  Java知识库 最新文章
计算距离春节还有多长时间
系统开发系列 之WebService(spring框架+ma
springBoot+Cache(自定义有效时间配置)
SpringBoot整合mybatis实现增删改查、分页查
spring教程
SpringBoot+Vue实现美食交流网站的设计与实
虚拟机内存结构以及虚拟机中销毁和新建对象
SpringMVC---原理
小李同学: Java如何按多个字段分组
打印票据--java
上一篇文章      下一篇文章      查看所有文章
加:2021-11-18 11:04:23  更:2021-11-18 11:04:57 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/24 3:07:06-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码