AQS底层源码深度剖析-Lock锁

news2024/11/13 10:16:58

目录

AQS底层源码深度剖析-Lock锁

ReentrantLock底层原理

 为什么把获取锁失败的线程加入到阻塞队列中,而不是采取其它方法?

总结:三大核心原理

CAS是啥?

代码模拟一个CAS:

 公平锁与非公平锁

可重入锁的应用场景:

AQS底层源码分析[重点,难点]

类图展示:

AQS类属性分析:

AQS类中Node内部节点类:

一:获取锁 lock()

概念:

源码深度剖析:

二:释放锁 unlock()

概念:

源码深度剖析:

三:中断interrupt() 与LockSupport.park(),LockSupport.park(Object obj)的关系【重点】

首先要明白以下三个API:

其次要明白:

提出问题:为什么需要中断方法interrupt()的调用?

代码示例1:原生park()和unpark()的使用

代码示例2:原生park()和unpark() 结合 interrupt相关的API 的使用

代码示例三:在代码示例二的基础上加上一个interrupted方法

重点: LockSupport.park(Object obj)结合AQS底层源码剖析:

得出结论:

四:lockInterruptibly()深度剖析

概念:

源码深度剖析:

总结:lock()与lockInterruptibly()里面中断(interrupt)信号的实际应用场景【重点】

lock():

lockInterruptibly():



 AQS底层源码深度剖析-Lock锁

ReentrantLock底层原理

 为什么把获取锁失败的线程加入到阻塞队列中,而不是采取其它方法?

1.sleep休眠不可取:因为获取锁的线程,执行业务多久是不确定的,所以获取锁失败的线程的休眠时间是不确定的。

2.让出CPU资源不可取:让给谁?

3.直接挂起不可取:直接挂起,等于说性能相当于重量级锁,把对未获取锁线程的管理的任务交给了操作系统,操作系统需要进行切换状态(从用户态切换回内核态),是极其耗费性能的。

所以,我们把未获取锁成功的线程加入到一个阻塞队列中,等到获取锁的线程执行完任务后可以去唤醒这些阻塞的线程。

总结:三大核心原理

自旋,LockSuport,CAS(高并发也可以保证原子性)

【 queue队列存储进入的线程,实现公平锁机制。】

分析:自旋就是一直while循环尝试获取锁。

LockSupport就是阻塞线程与唤醒线程,阻塞是为了降低自旋,以此降低CPU所消耗的性能。

CAS就是在多线程同时操作时,保证操作的原子性,同时一刻只能有一个线程修改主内存的数据成功。

CAS是啥?

代码模拟一个CAS:

/**
 * @Description: TODO
 * @Author: etcEriksen
 * @Date: 2023/3/5
 **/
@SuppressWarnings({"all"})
@Slf4j
public class Juc04_Thread_Cas {

    /**
     * state标识当前加锁状态,记录加锁的次数
     * 默认加锁次数为0
     */
    private volatile int state = 0 ;

    private static CyclicBarrier cyclicBarrier = new CyclicBarrier(5) ;

    private static Juc04_Thread_Cas cas = new Juc04_Thread_Cas() ;

    static class Worker implements Runnable {

