最全【并发编程】(学习笔记-共享模型之管程)-part3,新东方java开发面试题

最后

腾讯T3大牛总结的500页MySQL实战笔记意外爆火,P8看了直呼内行

腾讯T3大牛总结的500页MySQL实战笔记意外爆火,P8看了直呼内行

本文已被CODING开源项目:【一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码】收录

需要这份系统化的资料的朋友,可以点击这里获取

例2:

public class MyServlet extends HttpServlet {

// 是否安全?no,共享的

private UserService userService = new UserServiceImpl();

public void doGet(HttpServletRequest request, HttpServletResponse response) {

userService.update(…);

}

}

public class UserServiceImpl implements UserService {

// 记录调用次数

private int count = 0;//这里多个线程调用可能出问题

public void update() {

// …

count++;

}

}

例3:

@Aspect

@Component

public class MyAspect {//单例,成员变量会共享

// 是否安全?no

private long start = 0L;

@Before(“execution(* *(…))”)

public void before() {

start = System.nanoTime();

}

@After(“execution(* *(…))”)

public void after() {

long end = System.nanoTime();

System.out.println(“cost time:” + (end-start));

}

}

4.Monitor


4-1 Java对象头

这里以32位虚拟机为例:

普通对象:

在这里插入图片描述

数组对象:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pI1uEfvf-1635813080474)(C:\Users\30287\AppData\Roaming\Typora\typora-user-images\image-20211028191602239.png)]

其中Mark Word 结构为:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YXYejRPV-1635813080476)(C:\Users\30287\AppData\Roaming\Typora\typora-user-images\image-20211028191621912.png)]

64 位虚拟机 Mark Word:

在这里插入图片描述

4-2 原理之Monitor

Monitor被翻译为监视器管程

每个 Java 对象都可以关联一个 Monitor 对象,如果使用 synchronized 给对象上锁(重量级)之后,该对象头的 Mark Word 中就被设置指向 Monitor 对象的指针

Monitor 结构如下:

img

  • 刚开始 Monitor 中 Owner 为 null

  • 当 Thread-2 执行 synchronized(obj) 就会将 Monitor 的所有者 Owner 置为 Thread-2,Monitor中只能有一 个 Owner

  • 在 Thread-2 上锁的过程中,如果 Thread-3,Thread-4,Thread-5 也来执行 synchronized(obj),就会进入 EntryList (进入阻塞状态,BLOCKED )

  • Thread-2 执行完同步代码块的内容,然后唤醒 EntryList 中等待的线程来竞争锁,竞争的时是非公平的

  • 图中 WaitSet 中的 Thread-0,Thread-1 是之前获得过锁,但条件不满足进入 WAITING 状态的线程,后面讲 wait-notify 时会分析

注意:

  • synchronized 必须是进入同一个对象的 monitor 才有上述的效果
  • 不加 synchronized 的对象不会关联监视器,不遵从以上规则

5.Synchronized原理进阶


5-1 轻量级锁

  • 轻量级锁使用场景:当一个对象被多个线程所访问,但访问的时间是错开的(不存在竞争),此时就可以使用轻量级锁来优化。

  • 轻量级锁对使用者是透明的,即语法仍然是 synchronized

这里举个栗子:假设有两个方法同步块,利用同一个对象加锁

static final Object obj = new Object();

public static void method1() {

synchronized( obj ) {

// 同步块 A

method2();

}

}

public static void method2() {

synchronized( obj ) {

// 同步块 B

}

}

  • 创建锁记录(Lock Record)对象,每个线程的栈帧都会包含一个锁记录对象,内部可以存储锁定对象的mark word(不再一开始就使用Monitor)

img

  • 让锁记录中的Object reference指向锁对象(Object),并尝试用cas去替换Object中的mark word,将此mark word放入lock record中保存

img

  • 如果cas替换成功,则将Object的对象头替换为锁记录的地址状态 00(轻量级锁状态),并由该线程给对象加锁

img

  • 如果 cas 失败,有两种情况:

  • 如果是其它线程已经持有了该 Object 的轻量级锁,这时表明有竞争,进入锁膨胀过程

  • 如果是自己执行了 synchronized 锁重入,那么再添加一条 Lock Record 作为重入的计数

