文章目录
说说Java“锁“事
大厂面试题复盘
并发编程高级面试解析
一、Synchronized 相关问题
- Synchronized 用过吗,其原理是什么?
- 你刚才提到获取对象的锁,这个"锁"到底是什么? 如何确定对象的锁?
- 什么是可重入性,为什么说 Synchronized 是可重入锁?
- JVM 对 Java 的原生锁做了哪些优化?
- 为什么说 Synchronized 是非公平锁?
- 什么是锁消除和锁粗化?
- 为什么说 Synchronized 是一个悲观锁? 乐观锁的实现原理又是什么? 什么是 CAS ,它有
- 乐观锁一定就是好的吗
二、可重入锁 ReentrantLock 及其它显式锁相关问题
- 跟 Synchronized 相比,可重入锁 其实现原理有什么不同?
- 那么请谈谈 AQS 框架是怎么回事儿?
- 请尽可能详尽地对比下 Synchronized 和 ReentrantLock 的异同
- ReentrantLock 是如何实现可重入性的?
三、其它
- 你怎么理解 Java 语言多线程的? 怎么处理并发? 线程池有那几个核心参数?
- Java 加锁有哪儿种锁? 我先说 synchronized,刚讲到偏向锁,他就不让我讲了,太自信了
- 简单说说锁?
- hashmap的实现原理? hash冲突怎么解决? 为什么使用红黑树?
- spring 里面都使用了哪些设计模式? 循环依赖怎么解决?
- 项目中哪个地方用了 CountDownLatch,怎么使用的?
从轻松的乐观锁和悲观锁开讲
悲观锁
认为自己在使用数据的时候一定有别的线程来修改数据,因此在获取数据的时候会先加锁,确保数据不会被别的线程修改
synchronized和Lock的实现类都是悲观锁
适合写操作多的场景,先加锁可以保证写操作时数据正确,显示的锁定之后再操作同步资源
一句话定义:狼性锁
乐观锁
认为自己在使用数据的时候不会有别的线程修改数据或资源,所以不会添加锁
Java中使用无锁编程来实现,只是在更新的时候去判断,之前有没有别的线程更新了这个数据
- 如果这个数据没有被更新,当前线程将自己修改的数据成功写入
- 如果这个数据已经被其他线程更新,则根据不同的实现方式执行不同的操作,比如:放弃修改、重试抢锁等等
判断规则
- 版本号机制Version
- 最常采用的是CAS算法,Java原子类中的递增操作就通过CAS自旋实现的
适合读操作多的场景,不加锁的特性能够使其读操作的性能大幅提升
乐观锁则直接去操作同步资源,是一种无锁算法,得之我幸不得我命,再努力就是
一句话定义:佛系锁
乐观锁一般有两种实现方式:
- 采用Version版本号机制
- CAS(Compare-and-Swap,即比较并替换)算法实现
伪代码说明
通过8种情况演示锁运行案例,看看我们到底锁的是什么
锁相关的8种案例演示code
阿里巴巴Java开发手册
Code实验
package com.bilibili.juc.lock;
import java.util.concurrent.TimeUnit;
/**
* 题目:谈谈你对多线程锁的理解,8锁案例说明
* 口诀:线程 操作 资源类
* 8锁案例说明:
* 1. 标准访问ab两个线程,请问先打印邮件还是短信? --------先邮件,后短信 共用一个对象锁
* 2. sendEmail钟加入暂停3秒钟,请问先打印邮件还是短信?--------先邮件,后短信 共用一个对象锁
* 3. 添加一个普通的hello方法,请问先打印普通方法还是邮件? --------先hello,再邮件 资源没有争抢,hello方法没有用到对象锁
* 4. 有两部手机,请问先打印邮件还是短信? --------先短信后邮件 资源没有争抢,不是同一个对象锁
* 5. 有两个静态同步方法,一部手机, 请问先打印邮件还是短信?--------先邮件后短信 共用一个类锁
* 6. 有两个静态同步方法,两部手机, 请问先打印邮件还是短信? --------先邮件后短信 共用一个类锁
* 7. 有一个静态同步方法,一个普通同步方法,一部手机,请问先打印邮件还是短信? --------先短信后邮件 一个类锁一个对象锁
* 8. 有一个静态同步方法,一个普通同步方法,两部手机,请问先打印邮件还是短信? ---------先短信后邮件 一个类锁一个对象锁
*/
public class Lock8Demo {
public static void main(String[] args) {
Phone phone = new Phone();
Phone phone2 = new Phone();
new Thread(() -> phone.sendEmail(), "a").start();
// 暂停200毫秒,保证线程先启动
try {
TimeUnit.MILLISECONDS.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(() -> /*phone.sendSMS()*/ /*phone.hello()*/ phone2.sendSMS(), "b").start();
}
}
// 资源类
class Phone {
public static synchronized void sendEmail() {
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("--------sendEmail--------");
}
public /*static*/ synchronized void sendSMS() {
System.out.println("--------sendSMS--------");
}
public void hello() {
System.out.println("--------hello--------");
}
}
案例总结
1-2:
一个对象里面如果有多个 synchronized 方法,某一个时刻内,只要一个线程去调用其中的一个synchronized方法,其它线程都只能等待。换句话说,某一个时刻内,只能有唯一的一个线程去访问这些synchronized方法。锁的是当前对象this,被锁定后,其它线程都不能进入到当前对象的其它的synchronized方法
3-4:
加个普通方法后发现和同步锁无关
换成两个对象后,不是同一把锁了,情况立即变化
5-6: 都换成静态同步方法后,情况又变化
三种synchronized锁的内容有一些差别:
- 对于普通同步方法,锁的是当前实例对象,通常指this,所有的同步方法用的都是同一把锁—>实例对象本身(即Phone phone = new Phone();)
- 对于静态同步方法,锁的是当前类的Class对象,即Phone.class唯一的一个模板
- 对于同步方法块,锁的是synchronized括号内的对象
7-8
当一个线程试图访问同步代码时,它首先必须得到锁,正常退出或抛出异常时必须释放锁
所有的普通同步方法用的都是同一把锁——实例对象本身,就是new出来的具体实例对象本身,本类this。也就是说如果一个实例对象的普通同步方法获取锁后,该实例对象的其它普通同步方法必须等待获取锁的方法释放锁后才能获取锁
所有的静态方法用的也是同一把锁——类对象本身,就是我们说过的唯一模板class。具体实例对象this和唯一模板class,这两把锁是两个不同的对象,所以静态同步方法和普通同步方法之间是不会有竞态条件的。但是一旦一个静态同步方法获取锁后,其它的静态同步方法都必须等待该方法释放锁后才能获取锁
解释说明-小总结
synchronized有三种应用方式
JDK源码(notify方法)说明举例
8种锁的案例实际体现在3个地方
- 作用于实例方法,当前实例加锁,进入同步代码前要获得当前实例的锁
- 作用于代码块,对括号里配置的对象加锁
- 作用于静态方法,当前类加锁,进去同步代码前要获得当前类对象的锁
从字节码角度分析synchronized实现
javap -c ***.class 文件反编译
javap -c ***.class:对代码进行反编译
假设你需要更多信息:javap -v ***.class 文件反编译,-v代表-verbose:输出附加信息(包括行号、本地变量表、反汇编等详细信息)
synchronized同步代码块
Code
package com.bilibili.juc.lock.sync;
public class LockSyncDemo {
Object object = new Object();
public void m1() {
synchronized (object) {
System.out.println("--------hello synchronized code block--------");
}
}
public static void main(String[] args) {
}
}
反编译
通过javap -c LockSyncDemo.class反编译
实现使用的是monitorenter和monitorexit指令
一定是一个enter两个exit吗?
一般情况下是一个enter对应两个exit
极端情况:m1方法里面自己添加一个异常试试
package com.bilibili.juc.lock.sync;
public class LockSyncDemo {
Object object = new Object();
public void m1() {
synchronized (object) {
System.out.println("--------hello synchronized code block--------");
throw new RuntimeException("--------exp--------");
}
}
public static void main(String[] args) {
}
}
反编译结果:一个enter对应一个exit
synchronized普通同步方法
Code
package com.bilibili.juc.lock.sync;
public class LockSyncDemo2 {
public synchronized void m2() {
System.out.println("--------hello synchronized m2--------");
}
public static void main(String[] args) {
}
}
反编译
通过javap -v LockSyncDemo2.class反编译
小总结
synchronized普通同步方法调用指令将会检查方法的ACC_SYNCHRONIZED访问标志是否被设置,如果设置了,执行线程会将现持有monitor锁,然后再执行该方法,最后在方法完成(无论是否正常结束)时释放monitor
synchronized静态同步方法
Code
package com.bilibili.juc.lock.sync;
public class LockSyncDemo3 {
public synchronized void m2() {
System.out.println("--------hello synchronized m2--------");
}
public static synchronized void m3() {
System.out.println("--------hello static synchronized m3--------");
}
public static void main(String[] args) {
}
}
反编译
通过javap -v LockSyncDemo3.class反编译
小总结
ACC_STATIC、ACC_SYNCHRONIZED访问标志区分该方法是否是静态同步方法
反编译synchronized锁的是什么
面试题:为什么任何一个对象都可以成为一个锁?
Code
package com.bilibili.juc.lock.sync;
public class LockSyncDemo4 {
Object object = new Object();
Book book = new Book();
public void m1() {
synchronized (book) {
System.out.println("--------hello synchronized code block--------");
}
}
public static void main(String[] args) {
}
}
class Book /*extends Object // 任何一个对象都默认继承Object*/ {
}
提出疑问
我们知道在Java中每一个对象都继承Object类,可写可不写。但是为什么任何一个对象都可以成为一个锁?下面通过C底层源码进一步了解为什么任何一个对象都可以成为一个锁?
什么是管程monitor
大厂面试题讲解
一、Java集合类
- 从集合开始吧,介绍一下常用的集合类,哪些是有序的,哪些是无序的
- hashmap是如何寻址的,哈希碰撞后是如何存储数据的,1.8后什么时候变成红黑树、说下红黑理,红黑树有什么好处
- concurrrenthashmap 怎么实现线程安全,一个里面会有几个段 segment,jdk1.8后有优化concurrenthashmap吗? 分段锁有什么坏处
二、多线程JUC
- 重入锁实现原理,简单说下aqs
- synchronized实现原埋,monitor对像什么时候生成的? 知道monitor的monitorenter和monitexit这两个是怎么保证同步的吗,或者说,这两个操作计算机底层是如何执行的
- 刚刚你提到了同步的优化过程,详细说一下吧。偏向锁和轻量级锁有什么区别?
- 线程池几个参数说下,你们项目中如何根据实际场景设置参数的,为什么CPU密集设置的线程数集型少
管程
管程(英语:monitor,也称为监视器)是一种程序结构,结构内的多个子程序(对象或模块)形成的多个工作线程互斥访问共享资源。这些共享资源一般是硬件设备或一群变量。对共享变量能够进行的所有操作集中在一个模块中。(把信号量及其操作原语“封装”在一个对象内部)管程实现了在一个时间点,最多只有一个线程在执行管程的某个子程序。管程提供了一种机制,管程可以看做一个软件模块。它是将共享的变量和对于这些共享变量的操作封装起来,形成一个具有一定接口的功能模块,进程可以调用管程来实现进程级别的并发控制。
结合《JVM虚拟机》这本书
上述C++源码解读
在HotSpot虚拟机中,monitor采用ObjectMonitor实现
在Java中任何一个对象都继承Object类,对应C++源码底层的源码类:ObjectMonitor.java—>ObjectMonitor.cpp—>ObjectMonitor.hpp
ObjectMonitor.hpp类初始化monitor代码:
为什么任何一个对象都可以成为一个锁?因为每个对象天生都带着一个对象监视器,每一个被锁住的对象都会和Monitor关联起来
对于synchronized关键字后面章节详说
提前剧透,混个眼熟
synchronized必须作用于某个对象中,所以Java在对象的头文件存储了锁的相关信息。锁升级功能主要依赖于 MarkWord 中的锁标志位和释放偏向锁标志位,后续讲解锁升级时候我们再加深,目前为了承前启后的学习,对下图先混个眼熟即可,O(n_n)O
Hotspot的实现
公平锁和非公平锁
从ReentrantLock卖票demo演示公平和非公平现象
非公平锁Code
package com.bilibili.juc.lock.fair;
import java.util.concurrent.locks.ReentrantLock;
/**
* 卖票案例:模拟三个售票员卖完50张票
*/
public class SaleTicketDemo {
public static void main(String[] args) {
Ticket ticket = new Ticket();
new Thread(() -> {
for (int i = 0; i < 55; i++) {
ticket.sale();
}
}, "a").start();
new Thread(() -> {
for (int i = 0; i < 55; i++) {
ticket.sale();
}
}, "b").start();
new Thread(() -> {
for (int i = 0; i < 55; i++) {
ticket.sale();
}
}, "c").start();
}
}
class Ticket {
private int number = 50;
ReentrantLock lock = new ReentrantLock();
public void sale() {
lock.lock();
try {
if (number > 0) {
System.out.println(Thread.currentThread().getName() + "卖出第:\t" + (number--) + "\t 还剩下:" + number);
}
} finally {
lock.unlock();
}
}
}
输出结果:
a卖出第: 50 还剩下:49
a卖出第: 49 还剩下:48
a卖出第: 48 还剩下:47
a卖出第: 47 还剩下:46
a卖出第: 46 还剩下:45
a卖出第: 45 还剩下:44
a卖出第: 44 还剩下:43
a卖出第: 43 还剩下:42
a卖出第: 42 还剩下:41
a卖出第: 41 还剩下:40
a卖出第: 40 还剩下:39
a卖出第: 39 还剩下:38
a卖出第: 38 还剩下:37
a卖出第: 37 还剩下:36
a卖出第: 36 还剩下:35
a卖出第: 35 还剩下:34
a卖出第: 34 还剩下:33
a卖出第: 33 还剩下:32
c卖出第: 32 还剩下:31
c卖出第: 31 还剩下:30
c卖出第: 30 还剩下:29
c卖出第: 29 还剩下:28
c卖出第: 28 还剩下:27
c卖出第: 27 还剩下:26
c卖出第: 26 还剩下:25
c卖出第: 25 还剩下:24
c卖出第: 24 还剩下:23
c卖出第: 23 还剩下:22
c卖出第: 22 还剩下:21
c卖出第: 21 还剩下:20
c卖出第: 20 还剩下:19
c卖出第: 19 还剩下:18
c卖出第: 18 还剩下:17
c卖出第: 17 还剩下:16
c卖出第: 16 还剩下:15
c卖出第: 15 还剩下:14
c卖出第: 14 还剩下:13
c卖出第: 13 还剩下:12
c卖出第: 12 还剩下:11
c卖出第: 11 还剩下:10
c卖出第: 10 还剩下:9
c卖出第: 9 还剩下:8
c卖出第: 8 还剩下:7
c卖出第: 7 还剩下:6
c卖出第: 6 还剩下:5
c卖出第: 5 还剩下:4
c卖出第: 4 还剩下:3
c卖出第: 3 还剩下:2
c卖出第: 2 还剩下:1
c卖出第: 1 还剩下:0
公平锁Code
package com.bilibili.juc.lock.fair;
import java.util.concurrent.locks.ReentrantLock;
/**
* 卖票案例:模拟三个售票员卖完50张票
*/
public class SaleTicketDemo {
public static void main(String[] args) {
Ticket ticket = new Ticket();
new Thread(() -> {
for (int i = 0; i < 55; i++) {
ticket.sale();
}
}, "a").start();
new Thread(() -> {
for (int i = 0; i < 55; i++) {
ticket.sale();
}
}, "b").start();
new Thread(() -> {
for (int i = 0; i < 55; i++) {
ticket.sale();
}
}, "c").start();
}
}
class Ticket {
private int number = 50;
ReentrantLock lock = new ReentrantLock(true);
public void sale() {
lock.lock();
try {
if (number > 0) {
System.out.println(Thread.currentThread().getName() + "卖出第:\t" + (number--) + "\t 还剩下:" + number);
}
} finally {
lock.unlock();
}
}
}
输出结果:
a卖出第: 50 还剩下:49
a卖出第: 49 还剩下:48
a卖出第: 48 还剩下:47
a卖出第: 47 还剩下:46
a卖出第: 46 还剩下:45
a卖出第: 45 还剩下:44
a卖出第: 44 还剩下:43
a卖出第: 43 还剩下:42
b卖出第: 42 还剩下:41
a卖出第: 41 还剩下:40
c卖出第: 40 还剩下:39
b卖出第: 39 还剩下:38
a卖出第: 38 还剩下:37
c卖出第: 37 还剩下:36
b卖出第: 36 还剩下:35
a卖出第: 35 还剩下:34
c卖出第: 34 还剩下:33
b卖出第: 33 还剩下:32
a卖出第: 32 还剩下:31
c卖出第: 31 还剩下:30
b卖出第: 30 还剩下:29
a卖出第: 29 还剩下:28
c卖出第: 28 还剩下:27
b卖出第: 27 还剩下:26
a卖出第: 26 还剩下:25
c卖出第: 25 还剩下:24
b卖出第: 24 还剩下:23
a卖出第: 23 还剩下:22
c卖出第: 22 还剩下:21
b卖出第: 21 还剩下:20
a卖出第: 20 还剩下:19
c卖出第: 19 还剩下:18
b卖出第: 18 还剩下:17
a卖出第: 17 还剩下:16
c卖出第: 16 还剩下:15
b卖出第: 15 还剩下:14
a卖出第: 14 还剩下:13
c卖出第: 13 还剩下:12
b卖出第: 12 还剩下:11
a卖出第: 11 还剩下:10
c卖出第: 10 还剩下:9
b卖出第: 9 还剩下:8
a卖出第: 8 还剩下:7
c卖出第: 7 还剩下:6
b卖出第: 6 还剩下:5
a卖出第: 5 还剩下:4
c卖出第: 4 还剩下:3
b卖出第: 3 还剩下:2
a卖出第: 2 还剩下:1
c卖出第: 1 还剩下:0
何为公平锁/非公平锁
- 公平锁:是指多个线程按照申请锁的顺序来获取锁,这里类似于排队买票,先来的人先买,后来的人再队尾排着,这是公平的
Lock lock = new ReentrantLock(true):表示公平锁,先来先得 - 非公平锁:是指多个线程获取锁的顺序并不是按照申请的顺序,有可能后申请的线程比先申请的线程优先获取锁,在高并发环境下,有可能造成优先级反转或者饥饿的状态(某个线程一直得不到锁)
Lock lock = new ReentrantLock(false):表示非公平锁,后来的也可能先获得锁,默认为非公平锁
Lock lock = new ReentrantLock():默认是非公平锁
面试题
为什么会有公平锁/非公平锁的设计?为什么默认非公平?
- 恢复挂起的线程到真正锁的获取还是有时间差的,从开发人员来看这个时间微乎其微,但是从CPU的角度来看,这个时间差存在的还是很明显的。所以非公平锁能更充分地利用CPU的时间片,尽量减少CPU空间状态时间
- 使用多线程很重要的考量点是线程切换的开销,当采用非公平锁时,当一个线程请求锁获取同步状态,然后释放同步状态,所以刚释放锁的线程在此刻再次获取同步状态的概率就变得很大,所以就减少了线程的开销
什么时候用公平?什么时候用非公平?
- 如果为了更高的吞吐量,很显然非公平锁是比较合适的,因为节省了很多线程切换的时间,吞吐量自然就上去了;否则就用公平锁,大家公平使用
预埋伏AQS
后续深入分析
可重入锁(又名递归锁)
概念说明
可重入锁又名递归锁
是指在同一线程在外层方法获取到锁的时侯,在进入该线程的内层方法会自动获取锁(前提,锁对象的是同一个对象),不会因为之前已经获取过还没释放而阻塞
如果是1个有 synchronized 修饰的递归调用方法,程序第2次进入被自己阻塞了岂不是天大的笑话,出现了作茧自缚
所以Java中的 ReentrantLock 和 synchronized 都是可重入锁,可重入锁的一个优点是可一定程度避免死锁
可重入锁种类
隐式锁(即synchronized关键字使用的锁),默认是可重入锁
指的是可重复可递归调用的锁,在外层使用锁之后,在内层仍然可以使用,并且不发生死锁,这样的锁就叫做可重入锁
简单的来说就是,在一个synchronized修饰的方法或代码块的内部调用本类的其它synchronized修饰的方法或代码块时,是永远可以得到锁的
同步块
package com.bilibili.juc.lock.reentry;
public class ReEntryLockDemo {
public static void main(String[] args) {
final Object o = new Object();
new Thread(() -> {
synchronized (o) {
System.out.println(Thread.currentThread().getName() + "\t --------外层调用--------");
synchronized (o) {
System.out.println(Thread.currentThread().getName() + "\t --------中层调用--------");
synchronized (o) {
System.out.println(Thread.currentThread().getName() + "\t --------内层调用--------");
}
}
}
}, "t1").start();
}
}
输出结果:
t1 --------外层调用--------
t1 --------中层调用--------
t1 --------内层调用--------
同步方法
package com.bilibili.juc.lock.reentry;
public class ReEntryLockDemo2 {
public static void main(String[] args) {
ReEntryLockDemo2 reEntryLockDemo2 = new ReEntryLockDemo2();
new Thread(() -> reEntryLockDemo2.m1(), "t1").start();
}
public synchronized void m1() {
System.out.println(Thread.currentThread().getName() + "\t --------m1 come in--------");
m2();
System.out.println(Thread.currentThread().getName() + "\t --------m1 end--------");
}
public synchronized void m2() {
System.out.println(Thread.currentThread().getName() + "\t --------m2 come in--------");
m3();
}
public synchronized void m3() {
System.out.println(Thread.currentThread().getName() + "\t --------m3 come in--------");
}
}
输出结果:
t1 --------m1 come in--------
t1 --------m2 come in--------
t1 --------m3 come in--------
t1 --------m1 end--------
synchronized的重入的实现原理
之前解释为什么任何一个对象都可以称为一个锁可以看到ObjectMonitor.hpp类中初始化monitor代码,ObjectMonitor对象中的_recursions
和_count
,锁的重入次数和用来记录该线程获取锁的次数
小总结
每个锁对象都拥有一个锁的计数器和一个指向持有该锁的线程的指针
当执行monitorenter时,如果目标锁对象的计数器为零,那么说明它没有被其它线程持有,Java虚拟机会将该锁对象的持有线程设置为当前线程,并且将其计数器加1
在目标对象的计数器不为零的情况下,如果锁对象的持有线程是当前线程,那么Java虚拟机可以将其计数器加1,否则需要等待,直至持有锁的线程释放该锁
当执行monitorexit时,Java虚拟机则需将锁对象的计数器减1。计数器为零代表锁已被释放
显式锁(即Lock)也有 ReentrantLock 这样的可重入锁
Code
package com.bilibili.juc.lock.reentry;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class ReEntryLockDemo3 {
static Lock lock = new ReentrantLock();
public static void main(String[] args) {
new Thread(() -> {
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + "\t --------外层调用come in--------");
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + "\t --------内层调用come in--------");
} finally {
lock.unlock();
}
} finally {
lock.unlock();
}
}, "t1").start();
}
}
输出结果:
t1 --------外层调用come in--------
t1 --------内层调用come in--------
加锁次数和释放锁次数不匹配
package com.bilibili.juc.lock.reentry;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* @ClassName: ReEntryLockDemo2
* @Description:
* @Author: zhangjin
* @Date: 2023/10/16
*/
public class ReEntryLockDemo3 {
static Lock lock = new ReentrantLock();
public static void main(String[] args) {
new Thread(() -> {
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + "\t --------外层调用come in--------");
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + "\t --------内层调用come in--------");
} finally {
lock.unlock();
}
} finally {
// 由于加锁次数和释放次数不一样,第二个线程始终无法获取到锁,导致一直在等待
// lock.unlock();
}
}, "t1").start();
new Thread(() -> {
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + "\t --------外层调用come in--------");
} finally {
lock.unlock();
}
}, "t2").start();
}
}
输出结果:
t1 --------外层调用come in--------
t1 --------内层调用come in--------
// ...程序未结束
小总结
隐式锁(即synchronized关键字使用的锁)天生具备可重入性,显式锁(即Lock)的可重入性,需要注意锁了几次就要释放几次
死锁及排查
概念
死锁是指两个或两个以上的线程在执行过程中,因抢夺资源而造成的一种互相等待的现象,若无外力干涉,则它们无法再继续推进下去。如果系统资源充足,进程的资源请求能够得到满足,死锁出现的可能性很低,否则就会因为争夺有限的资源而陷入死锁
产生原因
- 系统资源不足
- 进程运行推进顺序不合适
- 系统资源分配不当
写一个死锁代码case
package com.bilibili.juc.lock.dead;
import java.util.concurrent.TimeUnit;
public class DeadLockDemo {
final static Object a = new Object();
final static Object b = new Object();
public static void main(String[] args) {
new Thread(() -> {
synchronized (a) {
System.out.println(Thread.currentThread().getName() + "线程持有a锁,试图获取b锁");
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (b) {
System.out.println(Thread.currentThread().getName() + "线程成功获取到b锁");
}
}
},"t1").start();
new Thread(() -> {
synchronized (b) {
System.out.println(Thread.currentThread().getName() + "线程持有b锁,试图获取a锁");
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (a) {
System.out.println(Thread.currentThread().getName() + "线程成功获取到a锁");
}
}
},"t2").start();
}
}
输出结果:
t1线程持有a锁,试图获取b锁
t2线程持有b锁,试图获取a锁
// ...程序未结束
如何排查死锁
纯命令
- jps -l:查看Java进程编号
- jstack 进程编号:查看指定进程编号打印出来的栈信息
图形化
jconsole
写锁(独占锁)/读锁(共享锁)
深度源码分析见后面
自旋锁spinLock
深度源码分析见后面
无锁->独占锁->读写锁->邮戳锁
深度源码分析见后面
无锁->偏向锁->轻量锁->重量锁
深度源码分析见后面
总结【重要】
指针指向Monitor对象(也称为管程或监视器)的真实地址。每个对象都存在着一个monitor与之关联,当一个monitor被某个线程持有后,它便处于锁定状态。在Java虚拟机(HotSpot)中,monitor是由ObjectMonitor实现的,具体图解如下: