闲聊AQS面试和源码解读---可重入锁、LockSupport、CAS;从ReentrantLock源码来看公平锁与非公平锁、AQS到底是怎么用CLH队列来排队的?

news/2024/5/20 9:11:34 标签: java, 后端, 源码, juc, 并发

AQS原理可谓是JUC面试中的重灾区之一,今天我们就来一起看看AQS到底是什么?
这里我先整理了一些JUC面试最常问的问题?
1、Synchronized 相关问题以及可重入锁 ReentrantLock及其他显式锁相关问题

1、 Synchronized 用过吗,其原理是什么?

2、你刚才提到获取对象的锁,这个“锁”到底是什么?如何确定对象的锁

3、什么是可重入性,为什么说Synchronized 是可重入锁?

4、JVM对Java的原生锁做了哪些优化?

5、为什么说Synchronized是非公平锁?

6、什么是锁消除和锁粗化?

7、为什么说Synchronized是一个悲观锁?乐观锁的实现原理又是什么?什么是CAS,它有什么优点和缺点?

8、乐观锁一定就是好的吗?

9、跟Synchronized相比,可重入锁ReentrantLock 其实现原理有什么不同?

10、那么请谈谈AQS框架是怎么回事儿?

11、请尽可能详尽地对比下Synchronized 和ReentrantLock的异同。

12、ReentrantLock 是如何实现可重入性的?

在正式开始AQS之前,我们需要先了解下课重入锁和LockSupport的原理。开始淦!

一、可重入锁

1、可重入锁的概念

可重入锁又名 递归锁, 是指在同一个线程在外层方法获取锁的时候,再进入该线程的内层方法会自动获取锁(前提,锁对象得是同一个对象),不会因为之前已经获取过还没释放而阻塞。从字面意思来看,可重入也就是可以再次进入同步锁。

Java中ReentrantLock和synchronized都是可重入锁,可重入锁的一个优点是可一定程度避免死锁。一个线程中的多个流程可以获取同一把锁,持有这把同步锁可以再次进入。即自己可以获取自己的内部锁。

2、可重入锁的种类

隐式锁: Synchronized关键字使用的锁就是隐式锁,其默认是可重入锁。

1、同步代码块

代码演示:

java">package AQS.可重入锁.同步代码块;

/**
 * @program: juc
 * @description 可重入锁:可重复可递归调用的锁,在外层使用锁之后,在内层仍然可以使用,并且不发生死锁,这样的锁就叫做可重入锁。
 *  * 在一个synchronized修饰的方法或代码块的内部调用本类的其他synchronized修饰的方法或代码块时,是永远可以得到锁的
 * @author: 不会编程的派大星
 * @create: 2021-08-14 15:29
 **/
public class ReEnterLockDemo1 {

    static Object ObjectLock = new Object();

    public static void main(String[] args) {
        new Thread(() -> {
            synchronized (ObjectLock){
                System.out.println(Thread.currentThread().getName() +"---外层调用");
                synchronized (ObjectLock){
                    System.out.println(Thread.currentThread().getName()+"---中层调用");
                    synchronized (ObjectLock){
                        System.out.println(Thread.currentThread().getName()+"---内层调用");
                    }
                }
            }
        },"t1").start();
    }
}

演示结果:
在这里插入图片描述
可以看到哈,在同一个线程内成功获取通一把锁。

2、同步方法

代码演示:

java">
/**
 * @program: juc
 * @description可重入锁:可重复可递归调用的锁,在外层使用锁之后,在内层仍然可以使用,并且不发生死锁,这样的锁就叫做可重入锁。
 *  * 在一个synchronized修饰的方法或代码块的内部调用本类的其他synchronized修饰的方法或代码块时,是永远可以得到锁的
 * @author: 不会编程的派大星
 * @create: 2021-08-15 10:55
 **/
public class ReEnterLockDemo2 {

    public synchronized void m1(){
        System.out.println("---外层---");
        m2();
    }

    private synchronized void m2() {
        System.out.println("---中层---");
        m3();
    }

    private synchronized void m3() {
        System.out.println("---内层---");
    }

    public static void main(String[] args) {
        new ReEnterLockDemo2().m1();
    }
}