在这里插入图片描述

  • 当退出 synchronized 代码块(解锁时)如果有取值为 null 的锁记录,表示有重入,这时重置锁记录,表示重入计数减一

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-sBIUQ4HQ-1635813080488)(C:\Users\30287\AppData\Roaming\Typora\typora-user-images\image-20211031115303491.png)]

  • 当退出 synchronized 代码块(解锁时)锁记录的值不为 null,这时使用 cas 将 Mark Word 的值恢复给对象头

  • 成功,则解锁成功

  • 失败,说明轻量级锁进行了锁膨胀或已经升级为重量级锁,进入重量级锁解锁流程

5-2 锁膨胀

如果在尝试加轻量级锁的过程中,CAS 操作无法成功,这时一种情况就是有其它线程为此对象加上了轻量级锁(有竞争),这时需要进行锁膨胀,将轻量级锁变为重量级锁。

  • 当 Thread-1 进行轻量级加锁时,Thread-0 已经对该对象加了轻量级锁

img

  • 这时 Thread-1 加轻量级锁失败,进入锁膨胀流程

  • 即为 Object 对象申请 Monitor 锁,让 Object 指向重量级锁地址

  • 然后自己进入 Monitor 的 EntryList BLOCKED

img

  • 当 Thread-0 退出同步块解锁时,使用 cas 将 Mark Word 的值恢复给对象头,失败。这时会进入重量级解锁流程,即按照 Monitor 地址找到 Monitor 对象,设置 Owner 为 null,唤醒 EntryList 中 BLOCKED 线程

5-3 自旋优化

重量级锁竞争的时候,还可以使用自旋来进行优化,如果当前线程自旋成功(即这时候持锁线程已经退出了同步块,释放了锁),这时当前线程就可以避免阻塞

  • 自旋会占用 CPU 时间,单核 CPU 自旋就是浪费,多核 CPU 自旋才能发挥优势。

  • 在 Java 6 之后自旋锁是自适应的,比如对象刚刚的一次自旋操作成功过,那么认为这次自旋成功的可能性会高,就多自旋几次;反之,就少自旋甚至不自旋,总之,比较智能。

  • Java 7 之后不能控制是否开启自旋功能

5-4 偏向锁

轻量级锁在没有竞争时(就自己这个线程),每次重入仍然需要执行 CAS 操作。

Java 6 中引入了偏向锁来做进一步优化:只有第一次使用 CAS 将线程 ID 设置到对象的 Mark Word 头,之后发现这个线程 ID 是自己的就表示没有竞争,不用重新 CAS。以后只要不发生竞争,这个对象就归该线程所有

img

偏向状态

  • Normal:一般状态,没有加任何锁,前面62位保存的是对象的信息,最后2位为状态(01),倒数第三位表示是否使用偏向锁(未使用:0)

  • Biased:偏向状态,使用偏向锁,前面54位保存的当前线程的ID,最后2位为状态(01),倒数第三位表示是否使用偏向锁(使用:1)

  • Lightweight:使用轻量级锁,前62位保存的是锁记录的指针,最后两位为状态(00)

  • Heavyweight:使用重量级锁,前62位保存的是Monitor的地址指针,后两位为状态(10)

img

  • 如果开启了偏向锁(默认开启),在创建对象时,对象的Mark Word后三位应该是101

  • 但是偏向锁默认是有延迟的,不会再程序一启动就生效,而是会在程序运行一段时间(几秒之后),才会对创建的对象设置为偏向状态

  • 如果没有开启偏向锁,对象的Mark Word后三位应该是001

撤销偏向

以下几种情况会使对象的偏向锁失效

  • 调用对象的hashCode方法

  • 多个线程使用该对象

  • 调用了wait/notify方法(调用wait方法会导致锁膨胀而使用重量级锁

批量重偏向

  • 如果对象虽然被多个线程访问,但是线程间不存在竞争,这时偏向T1的对象仍有机会重新偏向T2。重偏向会重置Thread ID

  • 当撤销超过20次后(超过阈值),JVM会觉得是不是偏向错了,这时会在给对象加锁时,重新偏向至加锁线程

批量撤销

当撤销偏向锁阈值超过 40 次后,jvm 会这样觉得,自己确实偏向错了,根本就不该偏向。于是整个类的所有对象都会变为不可偏向的,新建的对象也是不可偏向的

6.Wait/Notify


6-1 原理

img

  • Owner 线程发现条件不满足,调用 wait 方法,即可进入 WaitSet 变为 WAITING 状态

  • BLOCKED 和 WAITING 的线程都处于阻塞状态,不占用 CPU 时间片

  • BLOCKED 线程会在 Owner 线程释放锁时唤醒

  • WAITING 线程会在 Owner 线程调用 notifynotifyAll 时唤醒,但唤醒后并不意味者立刻获得锁,仍需进入 EntryList 重新竞争

这里附《Java并发编程的艺术》里的一张图:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YF2j6M2N-1635813080496)(C:\Users\30287\AppData\Roaming\Typora\typora-user-images\image-20211029202453088.png)]