        @Override
        public void run() {
            log.info("请求:{}到达预定点,准备开始抢state:)",Thread.currentThread().getName());
            try {
                //阻塞所有的线程到这里,然后一起执行后面的逻辑操作
                cyclicBarrier.await();
                if (cas.compareAndSwapState(0,1)) {
                    log.info("当前请求:{},抢到锁!",Thread.currentThread().getName());
                }else{
                    log.info("当前请求:{},抢锁失败!",Thread.currentThread().getName());
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }


    }

    public final boolean compareAndSwapState(int oldValue,int newValue) {
        /**
         * 比较stateOffset的值和oldValue值是否一致,若不一致,那么不可修改 。如果一致,那么修改值为:newValue。
         */
        return unsafe.compareAndSwapInt(this,stateOffset,oldValue,newValue);
    }

    private static final Unsafe unsafe = UnsafeInstance.reflectGetUnsafe();

    private static final long stateOffset;

    static {
        try {
            stateOffset = unsafe.objectFieldOffset(Juc04_Thread_Cas.class.getDeclaredField("state"));
        } catch (Exception e) {
            throw new Error();
        }
    }
}
public class UnsafeInstance {
    public static Unsafe reflectGetUnsafe() {
        try {
            Field field = Unsafe.class.getDeclaredField("theUnsafe");
            field.setAccessible(true);
            return (Unsafe) field.get(null);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

    public static void main(String[] args) {
        int j=1;
        reflectGetUnsafe().loadFence();
        int i= 0;
    }
}

 公平锁与非公平锁

t-0正在CAS执行任务逻辑,当执行完的那一刻,t-4正好进入了,那么如果是非公平锁,那么t-4不用到阻塞队列的最后去排队,而是直接去抢t-0线程执行完任务后进行释放的锁对象。如果是公平锁,那么t-4就会进入阻塞队列,老老实实的排到队尾等待。

可重入锁的应用场景:

AQS底层源码分析[重点,难点]

类图展示:

AQS类属性分析:

注释:state为锁的状态器。eg:无锁状态时,state=0。同一线程每成功获取该锁一次,则state+1,同一线程每释放该锁对象一次,则state-1。

exclusiveOwnerThread: 表示当前持有锁对象的线程为谁

head和tail就是在维护一个双端队列,后面该队列用来存储获取锁失败的线程。

AQS类中Node内部节点类:

注释:prev和next:分别表示当前Node节点的前驱和后继节点

waitStatus:Node节点的生命状态,信号量。表示Node节点在不同生命状态下所呈现出的不同性质,有五种,如下:

       SIGNAL = -1 //可被唤醒
        CANCELLED = 1 //代表出现异常,中断引起的,需要废弃结束
        CONDITION = -2 // 条件等待
        PROPAGATE = -3 // 传播
        0 - 初始状态Init状态

thread:由于我们是使用多个Node节点组成一个双端阻塞队列,队列中的每一个元素都是一个Node,每一个Node对应维护一个线程对象,所以我们需要在Node节点类中维护一个Thread类属性:thread 。

一:获取锁 lock()

概念:

这种lock()获取锁对象的方式必须等持有锁对象的线程做完事情,其他等待的线程才可以做事情。而且中途不能退出,会一直阻塞存储在CLH双端队列中。

说详细点就是:当一个节点对象加入到CLH双端阻塞队列中后,在被持有锁对象唤醒之前,它中途就不能退出该队列,即使阻塞队列中节点对应的线程对象被中断,也不可以退出!那么对队列中节点对应线程的中断有啥作用呢?最后的总结lock()和lockInterruptibly()会举例细说

源码深度剖析:

1.

如果锁未被另一个线程持有,则获取锁并立即返回,将锁保持计数设置为 1。
如果当前线程已持有锁,则保留计数将递增 1,并且该方法会立即返回。
如果锁由另一个线程持有,则当前线程将出于线程调度目的而被禁用,并处于休眠状态,直到获取锁为止,此时锁保持计数设置为 1。

2.

 3.

4.

接下来会详细分析tryAcquire和acquireQueued这两个方法:

 tryAcquire方法解析:

tryAcquire方法中调用的hasQueuePredecessors方法的解析:

acquireQueued方法解析:

acquireQueued调用的addWaiter方法解析:

addWaiter调用的enq()方法解析:

 提出一个疑问和思考:

当队列初始化完毕时,为什么在addWaiter方法中已经做了插入新Node到队列中的操作,然而又在addWaiter调用的enq方法中又重复做了一次插入新Node节点到队列中的操作呢???

答案:因为并发多线程环境!

acquireQueued方法解析:

acquireQueued方法调用的shouldParkAfterFailedAcquire方法解析:

acquireQueued方法调用的parkAndCheckInterrupt方法解析:

为什么要把第一个真实节点的状态设置在虚拟头节点的waitStatus上?为什么不设计在自己节点所维护的waitStatus呢?

首先你应该明白一个点:我们是公平锁,所谓公平锁就是先来的先可以获取到锁对象,后来的只能在后面排队等。

所以我们构建出了一个有序的阻塞队列,后来的线程插在最后面,第一个真实节点(虚拟节点的下一个节点)就是下一次锁对象的持有者。

当锁对象释放后,每次都是把锁对象分配给第一个真实节点(即虚拟头节点的下一个节点),这就是保证了公平。

第一个真实节点获取到锁对象后,会转变为虚拟头节点(此时waitStatus==0),原来的虚拟头节点会被GC回收掉。此时只需把新虚拟节点的waitStatus改为-1即可,因为waitStatus维护的是Node节点的性质,所以最好维护在虚拟头节点。

为什么要费尽心思搞出一个waitStatus并且设置为-1?

因为waitStatus==-1就是代表一个Node节点的状态量,代表的是可以进行被唤醒,那么在释放锁的时候,我们只需进行检验该waitStatus是否为-1,为-1,那么才可以去释放锁 才可以去唤醒该线程对应的Node节点。

二:释放锁 unlock()

概念:

就是当前锁持有者对应的线程执行锁对象的释放并且通知唤醒阻塞队列中的第一个真实节点对应的线程对象(即虚拟头节点Head的下一个节点)。

在公平锁的实现下,释放的锁对象肯定是被排在阻塞队列中的第一个节点对应的线程获取。但是在非公平锁的实现下,释放的锁对象可能会被刚刚进入的线程竞争获取到,导致阻塞队列第一个节点对应的线程对象获取锁对象失败。

源码深度剖析:

1.

2.

3.release方法解析:

release方法调用的tryRelease方法解析:

release方法调用的unparkSuccessor方法解析:

unparkSuccessor方法执行唤醒线程的操作后,会唤醒加锁过程中调用的parkAndCheckInterrupt方法中阻塞的线程:

parkAndCheckInterrupt阻塞的线程被唤醒后,说明parkAndCheckInterrupt阻塞完毕 即执行完毕,那么再一次循环向上,如图分析:

为什么要在 unparkSuccessor 方法中把Head虚拟头节点的waitStatus从-1改为0??

其实也说的很清楚了。

重复一遍释放锁唤醒线程的逻辑:t1线程执行完业务后进行释放锁并且通知唤醒阻塞队列中阻塞着的t2线程,在唤醒之前,需要把阻塞队列虚拟头节点Head的waitStatus由-1改为0。

但是在非公平锁的情况下,可能阻塞队列的第一个Node对应的t2线程在获取锁的时候 有其它线程进入跟它竞争锁,可能t2线程获取锁失败。获取锁成功,那好说。

如果t2线程获取锁失败,t2线程会进行两次循环【第二次循环是逻辑关键,阻塞前最后一次尝试获取锁,若成功那么就无需阻塞啦,极大的节省性能】:

第一次循环:由于t2线程获取锁失败,那么执行的acquireQueued方法中执行调用tryAcquire方法返回false。t2线程会再一次进入到shouldParkAfterFailedAcquire方法中,由于Head的waitStatus被t1线程在释放锁的时候改为了0,所以shouldParkAfterFailedAcquire方法中会执行逻辑把waitStatus改为-1,并且返回false。

第二次循环:会在阻塞前再一次的进行尝试获取锁(tryAcquire),因为阻塞是重型操作,需要切换状态,所以阻塞之前再一次进行尝试获取锁(做最后一次尝试),如果获取锁还是失败,那么就进行阻塞,循环结束。

假设说,t1线程释放锁后,在唤醒t2之前,不把阻塞队列虚拟头节点Head的waitStatus由-1改为0。那么t2线程只会进行一次循环,那么就不可能完成"阻塞前最后一次tryAcquire尝试获取锁"的功能啦

三:中断interrupt() 与LockSupport.park()LockSupport.park(Object obj)的关系【重点】

对于第三点,我们依旧结合AQS底层源码进行分析,前面一(获取锁)和二(释放锁)这两大方面分别代表了AQS源码的逻辑主线,第三点算是对前面两点中涉及到interrupt中断源码的分析与补充。

首先要明白以下三个API:

1.Thread.currentThread().interrupt(),表示为当前线程打一个中断标记,中断当前线程。

2.Thread.interrupted(),表示清除中断标记,如果当前线程被中断了,返回true。否则返回false

3.Thread.currentThread().isInterrupted()表示查看当前线程的状态是否中断,不清除中断标记

其次要明白:

对于AQS底层,我们调用unlock()方法的底层就是调用LockSupport.park(Object obj)的,

LockSupport.park()与LockSupport.park(Object obj)这两个方法还有所不同的。

LockSupport.park()方法:在执行park()方法之前先进行检验当前线程t1是否有中断标识,如果没有,那么标识着当前t1线程要被阻塞住,不能向下执行。

所谓中断标识就是:如果在其它线程中执行t1.interrupt()中断阻塞着的park方法,那么就会给当前线程t1分配一个中断标识,就会停止阻塞,向下继续执行。

但是当我们执行Thread.currentThread().interrupted()后就会消除该中断标识,那么当下一次park()执行前进行检验中断标识时 ,发现没有!那么就会继续阻塞住。

LockSupport.park(Object obj)方法:它与LockSupport.park()方法相同,obj标识阻塞等待的对象

提出问题:为什么需要中断方法interrupt()的调用?

分析:

interrupt方法为中断线程的方法api,可以中断线程的阻塞。即是中断LockSupport.park()对线程的阻塞,也可以中断Lock.park(Object obj)对线程的阻塞。

之前是如何中断线程的?使用的是底层的stop方法,就好比linux系统下的kill -9 操作,无论是jdk提供的stop还是linux的kill,都是极其的暴力的,会立刻杀死当前线程(线程是进程的组成部分),假设业务执行一半被强制杀死了,那么就是十分危险的。

所以我们寻求一种柔和的中断方式,可以给我们判断的机会来自定义何时进行退出,以此保证业务的安全退出与执行,因此引出interrupt方法+isInterrupted方法来保证自定义退出逻辑:

首先:使用interrupt()方法进行中断阻塞后,会给中断阻塞的线程一个中断标识,标识该线程已经中断啦。

然后:我们就可以结合Thread.currentThread().isInterrupted()方法进行查看当前线程是否已经中断,如果别处已经把该线程中断(即调用interrupt()方法),那么我们就要执行退出。如果判断出该线程未中断,那么继续执行业务。

代码示例1:原生park()和unpark()的使用

/**
 * @Description: TODO
 * @Author: etcEriksen
 * @Date: 2023/3/6
 **/
@SuppressWarnings({"all"})
@Slf4j
public class demo2 {


    public static void main(String[] args) {

        Thread t1 = new Thread(() -> {
            while (true) {
                log.info("开始阻塞");
                LockSupport.park();
                log.info("阻塞结束");
                
                log.info("若不中断,unpark执行完,那么执行业务逻辑");
            }
        });
        t1.start();

        try {
            Thread.sleep(2000);
            log.info("中断线程");
            
            LockSupport.unpark(t1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        log.info("main线程执行结束");
    }


}

代码分析:

 比对运行结果,与分析一致:

代码示例2:原生park()和unpark() 结合 interrupt相关的API 的使用

记住两个点:

(1) interrupt()方法是给线程一个标记:你这个线程已经被附上一个阻断标识了啊,你再park()阻塞也没用,不会阻塞的哈!

(2) interrupt()进行中断后,会给线程做一个中断标识,我们可以调用isInterrupted这个方法去自定义自己的逻辑:中断后去做啥逻辑,退出?还是执行其它业务?这些都由你自己决定!再一次验证了前面的结论:这是一个自定义的,是柔和的中断。

/**
 * @Description: TODO
 * @Author: etcEriksen
 * @Date: 2023/3/6
 **/
@SuppressWarnings({"all"})
@Slf4j
public class demo2 {


    public static void main(String[] args) {

        Thread t1 = new Thread(() -> {
            while (true) {
                log.info("开始阻塞");
                LockSupport.park();
                log.info("阻塞结束");
                if (Thread.currentThread().isInterrupted()) {
                    //再一次执行park()是为了表明:只要该线程被标识了中断标记,那么park()执行多少次也不会阻塞的!
                    LockSupport.park();
                    log.info("中断退出,不执行后面的业务逻辑");
                }
                log.info("若不中断,unpark执行完,那么执行业务逻辑");
            }
        });
        t1.start();

        try {
            Thread.sleep(2000);
            log.info("中断线程");
            t1.interrupt();
            //LockSupport.unpark(t1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        log.info("main线程执行结束");
    }


}

代码分析:

 代码运行结果:一直死循环打印输出,没有尽头

17:19:15.853 [Thread-0] INFO com.messi.leo.AQS.lock.demo2 - 中断退出,不执行后面的业务逻辑
17:19:15.853 [Thread-0] INFO com.messi.leo.AQS.lock.demo2 - 若不中断,unpark执行完,那么执行业务逻辑
17:19:15.853 [Thread-0] INFO com.messi.leo.AQS.lock.demo2 - 开始阻塞
17:19:15.853 [Thread-0] INFO com.messi.leo.AQS.lock.demo2 - 阻塞结束
17:19:15.853 [Thread-0] INFO com.messi.leo.AQS.lock.demo2 - 中断退出,不执行后面的业务逻辑
17:19:15.853 [Thread-0] INFO com.messi.leo.AQS.lock.demo2 - 若不中断,unpark执行完,那么执行业务逻辑
17:19:15.853 [Thread-0] INFO com.messi.leo.AQS.lock.demo2 - 开始阻塞
17:19:15.853 [Thread-0] INFO com.messi.leo.AQS.lock.demo2 - 阻塞结束
17:19:15.853 [Thread-0] INFO com.messi.leo.AQS.lock.demo2 - 中断退出,不执行后面的业务逻辑
17:19:15.853 [Thread-0] INFO com.messi.leo.AQS.lock.demo2 - 若不中断,unpark执行完,那么执行业务逻辑
17:19:15.853 [Thread-0] INFO com.messi.leo.AQS.lock.demo2 - 开始阻塞
17:19:15.853 [Thread-0] INFO com.messi.leo.AQS.lock.demo2 - 阻塞结束
17:19:15.853 [Thread-0] INFO com.messi.leo.AQS.lock.demo2 - 中断退出,不执行后面的业务逻辑
17:19:15.853 [Thread-0] INFO com.messi.leo.AQS.lock.demo2 - 若不中断,unpark执行完,那么执行业务逻辑
17:19:15.853 [Thread-0] INFO com.messi.leo.AQS.lock.demo2 - 开始阻塞
17:19:15.853 [Thread-0] INFO com.messi.leo.AQS.lock.demo2 - 阻塞结束
17:19:15.853 [Thread-0] INFO com.messi.leo.AQS.lock.demo2 - 中断退出,不执行后面的业务逻辑
17:19:15.853 [Thread-0] INFO com.messi.leo.AQS.lock.demo2 - 若不中断,unpark执行完,那么执行业务逻辑
17:19:15.853 [Thread-0] INFO com.messi.leo.AQS.lock.demo2 - 开始阻塞
17:19:15.853 [Thread-0] INFO com.messi.leo.AQS.lock.demo2 - 阻塞结束
17:19:15.853 [Thread-0] INFO com.messi.leo.AQS.lock.demo2 - 中断退出,不执行后面的业务逻辑
17:19:15.853 [Thread-0] INFO com.messi.leo.AQS.lock.demo2 - 若不中断,unpark执行完,那么执行业务逻辑
17:19:15.853 [Thread-0] INFO com.messi.leo.AQS.lock.demo2 - 开始阻塞
17:19:15.853 [Thread-0] INFO com.messi.leo.AQS.lock.demo2 - 阻塞结束
17:19:15.853 [Thread-0] INFO com.messi.leo.AQS.lock.demo2 - 中断退出,不执行后面的业务逻辑
17:19:15.853 [Thread-0] INFO com.messi.leo.AQS.lock.demo2 - 若不中断,unpark执行完,那么执行业务逻辑
17:19:15.853 [Thread-0] INFO com.messi.leo.AQS.lock.demo2 - 开始阻塞
17:19:15.853 [Thread-0] INFO com.messi.leo.AQS.lock.demo2 - 阻塞结束
17:19:15.853 [Thread-0] INFO com.messi.leo.AQS.lock.demo2 - 中断退出,不执行后面的业务逻辑
17:19:15.853 [Thread-0] INFO com.messi.leo.AQS.lock.demo2 - 若不中断,unpark执行完,那么执行业务逻辑
17:19:15.853 [Thread-0] INFO com.messi.leo.AQS.lock.demo2 - 开始阻塞
17:19:15.853 [Thread-0] INFO com.messi.leo.AQS.lock.demo2 - 阻塞结束
17:19:15.853 [Thread-0] INFO com.messi.leo.AQS.lock.demo2 - 中断退出,不执行后面的业务逻辑
17:19:15.853 [Thread-0] INFO com.messi.leo.AQS.lock.demo2 - 若不中断,unpark执行完,那么执行业务逻辑
17:19:15.853 [Thread-0] INFO com.messi.leo.AQS.lock.demo2 - 开始阻塞
17:19:15.853 [Thread-0] INFO com.messi.leo.AQS.lock.demo2 - 阻塞结束
17:19:15.853 [Thread-0] INFO com.messi.leo.AQS.lock.demo2 - 中断退出,不执行后面的业务逻辑
17:19:15.853 [Thread-0] INFO com.messi.leo.AQS.lock.demo2 - 若不中断,unpark执行完,那么执行业务逻辑
17:19:15.853 [Thread-0] INFO com.messi.leo.AQS.lock.demo2 - 开始阻塞
17:19:15.853 [Thread-0] INFO com.messi.leo.AQS.lock.demo2 - 阻塞结束
17:19:15.853 [Thread-0] INFO com.messi.leo.AQS.lock.demo2 - 中断退出,不执行后面的业务逻辑
17:19:15.853 [Thread-0] INFO com.messi.leo.AQS.lock.demo2 - 若不中断,unpark执行完,那么执行业务逻辑
17:19:15.853 [Thread-0] INFO com.messi.leo.AQS.lock.demo2 - 开始阻塞
17:19:15.853 [Thread-0] INFO com.messi.leo.AQS.lock.demo2 - 阻塞结束
17:19:15.853 [Thread-0] INFO com.messi.leo.AQS.lock.demo2 - 中断退出,不执行后面的业务逻辑
17:19:15.853 [Thread-0] INFO com.messi.leo.AQS.lock.demo2 - 若不中断,unpark执行完,那么执行业务逻辑
17:19:15.853 [Thread-0] INFO com.messi.leo.AQS.lock.demo2 - 开始阻塞
17:19:15.853 [Thread-0] INFO com.messi.leo.AQS.lock.demo2 - 阻塞结束
17:19:15.853 [Thread-0] INFO com.messi.leo.AQS.lock.demo2 - 中断退出,不执行后面的业务逻辑
17:19:15.853 [Thread-0] INFO com.messi.leo.AQS.lock.demo2 - 若不中断,unpark执行完,那么执行业务逻辑
17:19:15.853 [Thread-0] INFO com.messi.leo.AQS.lock.demo2 - 开始阻塞
17:19:15.853 [Thread-0] INFO com.messi.leo.AQS.lock.demo2 - 阻塞结束
17:19:15.853 [Thread-0] INFO com.messi.leo.AQS.lock.demo2 - 中断退出,不执行后面的业务逻辑
17:19:15.853 [Thread-0] INFO com.messi.leo.AQS.lock.demo2 - 若不中断,unpark执行完,那么执行业务逻辑
17:19:15.853 [Thread-0] INFO com.messi.leo.AQS.lock.demo2 - 开始阻塞
17:19:15.853 [Thread-0] INFO com.messi.leo.AQS.lock.demo2 - 阻塞结束
17:19:15.853 [Thread-0] INFO com.messi.leo.AQS.lock.demo2 - 中断退出,不执行后面的业务逻辑
17:19:15.853 [Thread-0] INFO com.messi.leo.AQS.lock.demo2 - 若不中断,unpark执行完,那么执行业务逻辑
17:19:15.853 [Thread-0] INFO com.messi.leo.AQS.lock.demo2 - 开始阻塞
17:19:15.853 [Thread-0] INFO com.messi.leo.AQS.lock.demo2 - 阻塞结束
17:19:15.853 [Thread-0] INFO com.messi.leo.AQS.lock.demo2 - 中断退出,不执行后面的业务逻辑
17:19:15.853 [Thread-0] INFO com.messi.leo.AQS.lock.demo2 - 若不中断,unpark执行完,那么执行业务逻辑
17:19:15.853 [Thread-0] INFO com.messi.leo.AQS.lock.demo2 - 开始阻塞
17:19:15.853 [Thread-0] INFO com.messi.leo.AQS.lock.demo2 - 阻塞结束
17:19:15.853 [Thread-0] INFO com.messi.leo.AQS.lock.demo2 - 中断退出,不执行后面的业务逻辑
17:19:15.853 [Thread-0] INFO com.messi.leo.AQS.lock.demo2 - 若不中断,unpark执行完,那么执行业务逻辑
17:19:15.853 [Thread-0] INFO com.messi.leo.AQS.lock.demo2 - 开始阻塞
17:19:15.853 [Thread-0] INFO com.messi.leo.AQS.lock.demo2 - 阻塞结束
17:19:15.853 [Thread-0] INFO com.messi.leo.AQS.lock.demo2 - 中断退出,不执行后面的业务逻辑
17:19:15.853 [Thread-0] INFO com.messi.leo.AQS.lock.demo2 - 若不中断,unpark执行完,那么执行业务逻辑
17:19:15.853 [Thread-0] INFO com.messi.leo.AQS.lock.demo2 - 开始阻塞
17:19:15.853 [Thread-0] INFO com.messi.leo.AQS.lock.demo2 - 阻塞结束
17:19:15.853 [Thread-0] INFO com.messi.leo.AQS.lock.demo2 - 中断退出,不执行后面的业务逻辑
17:19:15.853 [Thread-0] INFO com.messi.leo.AQS.lock.demo2 - 若不中断,unpark执行完,那么执行业务逻辑
17:19:15.853 [Thread-0] INFO com.messi.leo.AQS.lock.demo2 - 开始阻塞
17:19:15.853 [Thread-0] INFO com.messi.leo.AQS.lock.demo2 - 阻塞结束
17:19:15.853 [Thread-0] INFO com.messi.leo.AQS.lock.demo2 - 中断退出,不执行后面的业务逻辑
17:19:15.853 [Thread-0] INFO com.messi.leo.AQS.lock.demo2 - 若不中断,unpark执行完,那么执行业务逻辑
17:19:15.853 [Thread-0] INFO com.messi.leo.AQS.lock.demo2 - 开始阻塞
17:19:15.853 [Thread-0] INFO com.messi.leo.AQS.lock.demo2 - 阻塞结束
17:19:15.853 [Thread-0] INFO com.messi.leo.AQS.lock.demo2 - 中断退出,不执行后面的业务逻辑
17:19:15.853 [Thread-0] INFO com.messi.leo.AQS.lock.demo2 - 若不中断,unpark执行完,那么执行业务逻辑
17:19:15.853 [Thread-0] INFO com.messi.leo.AQS.lock.demo2 - 开始阻塞
17:19:15.853 [Thread-0] INFO com.messi.leo.AQS.lock.demo2 - 阻塞结束
17:19:15.853 [Thread-0] INFO com.messi.leo.AQS.lock.demo2 - 中断退出,不执行后面的业务逻辑
17:19:15.853 [Thread-0] INFO com.messi.leo.AQS.lock.demo2 - 若不中断,unpark执行完,那么执行业务逻辑
17:19:15.853 [Thread-0] INFO com.messi.leo.AQS.lock.demo2 - 开始阻塞
17:19:15.853 [Thread-0] INFO com.messi.leo.AQS.lock.demo2 - 阻塞结束
17:19:15.853 [Thread-0] INFO com.messi.leo.AQS.lock.demo2 - 中断退出,不执行后面的业务逻辑
17:19:15.853 [Thread-0] INFO com.messi.leo.AQS.lock.demo2 - 若不中断,unpark执行完,那么执行业务逻辑
17:19:15.853 [Thread-0] INFO com.messi.leo.AQS.lock.demo2 - 开始阻塞
17:19:15.853 [Thread-0] INFO com.messi.leo.AQS.lock.demo2 - 阻塞结束
17:19:15.853 [Thread-0] INFO com.messi.leo.AQS.lock.demo2 - 中断退出,不执行后面的业务逻辑
17:19:15.853 [Thread-0] INFO com.messi.leo.AQS.lock.demo2 - 若不中断,unpark执行完,那么执行业务逻辑
17:19:15.853 [Thread-0] INFO com.messi.leo.AQS.lock.demo2 - 开始阻塞
17:19:15.853 [Thread-0] INFO com.messi.leo.AQS.lock.demo2 - 阻塞结束
17:19:15.853 [Thread-0] INFO com.messi.leo.AQS.lock.demo2 - 中断退出,不执行后面的业务逻辑
17:19:15.853 [Thread-0] INFO com.messi.leo.AQS.lock.demo2 - 若不中断,unpark执行完,那么执行业务逻辑
17:19:15.853 [Thread-0] INFO com.messi.leo.AQS.lock.demo2 - 开始阻塞
17:19:15.853 [Thread-0] INFO com.messi.leo.AQS.lock.demo2 - 阻塞结束
17:19:15.853 [Thread-0] INFO com.messi.leo.AQS.lock.demo2 - 中断退出,不执行后面的业务逻辑
17:19:15.853 [Thread-0] INFO com.messi.leo.AQS.lock.demo2 - 若不中断,unpark执行完,那么执行业务逻辑
17:19:15.853 [Thread-0] INFO com.messi.leo.AQS.lock.demo2 - 开始阻塞
17:19:15.853 [Thread-0] INFO com.messi.leo.AQS.lock.demo2 - 阻塞结束
17:19:15.853 [Thread-0] INFO com.messi.leo.AQS.lock.demo2 - 中断退出,不执行后面的业务逻辑
17:19:15.853 [Thread-0] INFO com.messi.leo.AQS.lock.demo2 - 若不中断,unpark执行完,那么执行业务逻辑
17:19:15.853 [Thread-0] INFO com.messi.leo.AQS.lock.demo2 - 开始阻塞
17:19:15.853 [Thread-0] INFO com.messi.leo.AQS.lock.demo2 - 阻塞结束
17:19:15.853 [Thread-0] INFO com.messi.leo.AQS.lock.demo2 - 中断退出,不执行后面的业务逻辑
17:19:15.853 [Thread-0] INFO com.messi.leo.AQS.lock.demo2 - 若不中断,unpark执行完,那么执行业务逻辑
17:19:15.853 [Thread-0] INFO com.messi.leo.AQS.lock.demo2 - 开始阻塞
17:19:15.853 [Thread-0] INFO com.messi.leo.AQS.lock.demo2 - 阻塞结束
17:19:15.853 [Thread-0] INFO com.messi.leo.AQS.lock.demo2 - 中断退出,不执行后面的业务逻辑
17:19:15.853 [Thread-0] INFO com.messi.leo.AQS.lock.demo2 - 若不中断,unpark执行完,那么执行业务逻辑
17:19:15.853 [Thread-0] INFO com.messi.leo.AQS.lock.demo2 - 开始阻塞
17:19:15.853 [Thread-0] INFO com.messi.leo.AQS.lock.demo2 - 阻塞结束
17:19:15.853 [Thread-0] INFO com.messi.leo.AQS.lock.demo2 - 中断退出,不执行后面的业务逻辑
17:19:15.853 [Thread-0] INFO com.messi.leo.AQS.lock.demo2 - 若不中断,unpark执行完,那么执行业务逻辑
17:19:15.853 [Thread-0] INFO com.messi.leo.AQS.lock.demo2 - 开始阻塞
17:19:15.853 [Thread-0] INFO com.messi.leo.AQS.lock.demo2 - 阻塞结束
17:19:15.853 [Thread-0] INFO com.messi.leo.AQS.lock.demo2 - 中断退出,不执行后面的业务逻辑
17:19:15.853 [Thread-0] INFO com.messi.leo.AQS.lock.demo2 - 若不中断,unpark执行完,那么执行业务逻辑
17:19:15.853 [Thread-0] INFO com.messi.leo.AQS.lock.demo2 - 开始阻塞
17:19:15.853 [Thread-0] INFO com.messi.leo.AQS.lock.demo2 - 阻塞结束
17:19:15.853 [Thread-0] INFO com.messi.leo.AQS.lock.demo2 - 中断退出,不执行后面的业务逻辑
17:19:15.853 [Thread-0] INFO com.messi.leo.AQS.lock.demo2 - 若不中断,unpark执行完,那么执行业务逻辑
17:19:15.853 [Thread-0] INFO com.messi.leo.AQS.lock.demo2 - 开始阻塞
17:19:15.853 [Thread-0] INFO com.messi.leo.AQS.lock.demo2 - 阻塞结束
17:19:15.853 [Thread-0] INFO com.messi.leo.AQS.lock.demo2 - 中断退出,不执行后面的业务逻辑
17:19:15.853 [Thread-0] INFO com.messi.leo.AQS.lock.demo2 - 若不中断,unpark执行完,那么执行业务逻辑
17:19:15.853 [Thread-0] INFO com.messi.leo.AQS.lock.demo2 - 开始阻塞
17:19:15.853 [Thread-0] INFO com.messi.leo.AQS.lock.demo2 - 阻塞结束
17:19:15.853 [Thread-0] INFO com.messi.leo.AQS.lock.demo2 - 中断退出,不执行后面的业务逻辑
17:19:15.853 [Thread-0] INFO com.messi.leo.AQS.lock.demo2 - 若不中断,unpark执行完,那么执行业务逻辑
17:19:15.853 [Thread-0] INFO com.messi.leo.AQS.lock.demo2 - 开始阻塞
17:19:15.853 [Thread-0] INFO com.messi.leo.AQS.lock.demo2 - 阻塞结束
17:19:15.853 [Thread-0] INFO com.messi.leo.AQS.lock.demo2 - 中断退出,不执行后面的业务逻辑
17:19:15.853 [Thread-0] INFO com.messi.leo.AQS.lock.demo2 - 若不中断,unpark执行完,那么执行业务逻辑
17:19:15.853 [Thread-0] INFO com.messi.leo.AQS.lock.demo2 - 开始阻塞
17:19:15.853 [Thread-0] INFO com.messi.leo.AQS.lock.demo2 - 阻塞结束
17:19:15.853 [Thread-0] INFO com.messi.leo.AQS.lock.demo2 - 中断退出,不执行后面的业务逻辑
17:19:15.853 [Thread-0] INFO com.messi.leo.AQS.lock.demo2 - 若不中断,unpark执行完,那么执行业务逻辑
17:19:15.853 [Thread-0] INFO com.messi.leo.AQS.lock.demo2 - 开始阻塞
17:19:15.853 [Thread-0] INFO com.messi.leo.AQS.lock.demo2 - 阻塞结束
17:19:15.853 [Thread-0] INFO com.messi.leo.AQS.lock.demo2 - 中断退出,不执行后面的业务逻辑
17:19:15.853 [Thread-0] INFO com.messi.leo.AQS.lock.demo2 - 若不中断,unpark执行完,那么执行业务逻辑
17:19:15.853 [Thread-0] INFO com.messi.leo.AQS.lock.demo2 - 开始阻塞
17:19:15.853 [Thread-0] INFO com.messi.leo.AQS.lock.demo2 - 阻塞结束
17:19:15.853 [Thread-0] INFO com.messi.leo.AQS.lock.demo2 - 中断退出,不执行后面的业务逻辑
17:19:15.853 [Thread-0] INFO com.messi.leo.AQS.lock.demo2 - 若不中断,unpark执行完,那么执行业务逻辑
17:19:15.853 [Thread-0] INFO com.messi.leo.AQS.lock.demo2 - 开始阻塞
17:19:15.853 [Thread-0] INFO com.messi.leo.AQS.lock.demo2 - 阻塞结束
17:19:15.853 [Thread-0] INFO com.messi.leo.AQS.lock.demo2 - 中断退出,不执行后面的业务逻辑
17:19:15.853 [Thread-0] INFO com.messi.leo.AQS.lock.demo2 - 若不中断,unpark执行完,那么执行业务逻辑
17:19:15.853 [Thread-0] INFO com.messi.leo.AQS.lock.demo2 - 开始阻塞
17:19:15.853 [Thread-0] INFO com.messi.leo.AQS.lock.demo2 - 阻塞结束
17:19:15.853 [Thread-0] INFO com.messi.leo.AQS.lock.demo2 - 中断退出,不执行后面的业务逻辑
17:19:15.853 [Thread-0] INFO com.messi.leo.AQS.lock.demo2 - 若不中断,unpark执行完,那么执行业务逻辑
17:19:15.853 [Thread-0] INFO com.messi.leo.AQS.lock.demo2 - 开始阻塞
17:19:15.853 [Thread-0] INFO com.messi.leo.AQS.lock.demo2 - 阻塞结束
17:19:15.853 [Thread-0] INFO com.messi.leo.AQS.lock.demo2 - 中断退出,不执行后面的业务逻辑
17:19:15.853 [Thread-0] INFO com.messi.leo.AQS.lock.demo2 - 若不中断,unpark执行完,那么执行业务逻辑
17:19:15.853 [Thread-0] INFO com.messi.leo.AQS.lock.demo2 - 开始阻塞
17:19:15.853 [Thread-0] INFO com.messi.leo.AQS.lock.demo2 - 阻塞结束
17:19:15.853 [Thread-0] INFO com.messi.leo.AQS.lock.demo2 - 中断退出,不执行后面的业务逻辑
17:19:15.853 [Thread-0] INFO com.messi.leo.AQS.lock.demo2 - 若不中断,unpark执行完,那么执行业务逻辑
17:19:15.853 [Thread-0] INFO com.messi.leo.AQS.lock.demo2 - 开始阻塞
17:19:15.853 [Thread-0] INFO com.messi.leo.AQS.lock.demo2 - 阻塞结束
17:19:15.853 [Thread-0] INFO com.messi.leo.AQS.lock.demo2 - 中断退出,不执行后面的业务逻辑
 

代码示例三:在代码示例二的基础上加上一个interrupted方法

记住一个点:

interrupted方法就是消除当前调用方法的线程对象的中断标识。那么当线程的中断标识被消除成功时,我们再一次park(),线程就会被阻塞掉

返回值为布尔类型,若当前线程有中断标识,那么返回true。否则返回false

代码演示:

/**
 * @Description: TODO
 * @Author: etcEriksen
 * @Date: 2023/3/6
 **/
@SuppressWarnings({"all"})
@Slf4j
public class demo2 {


    public static void main(String[] args) {

        Thread t1 = new Thread(() -> {
            while (true) {
                log.info("开始阻塞");
                LockSupport.park();
                log.info("阻塞结束");
                if (Thread.currentThread().isInterrupted()) {
                    boolean isInterrupted = Thread.currentThread().interrupted();
                    if (isInterrupted) {
                        log.info("线程的中断标识被消除成功,再一次park()执行时 线程就会被阻塞掉");
                    }
                  
                    LockSupport.park();
                    log.info("中断退出,不执行后面的业务逻辑");
                }
                log.info("若不中断,unpark执行完,那么执行业务逻辑");
            }
        });
        t1.start();

        try {
            Thread.sleep(2000);
            log.info("中断线程");
            t1.interrupt();
            //LockSupport.unpark(t1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        log.info("main线程执行结束");
    }


}

代码分析:

 代码输出:

重点: LockSupport.park(Object obj)结合AQS底层源码剖析:

ReentrantLock类对应的加锁流程:

1.

2.

 3.

4.

 5.

 

6.

 7.

ReentrantLock类对应的释放锁流程:

1.

2.

 

得出结论:

unpark和interrupt方法都可以进行停止park方法的线程阻塞。

但是interrupt不同之处在于,它给线程对象分配了一个中断标识,当下一次进行park时判断出当前线程对象存在该中断标识,那么就不会park阻塞!!!因此我们可以使用interrupted方法进行消除线程对象的中断标识!

四:lockInterruptibly()深度剖析

概念:

lockInterruptibly()方法和lock方法一致,也必须是等待持有锁对象的线程做完事情,其他线程才能做事情,但中途可以进行退出。

说详细点就是:当节点(线程封装在节点中)加入到阻塞队列中后,若线程执行interrupt中断方法,会抛出中断异常,中途会从阻塞队列中进行退出,表示获取锁失败,waitStatus标记为1,表示出现异常导致退出!

源码深度剖析:

1.

 2.

 3.

4.

doAcquireInterruptibly方法调用的shouldParkAfterFailedAcquire方法解析:与lock()底层的shouldParkAfterFailedAcquire方法一致,不再详细赘述

doAcquireInterruptibly方法调用的 parkAndCheckInterrupt方法 解析:

5.

6.cacelAcquire方法解析:

 至于这个方法的具体实现,总结一句话就是对阻塞队列中出现异常的节点要进行从阻塞队列中移除,并且抛出中断异常,并且要进行修改节点对应waitStatus的值。具体怎么实现的,画一张图,一步步对照着走即可,easy!

总结:lock()与lockInterruptibly()里面中断(interrupt)信号的实际应用场景【重点】

lock():

概念回顾

这种lock()获取锁对象的方式必须等持有锁对象的线程做完事情,其他等待的线程才可以做事情。而且中途不能退出,会一直阻塞存储在CLH双端队列中。

说详细点就是:当一个节点对象加入到CLH双端阻塞队列中后,在被持有锁对象唤醒之前,它中途就不能退出该队列,即使阻塞队列中节点对应的线程对象被中断,也不可以退出!

lockI()底层的CLH阻塞队列中Node节点对应的线程对应的中断信号有啥用?

实际案例证明:还是追源码,中间省略了很多easy代码,只保留了关键处

 selfInterrupt方法对当前线程的中断有啥用?我在搞笑吗???

回到API调用的应用层:

如下图:画红圈就是作用,我们可以跳过interrupted()方法得到中断的信号去自定义业务逻辑!!!!很重要的一个点!

lockInterruptibly():

概念回顾:

方法和lock方法一致,也必须是等待持有锁对象的线程做完事情,其他线程才能做事情,但中途可以进行退出。

说详细点就是:当节点(线程封装在节点中)加入到阻塞队列中后,若线程执行interrupt中断方法,会抛出中断异常,中途会从阻塞队列中进行退出,表示获取锁失败,waitStatus标记为1,表示出现异常导致退出!

lockInterruptibly()底层的CLH阻塞队列中Node节点对应的线程对应的中断信号有啥用?

实际案例证明:还是追源码,中间省略了很多easy代码,只保留了关键处:

 

 回到API调用的应用层:

初次学习AQS底层源码,可能理解不深刻。若有不对,期待指正。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/391797.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

C语言-基础了解-25-C强制类型转换

C强制类型转换 一、强制类型转换 强制类型转换是把变量从一种类型转换为另一种数据类型。例如,如果您想存储一个 long 类型的值到一个简单的整型中,您需要把 long 类型强制转换为 int 类型。您可以使用强制类型转换运算符来把值显式地从一种类型转换为…

【深度学习】BERT变体—ALBERT

ALBERT的初衷是想解决BERT中参数量过多的问题,论文全称为:ALBERT: A Lite BERT for Self-supervised Learning of Language Representations。 相较于BERT,ALBERT模型减少BERT模型的参数量;预训练中的Next Sentence Prediction&a…

【面试系列】线程相关的面试题集锦

线程的状态 public enum State {/*** Thread state for a thread which has not yet started.*/NEW,/*** Thread state for a runnable thread. A thread in the runnable* state is executing in the Java virtual machine but it may* be waiting for other resources from …

最简单的线性回归模型-标量

首先考虑yyy为标量,www为标量的情况,那么我们的线性函数为ywxbywxbywxb。每批输入的量batch size 为111,每批输入的xxx为一个标量,设为x∗x^*x∗,标签yyy同样为一个标量,设为y∗y^*y∗。因此每批训练的损失…

直线模组的优势是什么?

直线模组是可以模拟人工操作的一些功能,通过固定程序来进行抓取,搬运、操作工具,实现自动变速,这也是为何直线模组使用率高的主要原因了,那么直线模组究竟有着怎样的优势呢? 1、整体结构紧凑,重…

k8s-Kubernetes集群部署

文章目录前言一、Kubernetes简介与架构1.Kubernetes简介2.kubernetes设计架构二、Kubernetes集群部署1.集群环境初始化2.所有节点安装kubeadm3.拉取集群所需镜像3.集群初始化4.安装flannel网络插件5.扩容节点6.设置kubectl命令补齐前言 一、Kubernetes简介与架构 1.Kubernetes…

Spark 磁盘作用

Spark 磁盘作用磁盘作用性能价值失败重试ReuseExchangeSpark 导航 磁盘作用 临时文件、中间文件、缓存数据,都会存储到 spark.local.dir 中 在 Shuffle Map 时, 当内存空间不足,就会溢出临时文件存储到磁盘上溢出的临时文件一起做归并计算…

Vue3---语法初探

目录 hello world 实现简易计时显示 反转字符串 显示隐藏 了解循环 了解双向绑定实现简易记事 设置鼠标悬停的文本 组件概念初探,进行组件代码拆分 hello world 最原始形态,找到 id 为 root 的标签,将 Vue 实例的模板放入标签之内 …

剑指 Offer 09. 用两个栈实现队列(java)

用两个栈实现一个队列。队列的声明如下,请实现它的两个函数 appendTail 和 deleteHead ,分别完成在队列尾部插入整数和在队列头部删除整数的功能。(若队列中没有元素,deleteHead 操作返回 -1 ) 示例 1: 输入: [“CQu…

SpringBoot中一行代码解决字符串向枚举类型转换的问题

1. 场景 在WEB开发,客户端和服务端传输的数据中经常包含一些这样的字段:字段的值只包括几个固定的字符串。 这样的字段意味着我们需要在数据传输对象(Data Transfer Object, DTO)中对该字段进行校验以避免客户端传输的非法数据持…

Android Service知识

一. 概览 Service 是一种可在后台执行长时间运行操作而不提供界面的应用组件。服务可由其他应用组件启动,而且即使用户切换到其他应用,服务仍将在后台继续运行。此外,组件可通过绑定到服务与之进行交互,甚至是执行进程间通信 (IPC…

你是真的“C”——为冒泡排序升级赋能!

你是真的“C”——为冒泡排序升级赋能!😎前言🙌冒泡排序升级赋能之境界一!冒泡排序升级赋能之境界二!qsort库函数的运用和认识总结撒花💞😎博客昵称:博客小梦 😊最喜欢的…

【CDP】更改solr 存储路径导致ranger-audit 大量报错问题解决

前言 我们生产上公司是使用的CDP集群,一次管理员通知,Solr 组件的数据存放路径磁盘空间不够。 我们的solr 组件时为 Ranger 服务提供日志审计功能, 在我们更改了磁盘路径,并重启了Solr 组件,然后发现相关组件&#…

基于Python的selenium

一、安装 1.1安装Python,安装Python时需要勾选增加环境变量 如果之前已经安装过Python,需要将Python相关文件以及环境变量删除 1.2安装成功:在命令行界面下输入Python,最终展示>>>即可成功 2.1安装pycharm,直接自定义安装…

论文阅读-(GLIP)Grounded Language-Image Pre-training (目标检测+定位)

Paper:Grounded Language-Image Pre-training Code:https://github.com/microsoft/GLIP 简介: 定位任务与图像检测任务非常类似,都是去图中找目标物体的位置,目标检测为给出一张图片找出bounding box,定位…

07react+echart,大屏代码开发

react框架引入第三方插件原链接gitHub:GitHub - hustcc/echarts-for-react: ⛳ Apache ECharts components for React wrapper. 一个简单的 Apache echarts 的 React 封装。import ReactECharts from echarts-for-react;import * as echarts from echarts;一、软件简介echarts-…

微机原理和计算机组成原理复习

1:冯诺依曼机器的主要特点? 1)计算机由运算器、存储器、控制器、输入设备和输出设备五大部分组成; 2)指令和数据存储在存储器中,并可以按地址访问; 3)指令和数据均以二进制表示&…

2023年软考中级信息安全工程师考什么?

信息安全工程师是属于软考中级科目,含金量很高的,信息安全工程师在社会上的需求是蛮大的。 信息安全工程师主要涉及计算机信息安全方面,在计算机软硬件、网络、应用相关领域从事安全系统设计、安全产品开发、产品集成、信息系统安全检测与审计…

小程序容器作为软件中间件技术不可忽视的价值

随着移动互联网的不断发展,越来越多的应用程序被广泛应用于各种行业和领域中。然而,不同的平台和设备之间的差异性和复杂性,给应用程序的开发和部署带来了很大的挑战。这时,小程序容器作为软件中间件技术的其中一环,成…

格密码学习笔记(三):闵可夫斯基第一定理

文章目录NNN维超球体体积结论闵可夫斯基凸体定理闵可夫斯基第一定理闵可夫斯基第二定理致谢NNN维超球体体积结论 在nnn维空间中,对半径为rrr的超球体(Ball),有vol(B(0,r))≥(2rn)n\mathrm{vol}(\mathcal{B}(0, r)) \geq \left( \…