演示结果:
在这里插入图片描述

3、Synchronized锁原理

1、 使用 javap -c xxx.class 指令反编译字节码文件,可以看到有一对配对出现的 monitorenter 和 monitorexit 指令,一个对应于加锁,一个对应于解锁

如下图所示:
在这里插入图片描述
2、 从字节码文件中,可以看到有两个monitorexit指令,为什么会多出来一个monitorexit指令呢?
如果同步代码块中出现exception或者error的情况,则会调用第二个monitorexit指令俩保证锁的释放。
每个锁对象拥有一个锁计数器和一个指向持有该锁的线程的指针。
当执行monitorenter时,如果目标锋对象的计数器为零,那么说明它没有被其他线程所持有,Java虚拟机会将该锁对象的持有线程设置为当前线程,并且将其计数器加1。

在目标锁对象的计数器不为零的情况下,如果锁对象的持有线程是当前线程,那么Java虚拟机可以将其计数器加1,否则需要等待,直至持有线程释放该锁。

当执行monitorexit时,Java虚拟机则需将锁对象的计数器减1。计数器为零代表锁已被释放。

显式锁(LOCK):

在显式锁中,也有ReentrantLock这样的可重入锁。

1、代码展示:

java">import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/**
 * @program: juc
 * @description
 * @author: 不会编程的派大星
 * @create: 2021-08-16 15:51
 **/
public class ReentrantLockDemo1 {

    static Lock lock = new ReentrantLock();

    public static void main(String[] args) {
        new Thread(() -> {
            lock.lock();
            try {
                System.out.println("---外层---");
                lock.lock();
                try {
                    System.out.println("---内层---");
                } catch (Exception e) {
                    e.printStackTrace();
                } finally {
                    lock.unlock();
                }
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        },"t1").start();
    }
}

演示结果:
在这里插入图片描述
在同一个线程内,内部成功获取到通一把锁。**一定要注意!!!加锁几次就要解锁几次!**否则另外一个线程就拿不到该锁! 接下来我们再来看看AQS的另一大前置知识点—LockSupport!

二、LockSupport

1、LockSupport的概念?

LockSupport是用来创建锁和其他同步类的基本线程阻塞原语。LockSupport中的park()和unpark()的作用分别是阻塞线程和解除阻塞线程,可以将其看作是线程等待唤醒机制(wait/notify)的加强版。

2、三种可以让线程等待和唤醒的方法

1: 使用Object中的wait()方法让线程等待, 使用Object中的notify()方法唤醒线程

注:wait和notify方法必须要在同步块或者方法里面且成对出现使用,先wait后notify才OK。

2: 使用JUC包中Condition的await()方法让线程等待,使用signal()方法唤醒线程

注:传统的synchronized和Lock实现等待唤醒通知的约束:线程先要获得并持有锁,必须在锁块(synchronized或lock)中,必须要先等待后唤醒,等待后线程才能够被唤醒。

3: LockSupport类可以阻塞当前线程以及唤醒指定被阻塞的线程

LockSupport 类使用了一种名为 permit(许可)的概念来做到阻塞和唤醒线程的功能,每个线程都有一个许可(permit),permit 只有两个值 1 和零,默认是零。

*LockSupport类主要方法
在这里插入图片描述
*阻塞
park()/park(Object blocker)

park() 方法的作用:阻塞当前线程/阻塞传入的具体线程

permit 默认是 0,所以一开始调用 park() 方法,当前线程就会阻塞,直到别的线程将当前线程的 permit 设置为 1 时,park() 方法会被唤醒,然后会将 permit 再次设置为 0 并返回。

park() 方法通过 Unsafe 类实现

java">// Disables the current thread for thread scheduling purposes unless the permit is available.
public static void park() {
    UNSAFE.park(false, 0L);
}

*唤醒
unpark(Thread thread)

unpark() 方法的作用:唤醒处于阻断状态的指定线程

调用 unpark(thread) 方法后,就会将 thread 线程的许可 permit 设置成 1(注意多次调用 unpark()方法,不会累加,permit 值还是 1),这会自动唤醒 thread 线程,即之前阻塞中的LockSupport.park()方法会立即返回。

unpark() 方法通过 Unsafe 类实现

java">// Makes available the permit for the given thread
public static void unpark(Thread thread) {
    if (thread != null)
        UNSAFE.unpark(thread);
}

*LockSupport代码示例

代码演示:

java">import java.util.concurrent.locks.LockSupport;

/**
 * @program: juc
 * @description
 * @author: 不会编程的派大星
 * @create: 2021-08-17 14:52
 **/
public class LockSupportDemo1 {