6-2 API介绍

  • wait() 让进入 object 监视器的线程到 waitSet 等待

  • notify() 在 object 上正在 waitSet 等待的线程中挑一个唤醒

  • notifyAll() 让 object 上正在 waitSet 等待的线程全部唤醒

  • wait() 方法会释放对象的锁,进入 WaitSet 等待区,从而让其他线程就机会获取对象的锁。无限制等待,直到 notify

  • 为止 wait(long n) 有时限的等待, 到 n 毫秒后结束等待,或是被 notify

注意:只有当对象被锁以后,才能调用wait和notify方法

例子:

@Slf4j

public class Test6 {

final static Object lock = new Object();

public static void main(String[] args) {

new Thread(() -> {

synchronized (lock) {

log.debug(“执行…”);

try {

lock.wait();//让线程在lock上一直等待下去

} catch (InterruptedException e) {

e.printStackTrace();

}

log.debug(“其他代码…”);

}

}, “t1”).start();

new Thread(() -> {

synchronized (lock) {

log.debug(“执行…”);

try {

lock.wait();//让线程在lock上一直等待下去

} catch (InterruptedException e) {

e.printStackTrace();

}

log.debug(“其他代码…”);

}

}, “t2”).start();

Sleeper.sleep(2);

log.debug(“唤醒 lock 上的线程”);

synchronized (lock) {

lock.notify(); //唤醒 lock 上的一个线程

//lock.notifyAll(); //唤醒 lock 上所有等待线程

}

}

}

//输出

20:36:08 DEBUG [t1] (Test6.java:13) - 执行…

20:36:08 DEBUG [t2] (Test6.java:25) - 执行…

20:36:10 DEBUG [main] (Test6.java:35) - 唤醒 lock 上的线程

20:36:10 DEBUG [t1] (Test6.java:19) - 其他代码…

6-3 wait/notify 的正确使用

开始之前先看看 sleep(long n)wait(long n) 的区别 :

  1. sleep 是 Thread 方法,而 wait 是 Object 的方法

  2. sleep 不需要强制和 synchronized 配合使用,但 wait 需要和 synchronized 一起用

  3. sleep 在睡眠的同时,不会释放对象锁,但 wait 在等待的时候会释放对象锁

  4. 他们的状态都是TIMED_WAITING

以下部分建议参考:《Java并发编程的艺术》读后笔记-part4

等待方遵循如下原则:

  1. 获取对象的锁

  2. 如果条件不满足,那么调用对象的wait()方法,被通知后仍要检查条件

  3. 条件满足则执行对应的逻辑

对应伪代码:

synchronized(对象){

while(条件不满足){

对象.wait();

}

对应的处理逻辑

}

通知方遵循如下原则:

  1. 获得对象的锁

  2. 改变条件

  3. 通知所有等待在对象上的线程

对应伪代码:

synchronized(对象){

改变条件

对象.notifyAll();

}

7.Park/Unpark


7-1 基本使用

park/unpark都是 LockSupport 类中的方法

//暂停线程运行

LockSupport.park;

//恢复线程运行

LockSupport.unpark(thread);

先 park 再 unpark:

@Slf4j

public class Test24 {

public static void main(String[] args) {

Thread t1 = new Thread(() -> {

log.debug(“start…”);

Sleeper.sleep(1);

log.debug(“park…”);

//暂停线程运行

LockSupport.park();

log.debug(“resume…”);

}, “t1”);

t1.start();

Sleeper.sleep(2);

log.debug(“unpark…”);

//恢复线程运行

LockSupport.unpark(t1);

}

}

13:11:08 DEBUG [t1] (Test24.java:12) - start…

13:11:09 DEBUG [t1] (Test24.java:14) - park…

13:11:10 DEBUG [main] (Test24.java:20) - unpark…

13:11:10 DEBUG [t1] (Test24.java:16) - resume…

7-2 特点

与 Object 的 wait/notify 相比:

  • wait,notify 和 notifyAll 必须配合Object Monitor一起使用,而park,unpark不必

  • park,unpark 是以线程为单位阻塞唤醒线程,而 notify 只能随机唤醒一个等待线程,notifyAll 是唤醒所有等待线程,就不那么精确

  • park & unpark 可以先 unpark,而 wait & notify 不能先 notify

  • park不会释放锁,而wait会释放锁

7-3 原理

每个线程都有一个自己的Park对象,并且该对象_counter_cond__mutex组成

  • 先调用park再调用unpark时

  • 先调用park

  • 线程运行时,会将Park对象中的_counter的值设为0;

  • 调用park时,会先查看counter的值是否为0,如果为0,则将线程放入阻塞队列cond中

  • 放入阻塞队列中后,会再次将counter设置为0

  • 然后调用unpark

  • 调用unpark方法后,会将counter的值设置为1

  • 去唤醒阻塞队列cond中的线程

  • 线程继续运行并将counter的值设为0

img

img

  • 先调用unpark,再调用park

  • 调用unpark

  • 会将counter设置为1(运行时0)

  • 调用park方法

  • 查看counter是否为0

  • 因为unpark已经把counter设置为1,所以此时将counter设置为0,但不放入阻塞队列cond中

img

8.重新理解线程状态转换


img

假设有线程Thread t

情况一:NEW –> RUNNABLE

  • 当调用了t.start()方法时,由 NEW –> RUNNABLE

情况二: RUNNABLE <–> WAITING

  • 当调用了t 线程用 synchronized(obj) 获取了对象锁后

  • 调用 obj.wait() 方法时,t 线程从 RUNNABLE –> WAITING

  • 调用 obj.notify()obj.notifyAll()t.interrupt()

  • 竞争锁成功,t 线程从 WAITING –> RUNNABLE

  • 竞争锁失败,t 线程从 WAITING –> BLOCKED

情况三:RUNNABLE <–> WAITING

  • 当前线程调用 t.join() 方法时,当前线程从 RUNNABLE –> WAITING

  • 注意是当前线程在t 线程对象的监视器上等待

  • t 线程运行结束,或调用了当前线程interrupt() 时,当前线程从 WAITING –> RUNNABLE

情况四: RUNNABLE <–> WAITING

  • 当前线程调用 LockSupport.park() 方法会让当前线程从 RUNNABLE –> WAITING

  • 调用 LockSupport.unpark(目标线程) 或调用了线程的 interrupt() ,会让目标线程从 WAITING –> RUNNABLE

情况五: RUNNABLE <–> TIMED_WAITING

t 线程用 synchronized(obj) 获取了对象锁后

  • 调用 obj.wait(long n) 方法时,t 线程从 RUNNABLE –> TIMED_WAITING

  • t 线程等待时间超过了 n 毫秒,或调用 obj.notify()obj.notifyAll()t.interrupt()

  • 竞争锁成功,t 线程从 TIMED_WAITING –> RUNNABLE

  • 竞争锁失败,t 线程从 TIMED_WAITING –> BLOCKED

情况六:RUNNABLE <–> TIMED_WAITING

  • 当前线程调用 t.join(long n) 方法时,当前线程从 RUNNABLE –> TIMED_WAITING

  • 注意是当前线程在t 线程对象的监视器上等待

  • 当前线程等待时间超过了 n 毫秒,或t 线程运行结束,或调用了当前线程的 interrupt() 时,当前线程从 TIMED_WAITING –> RUNNABLE

情况七:RUNNABLE <–> TIMED_WAITING

  • 当前线程调用 Thread.sleep(long n) ,当前线程从 RUNNABLE –> TIMED_WAITING

  • 当前线程等待时间超过了 n 毫秒,当前线程从 TIMED_WAITING –> RUNNABLE

情况八:RUNNABLE <–> TIMED_WAITING

  • 当前线程调用 LockSupport.parkNanos(long nanos)LockSupport.parkUntil(long millis) 时,当前线 程从 RUNNABLE –> TIMED_WAITING

  • 调用 LockSupport.unpark(目标线程) 或调用了线程的 interrupt() ,或是等待超时,会让目标线程从 TIMED_WAITING–> RUNNABLE