    public static void main(String[] args) {
         Thread a = new Thread(() -> {
            System.out.println(Thread.currentThread().getName()+"---进来了---");
            //线程A阻塞
            LockSupport.park();
            System.out.println(Thread.currentThread().getName()+"---我被唤醒了---");
        },"A");
         a.start();


        new Thread(() -> {
          LockSupport.unpark(a);
            System.out.println(Thread.currentThread().getName()+"唤醒,通知A");
        },"B").start();
    }
}

演示结果:
在这里插入图片描述
A线程先执行park()方法将通行证permit设置为0,这里其实无影响,因为permit初始值本来就为0,然后B线程呢在执行unpark(a)方法再将permit设置为1,这个时候呢,A线程就被唤醒了,就正常执行。

在上面哈,我们说过synchronized和lock都需要先等待在唤醒,否则线程就会一直处于等待状态,那在LockSupport中要是先unpark()再park()呢?

代码演示:

java">import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.LockSupport;

/**
 * @program: juc
 * @description
 * @author: 不会编程的派大星
 * @create: 2021-08-17 15:41
 **/
public class LockSupportDemo2 {

    public static void main(String[] args) {

        Thread a = new Thread(() -> {
            try {
                TimeUnit.SECONDS.sleep(3);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()+"---进来了---");
            //线程A阻塞
            LockSupport.park();
            System.out.println(Thread.currentThread().getName()+"---我被唤醒了---");
        });
        a.start();

        new Thread(() -> {
            LockSupport.unpark(a);
            System.out.println(Thread.currentThread().getName()+"唤醒,通知A");
        },"B").start();
    }
}

演示结果:
在这里插入图片描述
因为有太通行证的存在,所以先执行unpark()方法并没有什么影响,因为permit默认为0,执行unpark()后permit为1,执行park()时,park()就可以光明正大的消费掉这个1,所以不会造成A线程阻塞。

在上面我们也说到了一个问题,那就是permit的上限为1,我们来想想假如没有考虑到permit上限值为1的情况呐?

代码演示:

java">import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.LockSupport;

/**
 * @program: juc
 * @description
 * @author: 不会编程的派大星
 * @create: 2021-08-17 14:52
 **/
public class LockSupportDemo3 {