情况九:RUNNABLE <–> BLOCKED

  • t 线程用 synchronized(obj) 获取了对象锁时如果竞争失败,从 RUNNABLE –> BLOCKED

  • 持 obj 锁线程的同步代码块执行完毕,会唤醒该对象上所有 BLOCKED 的线程重新竞争,如果其中 t 线程竞争成功,从 BLOCKED –> RUNNABLE ,其它失败的线程仍然 BLOCKED

情况十: RUNNABLE <–> TERMINATED

当前线程所有代码运行完毕,进入 TERMINATED


这里附《Java并发编程的艺术》的一张图做一个概括:

在这里插入图片描述

9.多把锁


class BigRoom {

//额外创建对象来作为锁

private final Object studyRoom = new Object();

private final Object bedRoom = new Object();

}

将锁的粒度细分

  • 好处:是可以增强并发度

  • 坏处:如果一个线程需要同时获得多把锁,就容易发生死锁

10.活跃性


10-1 死锁

有这样的情况:一个线程需要同时获取多把锁,这时就容易发生死锁

如:

  • t1线程获得A对象锁,接下来想获取B对象的锁

  • t2线程获得B对象锁,接下来想获取A对象的锁

我们来看个死锁的栗子:

@Slf4j

public class Test26 {

public static void main(String[] args) {

test1();

}

private static void test1() {

Object A = new Object();

Object B = new Object();

Thread t1 = new Thread(() -> {

synchronized (A) {

log.debug(“lock A”);

Sleeper.sleep(1);

synchronized (B) {

log.debug(“lock B”);

log.debug(“操作…”);

}

}

}, “t1”);

Thread t2 = new Thread(() -> {

synchronized (B) {

log.debug(“lock B”);

Sleeper.sleep(1);

synchronized (A) {

log.debug(“lock A”);

log.debug(“操作…”);

}

}

}, “t2”);

t1.start();

t2.start();

}

}

死锁产生的必要条件

  1. 互斥条件。只有对必须互斥使用的资源的争抢才会导致死锁(如哲学家的筷子、打印机设备)。像内存、扬声器这样可以同时让多个进程使用的资源是不会导致死锁的(因为进程不用阻塞等待这种资源)。

  2. 不剥夺条件。进程所获得的资源在未使用完之前,不能由其他进程强行夺走,只能主动释放。

  3. 请求和保持条件。进程已经保持了至少一个资源,但又提出了新的资源请求,而该资源又被其他进程占有,此时请求进程被阻塞,但又对自己已有的资源保持不放。

  4. 循环等待条件。存在一种进程资源的循环等待链,链中的每一个进程已获得的资源同时被下一个进程所请求。

如果想要详细了解死锁,可以参考操作系统-死锁


10-2 定位死锁

检测死锁可以使用 jconsole工具,或者使用 jps 定位进程 id,再用 jstack 定位死锁

  • jps+jstack ThreadID

在这里插入图片描述

  • jconsole

img

img

10-3 哲学家就餐问题

img

代码演示:

public class Test27 {

public static void main(String[] args) {

Chopstick c1 = new Chopstick(“1”);

Chopstick c2 = new Chopstick(“2”);

Chopstick c3 = new Chopstick(“3”);

Chopstick c4 = new Chopstick(“4”);

Chopstick c5 = new Chopstick(“5”);

new Philosopher(“苏格拉底”, c1, c2).start();

new Philosopher(“柏拉图”, c2, c3).start();

new Philosopher(“亚里士多德”, c3, c4).start();

new Philosopher(“赫拉克利特”, c4, c5).start();

new Philosopher(“阿基米德”, c5, c1).start();

}

}

/**

  • 筷子类

*/

class Chopstick {

String name;

public Chopstick(String name) {

this.name = name;

}

@Override

public String toString() {

return “筷子{” + name + ‘}’;

}

}

/**

  • 哲学家类

*/

@Slf4j

class Philosopher extends Thread {

Chopstick left;

Chopstick right;

public Philosopher(String name, Chopstick left, Chopstick right) {

super(name);

this.left = left;

this.right = right;

}

public void eat() {

log.debug(“eating…”);

Sleeper.sleep(1);

}

@Override

public void run() {

while (true) {

while (true) {

// 获得左手筷子

synchronized (left) {

// 获得右手筷子

synchronized (right) {

// 吃饭

eat();

}

// 放下右手筷子

}

// 放下左手筷子

}

}

}

}

//该代码会发生死锁

10-4 活锁

活锁出现在两个线程互相改变对方的结束条件,最后后谁也无法结束

举个例子:

@Slf4j

public class Test28 {

static volatile int count = 10;

static final Object lock = new Object();

public static void main(String[] args) {

new Thread(() -> {

while (count > 0) {

Sleeper.sleep(0.2);

count–;

log.debug(“count:{}”, count);

}

}, “t1”).start();

new Thread(() -> {

while (count < 20) {

Sleeper.sleep(0.2);

count++;

log.debug(“count:{}”, count);

}

}, “t2”).start();

}

}

//可以改变两者睡眠时间,使其交错,避免活锁产生

死锁与活锁的区别

  • 死锁:是因为线程互相持有对象想要的锁,并且都不释放,最后到时线程阻塞停止运行的现象。

  • 活锁:是因为线程间修改了对方的结束条件,而导致代码一直在运行,却一直运行不完的现象

10-5 饥饿

一个线程由于优先级太低,始终得不到 CPU 调度执行,也不能够结束,这种现象就是饥饿

11.ReentrantLock


11-1 特点+语法

ReentrantLock相对于 synchronized 它具备如下特点:

  • 可中断

  • 可以设置超时时间

  • 可以设置为公平锁

  • 支持多个条件变量(具有多个waitset

synchronized 一样,都支持可重入

基本语法:

// 获取锁

reentrantLock.lock();

try {

// 临界区

} finally {

// 释放锁

reentrantLock.unlock();

}

11-2 可重入

  • 可重入是指同一个线程如果首次获得了这把锁,那么因为它是这把锁的拥有者,因此有权利再次获取这把锁

  • 如果是不可重入锁,那么第二次获得锁时,自己也会被锁挡住

锁重入例子:

@Slf4j

public class Test29 {

private static ReentrantLock lock = new ReentrantLock();

public static void main(String[] args) {

lock.lock();

try {

log.debug(“enter main”);

m1();

} finally {

lock.unlock();

}

}

public static void m1() {

lock.lock();

try {

log.debug(“enter m1”);

m2();

} finally {

lock.unlock();

}

}

public static void m2() {

lock.lock();

最后

这份《“java高分面试指南”-25分类227页1000+题50w+字解析》同样可分享给有需要的朋友,感兴趣的伙伴们可挑战一下自我,在不看答案解析的情况,测试测试自己的解题水平,这样也能达到事半功倍的效果!(好东西要大家一起看才香)

image

image

本文已被CODING开源项目:【一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码】收录

需要这份系统化的资料的朋友,可以点击这里获取

的现象

10-5 饥饿

一个线程由于优先级太低,始终得不到 CPU 调度执行,也不能够结束,这种现象就是饥饿

11.ReentrantLock


11-1 特点+语法

ReentrantLock相对于 synchronized 它具备如下特点:

  • 可中断

  • 可以设置超时时间

  • 可以设置为公平锁

  • 支持多个条件变量(具有多个waitset

synchronized 一样,都支持可重入

基本语法:

// 获取锁

reentrantLock.lock();

try {

// 临界区

} finally {

// 释放锁

reentrantLock.unlock();

}

11-2 可重入

  • 可重入是指同一个线程如果首次获得了这把锁,那么因为它是这把锁的拥有者,因此有权利再次获取这把锁

  • 如果是不可重入锁,那么第二次获得锁时,自己也会被锁挡住

锁重入例子:

@Slf4j

public class Test29 {

private static ReentrantLock lock = new ReentrantLock();

public static void main(String[] args) {

lock.lock();

try {

log.debug(“enter main”);

m1();

} finally {

lock.unlock();

}

}

public static void m1() {

lock.lock();

try {

log.debug(“enter m1”);

m2();

} finally {

lock.unlock();

}

}

public static void m2() {

lock.lock();

最后

这份《“java高分面试指南”-25分类227页1000+题50w+字解析》同样可分享给有需要的朋友,感兴趣的伙伴们可挑战一下自我,在不看答案解析的情况,测试测试自己的解题水平,这样也能达到事半功倍的效果!(好东西要大家一起看才香)

[外链图片转存中…(img-sh81Xm32-1715589620390)]

[外链图片转存中…(img-qw1s9uuV-1715589620391)]

本文已被CODING开源项目:【一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码】收录

需要这份系统化的资料的朋友,可以点击这里获取

  • 17
    点赞
  • 30
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值