    public static void main(String[] args) {
        try {
            TimeUnit.SECONDS.sleep(3);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
         Thread a = new Thread(() -> {
            System.out.println(Thread.currentThread().getName()+"---进来了---");
            //线程A阻塞
            LockSupport.park();

            LockSupport.park();
            System.out.println(Thread.currentThread().getName()+"---我被唤醒了---");
        },"A");
         a.start();


        new Thread(() -> {
          LockSupport.unpark(a);
          LockSupport.unpark(a);
            System.out.println(Thread.currentThread().getName()+"唤醒,通知A");
        },"B").start();
    }
}

演示结果:
在这里插入图片描述
可以看到哈,即使有两次unpark(),但是permit的上限为1,A线程两个park()只能消费一次,还有一个park()就会造成A线程阻塞。

*LockSupport小的总结
以前的等待唤醒通知机制必须synchronized里面执行wait和notify,在lock里面执行await和signal,这上面这两个都必须要持有锁才能正常使用。

LockSupport:俗称锁中断,LockSupport 解决了 synchronized 和 lock 的痛点, 1、 LockSupport不用持有锁块,不用加锁,程序性能好,无须注意唤醒和阻塞的先后顺序,不容易导致卡死。LockSupport和每个使用它的线程都有一个许可(permit)关联。permit相当于1,0的开关,默认是0,调用一次unpark就加1变成1,调用一次park会消费permit,也就是将1变成0,同时park立即返回。

2、 如再次调用park会变成阻塞(因为permit为零了会阻塞在这里,一直到permit变为1),这时调用unpark会把permit置为1。

3、 每个线程都有一个相关的permit,permit最多只有一个,重复调用unpark也不会积累凭证。

4、 当调用park方法时,如果有凭证,则会直接消耗掉这个凭证然后正常退出;如果无凭证,就必须阻塞等待凭证可用;而unpark则相反,它会增加一个凭证,但凭证最多只能有1个,累加无效。

三、AbstractQueuedSynchronizer — AQS

接下来,终于进入到主题AQS,AQS,即抽象的队列同步器。

1、AQS介绍以及概念

一般我们说的 AQS 就是 java.util.concurrent.locks 包下的AbstractQueuedSynchronizer
;AQS 是用来构建锁或者其它同步器组件的重量级基础框架及整个JUC体系的基石, 通过内置的FIFO队列来完成资源获取线程的排队工作,并通过一个int类变量(state)表示持有锁的状态;CLH:Craig、Landin and Hagersten 队列,是一个双向链表,AQS中的队列是CLH变体的虚拟双向队列FIFO,如下图所示:
在这里插入图片描述
AQS继承关系:
在这里插入图片描述

2、AQS—JUC的基石

我们来看看一张图片,就知道为什么AQS是JUC的基石了
在这里插入图片描述

我们在JUC见到的很多类都是基于AQS搭建的。例如ReentrantLock、CountDownLatch、ReentrantReadWriteLock、Semaphore等等许多。

通过这些关系,我们也可以更好的理解锁和同步器的关系:

锁, 面向锁的使用者。定义了程序员和锁交互的使用层API,隐藏了实现细节,你调用即可,可以理解为用户层面的 API。

同步器, 面向锁的实现者。比如Java并发的开发者,提出统一规范并简化了锁的实现,屏蔽了同步状态管理、阻塞线程排队和通知、唤醒机制等,Java 中有那么多的锁,就能简化锁的实现啦。

3、AQS能干嘛?
看下面这段话最好是结合着下面的图一起来理解和认识0.0!

加锁会导致阻塞,有阻塞就需要排队,实现排队必然需要有某种形式的队列来进行管理;

抢到资源的线程直接使用办理业务,抢占不到资源的线程的必然涉及一种排队等候机制,抢占资源失败的线程继续去等待(类似办理窗口都满了,暂时没有受理窗口的顾客只能去候客区排队等候),仍然保留获取锁的可能且获取锁流程仍在继续(候客区的顾客也在等着叫号,轮到了再去受理窗口办理业务)。

既然说到了排队等候机制,那么就一定 会有某种队列形成,这样的队列是什么数据结构呢?如果共享资源被占用,就需要一定的阻塞等待唤醒机制来保证锁分配。这个机制主要用的是CLH队列的变体实现的,将暂时获取不到锁的线程加入到队列中,这个队列就是AQS的抽象表现。它将请求共享资源的线程封装成队列的结点(Node) ,通过CAS、自旋以及LockSuport.park()的方式,维护state变量的状态,使并发达到同步的效果。
在这里插入图片描述

再从另一个角度来看看,有阻塞就需要排队,实现排队必然需要队列:

1、 AQS使用一个volatile的int类型的成员变量来表示同步状态,通过内置的 FIFO队列来完成资源获取的排队工作将每条要去抢占资源的线程封装成 一个Node节点来实现锁的分配,通过CAS完成对State值的修改。

2、 Node 节点是啥?答:你有见过 HashMap 的 Node 节点吗?JDK 用 static class Node<K,V> implements Map.Entry<K,V> { 来封装我们传入的 KV 键值对。这里也是一样的道理,JDK 使用 Node 来封装(管理)Thread

3、 可以将 Node 和 Thread 类比于候客区的椅子和等待用餐的顾客
在这里插入图片描述
我们来看看几个重要的成员变量

AQS的int变量state

AQS的同步状态State成员变量,类似于银行办理业务的受理窗口状态:零就是没人,自由状态可以办理;大于等于1,有人占用窗口,等着去。

AQS的CLH队列

为一个双向队列,类似于银行侯客区的等待顾客,通过自旋等待,state变量判断其是否阻塞,从尾部入队从头部出队

AQS内部类Node

Node的等待状态waitState成员变量,类似于等候区其它顾客(其它线程)的等待状态,队列中每个排队的个体就是一个Node,我们来看看内部结构
在这里插入图片描述

小总结
有阻塞就需要排队,实现排队必然需要队列,通过state 变量 + CLH双端 Node 队列实现,cas来控制变量state。

4、AQS到底是怎么排队的呢

排队的话就是使用我们上面所讲到的LockSupport.park()方法。

四、从ReentrantLock源码来剖析AQS、公平锁与非公平锁

1、ReentrantLock的原理

ReentrantLock 实现了 Lock 接口,在 ReentrantLock 内部聚合了一个 AbstractQueuedSynchronizer 的实现类,如下图所示:
在这里插入图片描述
2、公平锁和非公平锁

接下来就让我们通过ReentrantLock的源码来分析下公平锁与非公平锁。

在 ReentrantLock 内定义了静态内部类,分别为 NoFairSync(非公平锁)和 FairSync(公平锁)
在这里插入图片描述
ReentrantLock 的构造函数: 不传参数表示创建非公平锁;参数为 true 表示创建公平锁;参数为 false 表示创建非公平锁

我们来大概看一下lock()方法的执行过程
1
在这里插入图片描述
2
在这里插入图片描述

3
在这里插入图片描述4

在这里插入图片描述
非公平锁:
在这里插入图片描述
公平锁:
在这里插入图片描述

在 ReentrantLock 中,NoFairSync 和 FairSync 中 tryAcquire() 方法的区别,可以明显看出公平锁与非公平锁的lock()方法唯一的区别就在于公平锁在获取同步状态时多了一个限制条件:
hasQueuedPredecessors():是公平锁加锁时判断等待队列中是否存在有效节点的方法,如下图所示:
在这里插入图片描述

小总结

对比公平锁和非公平锁的tryAcqure()方法的实现代码, 其实差别就在于非公平锁获取锁时比公平锁中少了一个判断!hasQueuedPredecessors(),hasQueuedPredecessors()中判断了是否需要排队

主要导致的差异:

1、公平锁: 公平锁讲究先来先到,线程在获取锁时,如果这个锁的等待队列中已经有线程在等待,那么当前线程就会进入等待队列中;

2、非公平锁: 不管是否有等待队列,如果可以获取锁,则立刻占有锁对象。也就是说队列的第一 个排队线程在unpark(),之后还是需要竞争锁(存在线程竞争的情况下)

在这里插入图片描述
3、从非公平锁的lock()方法源码入手

演示案例说明: 假设 A、B、C 三个人都要去银行窗口办理业务,但是银行窗口只有一个个,带入一个银行办理业务的案例来模拟我们的AQS如何进行线程的管理和通知唤醒机制, 3个线程模拟3个来银行网点,受理窗口办理业务的顾客, A顾客就是第一个顾客,此时受理窗口没有任何人,A可以直接去办理;第二个顾客,第二个线程,由于受理业务的窗口只有一个(只能一个线程持有锁),此时B只能等待, 进入候客区;第三个顾客,第三个线程,由于受理业务的窗口只有一个(只能一个线程持有锁),此时C只能等待,。进入候客区

演示代码:

java">import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/**
 * @program: juc
 * @description
 * @author: 不会编程的派大星
 * @create: 2021-08-17 16:58
 **/
public class AQSDemo1 {

    public static void main(String[] args) {

        Lock lock = new ReentrantLock();

        new Thread(() -> {
            lock.lock();
            try {
                System.out.println("A该去窗口办理业务了");

                try {
                    TimeUnit.SECONDS.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            } finally {
                lock.unlock();
            }
        },"A").start();

        new Thread(() -> {
            lock.lock();
            try {
                System.out.println("B该去窗口办理业务了");
            } finally {
                lock.unlock();
            }
        },"B").start();

        new Thread(() -> {
            lock.lock();
            try {
                System.out.println("C该去窗口办理业务了");
            } finally {
                lock.unlock();
            }
        },"C").start();

    }
}

我们来分析下这段代码在业务中的执行过程:
首先这里我们在创建锁时,是使用的默认构造函数,即创建的是非公平锁。

在开始之前 我们再来回忆下之前学的CAS原理,通过 Unsafe 提供的 compareAndSwapXxx() 方法保证修改操作的原子性(通过 CPU 原语保证),如果变量的值等于期望值,则修改变量的值为 update,并返回 true;若不等,则返回 false。this 代表当前对象,stateOffset 表示 state 变量在该对象中的偏移量
详情可以参考之前写过的一片文章
准备好了吗?带你读底层!深入理解CAS ,从juc原子类一路追溯到unsafe类再到ABA原子引用!

第一次执行lock()方法
第一次执行lock()方法时,state的变量值为0,这是就说明该lock锁还没有被占用,此时执行cas(0,1)判断,成功后,将state的值改为1
在这里插入图片描述
这里setExclusiveOwnerThread() 方法是将拥有该lock锁的线程修改为线程A

第二次执行lock()方法–线程B

由于第二次执行 lock() 方法,state 变量的值等于 1,表示 lock 锁已经被占用,此时执行 compareAndSetState(0, 1) CAS 判断,可得 state != expected,因此 CAS 失败,进入 acquire() 方法
在这里插入图片描述
接下俩我们再来分析下acquire() 方法下的几个方法

tryAcquire(arg) 方法的执行流程

子类中具体的实现:
在这里插入图片描述
在这里插入图片描述
这里线程B执行,getState()后state为1,说明lock锁已经被占用,(C==0这里的代码块大家应该都明白,cas,和上面一样),进入到下一个判断条件else if (current == getExclusiveOwnerThread()) ,这里current是当前线程,而getExclusiveOwnerThread是lock所占用的线程(排他锁,exclusive),所以不满足条件,最后返回false,表示没有抢占到lock锁。

这里在补充两个可能遇到的特殊情况:

1、 第一种情况是,走到 int c = getState() 语句时,此时线程 A 恰好执行完成,让出了 lock 锁,那么 state 变量的值为 0,当然发生这种情况的概率很小,那么线程 B 执行 CAS 操作成功后,将占用 lock 锁的线程修改为自己,然后返回 true,也就是第一个判断条件,表示抢占锁成功。其实这里还有一种情况,到下面 unlock() 方法我们在细谈。

2、 第二种情况为可重入锁的表现,假设 A 线程又再次抢占 lock 锁(当然是上面的示例代码里面并没有体现出来,但其他地方可能会遇到),这时 current == getExclusiveOwnerThread() 条件成立,将 state 变量的值加上 acquire,这种情况下也应该 return true,表示线程 A 正在占用 lock 锁。因此,state 变量的值是可以大于 1 的,也就是第二个判断条件

好!我们继续往下走
在 tryAcquire() 方法返回 false 之后,进行 ! 操作后为 true,那么会继续执行 addWaiter() 方法

ddWaiter(Node.EXCLUSIVE)方法

我们在之前就说过哈,Node 节点用于封装用户线程,这里将当前正在执行的线程通过 Node 封装起来(当前线程正是抢占 lock 锁没有抢占到的线程)来看看源码
在这里插入图片描述
在构建完node后,判断tail尾指针是否为空,双端队列此时肯定还没元素,那么执行enq(node) 方法,将封装了线程B的Node节点入队。

enq(node) 方法:构建双端同步队列

开始看enq源码前,我们先来说说双端队列,双端队列的第一个节点被称为虚节点(哨兵节点),其实并不存储任何信息,也就是占着茅坑不拉屎,真正第一个有数据的节点,是从第二个开始的,好,接下来让我们来看看enq(node) 方法:
在这里插入图片描述

我们来一起分析下这个过程:

第一次for循环,这里相当于一个指针指向尾指针,此时肯定为空,然后就创建一个哨兵节点,此时队列就只有这一个哨兵节点,既是头结点,也是尾结点。如下图所示:
在这里插入图片描述
在来看看第二次for循环,现在也就是将封装了B线程的节点node放入到双端队列中,这里尾结点并不是为空,所以进入到else分支中,这里用到的是尾插法,先将封装了B线程的node的prev前缀指向之前的tail,再利用CAS将新的node节点设置为新的尾结点,再将t的next后缀指向node,最后返回t所指向的节点结束循环,如下图所示:
在这里插入图片描述

这里补充一下设置尾结点的方法实现,也就是cas的应用:
在这里插入图片描述
最后呀,注意一点,哨兵节点和waitStatus均为0,表示正在阻塞队列中。

再来看看C线程的执行的执行过程,和B线程整体相差不大,有一丢丢区别,因为tail尾结点已经不为空了,所以在addWaiter() 方法中就已经将封装了C线程的node节点添加到队尾了,就不需要在执行enq()方法了。
在这里插入图片描述
执行完addWaiter() 方法后,下一步就是acquireQueued() 方法了。

acquireQueued() 方法的执行逻辑

我们来看看acquireQueued() 方法的源码,两个if判断都是放在for循环当中的。其实这个就相当于自旋锁,实现了自旋的操作

.
我们就以B线程为例,看看它执行完addWaiter()方法之后是怎么执行acquireQueued() 放的。首先,传入的参数为封装了B线程的node节点,我们也都知道,Node(B线程)的前驱节点为哨兵节点,所以p指向的就是哨兵节点,在第一个if条件中,p == head条件是满足的,但是tryAcquire(arg)方法抢占lock锁还是会返回false,所以会执行下面的shouldParkAfterFailedAcquire(p, node)方法,我们来看看其源码
在这里插入图片描述
哨兵节点的waitStatus == 0,因此CAS操作会将哨兵节点的waitStatus改为Node.SIGNAL,其值为-1.

这里说一个注意点:compareAndSetWaitStatus(pred, ws, Node.SIGNAL) 调用 unsafe.compareAndSwapInt(node, waitStatusOffset, expect, update); 实现,虽然 compareAndSwapInt() 方法内无自旋,但是在 acquireQueued() 方法中的 for( ; ; ) 能保证此自选操作成功

上诉方法完成后,哨兵节点的waitStatus就为-1了,如下图所示:

在这里插入图片描述
同时,该if判断条件下,还会执行一个parkAndCheckInterrupt() 方法
在这里插入图片描述

线程B之类就调用LockSupport.park()方法后就阻塞了,该线程就不会继续向下执行,程序就在这儿排队等待。

线程C和线程B类似,最后也会执行到LockSupport.park()这里就阻塞,然后进入等待区。

小总结

如果前驱节点的 waitstatus 是 SIGNAL 状态(-1),即 shouldParkAfterFailedAcquire() 方法会返回 true,程序会继续向下执行 parkAndCheckInterrupt() 方法,用于将当前线程挂起
根据 park() 方法 API 描述,程序在下面三种情况会继续向下执行:

1、 被 unpark
2、 被中断(interrupt)
3、 其他不合逻辑的返回才会然续向下执行

unlock方法来了

我们来看看线程A在unlock()时都发生了什么

在这里插入图片描述
在这里插入图片描述
tryRelease(arg) 方法的执行逻辑
在这里插入图片描述
线程 A 只加锁过一次,因此 state 的值为 1,参数 release 的值也为 1,因此 c == 0。将 free 设置为 true,表示当前 lock 锁已被释放,将排他锁占有的线程设置为 null,表示没有任何线程占用 lock 锁,再来看看unparkSuccessor(h) 方法

unparkSuccessor(h) 方法的执行逻辑

在这里插入图片描述
在 release() 方法中获取到的头结点 h 为哨兵节点,h.waitStatus == -1,所以waitStatus的值为-1,因此执行 CAS操作将哨兵节点的 waitStatus 设置为 0,并获取哨兵节点的下一个节点Node(线程B),并且LockSupport.unpark()唤醒线程B.

此时线程B在再回到lock的执行流程中来,也就是最后执行LockSupport.park()方法
在这里插入图片描述
回到上一层acquireQueued()方法中,此时 lock 锁未被占用,线程 B 执行 tryAcquire(arg) 方法能够抢到 lock 锁,并且将 state 变量的值设置为 1,表示该 lock 锁已经被占用

setHead()方法
在这里插入图片描述
传入的节点为 Node(线程B),头指针指向 nodeB 节点;将 nodeB 中封装的线程置为 null(因为已经获得锁了);nodeB 不再指向其前驱节点(哨兵节点)。这一切都是为了将 nodeB 作为新的哨兵节点,如下图所示:
在这里插入图片描述
将 p.next 设置为 null,这是原来的哨兵节点就是完全孤立的一个节点,此时 nodeB 作为新的哨兵节点
在这里插入图片描述
到这里,相信大家都看湿了吧,爽的通透了吧,终于淦完了!!!

最后我们再来一个小总结、

总结

AQS利用CAS原子操作维护自身的状态,结合LockSupport对线程进行阻塞和唤醒从而实现更为灵活的同步操作。

AQS主要的脉络就是:1、通过CAS操作维护自身的状态 2、一个就是如何对线程的进行处理

完结撒花!!!欢迎小伙伴们提出疑问,留言讨论!!!

我们下期见!


http://www.niftyadmin.cn/n/1715567.html

相关文章

java amqp_RabbitMQ 的消息持久化与 Spring AMQP 的实现详解

前言要从奔溃的 RabbitMQ 中恢复的消息&#xff0c;我们需要做消息持久化。如果消息要从 RabbitMQ 奔溃中恢复&#xff0c;那么必须满足三点&#xff0c;且三者缺一不可。交换器必须是持久化。队列必须是持久化的。消息必须是持久化的。原生的实现方式原生的 RabbitMQ 客户端需…

Docker是怎么工作的?

Docker是一个Client-Server结构的系统&#xff0c;Docker的守护进程运行在主机上&#xff0c;通过Socket从客户端访问&#xff01;DockerServer接受到DockerClient的指令&#xff0c;就会执行这个命令&#xff01; 如下图所示&#xff1a; 我们在使用虚拟机和docker的时候&…

serv u使用mysql数据库_使用ODBC数据库管理Serv-U的FTP用户及相关ASP编程[附源码示例下载]...

使用ODBC数据库管理Serv-U的FTP用户及相关ASP编程[附源码示例下载]Serv-U是一种被广泛运用的FTP服务器端软件&#xff0c;支持3x/9x/ME/NT/2K等全Windows系列。可以设定多个FTP服务器、限定登录用户的权限、登录主目录及空间大小等&#xff0c;功能非常完备。 它具有非常完备的…

Docker常用的基本命令

1、帮助命令 帮助文档&#xff1a; docker常用命令帮助文档 2、镜像命令 3、容器的命令 注意哈&#xff0c;我们有了镜像才可以创建容器&#xff01; 4、常用的其他命令 exec和attach区别 从容器内复制文件到当前主机 常用命令总结 今天就到这里&#xff0c;欢…

Docker之提交一个镜像以及容器数据卷的使用

1、commit镜像 例如&#xff1a; 2、容器数据卷 3、mysql部署实战

java 访问项目中的附件_java获取项目中附件的属性

java根据路径获取项目中上传附件的属性/*** 根据文件路径获取文件属性* param path* return*/RequestMapping("/getFileProp")ResponseBodypublic List getFileProp(String path,HttpServletRequest request){// String basePath request.getScheme() "…

LRU缓存机制的两种实现:LInkedHashMap实现、自己构建双链表+HashMap实现

问题描述&#xff1a;运用你所掌握的数据结构&#xff0c;设计和实现一个 LRU (最近最少使用) 缓存机制 。 实现 LRUCache 类&#xff1a; LRUCache(int capacity) 以正整数作为容量 capacity 初始化 LRU 缓存 int get(int key) 如果关键字 key 存在于缓存中&#xff0c;则返回…

JAVA中try、catch、finally带return的执行顺序总结

异常处理中&#xff0c;try、catch、finally的执行顺序&#xff0c;大家都知道是按顺序执行的。即&#xff0c;如果try中没有异常&#xff0c;则顺序为try→finally&#xff0c;如果try中有异常&#xff0c;则顺序为try→catch→finally。但是当try、catch、finally中加入retur…