并发编程常见的锁
🧱 并发编程三要素
并发编程需要保证三个核心特性,才能避免线程安全问题:
| 特性 | 定义 | 核心问题 |
| 原子性 | 一个或多个操作要么全部执行成功,要么全部执行失败,期间不能被中断 | 线程切换会打断操作,导致数据不一致 |
| 有序性 | 程序执行顺序与代码顺序一致,避免处理器指令重排序导致逻辑混乱 | 指令重排可能破坏代码逻辑 |
| 可见性 | 一个线程对共享变量的修改,其他线程能立刻看到 | 多线程缓存导致修改不及时同步 |
🔒 Java 常见锁种类分类
一、悲观锁 vs 乐观锁
表格
| 类型 | 核心思想 | 实现示例 | 适用场景 |
| 悲观锁 | 认为数据一定会被其他线程修改,每次操作都先上锁,其他线程阻塞等待 | synchronized、ReentrantLock | 写操作多的场景 |
| 乐观锁 | 认为数据不会被修改,更新时才判断是否被改动,通过版本 / CAS 保证同步 | CAS(如 AtomicInteger)、数据库乐观锁 | 读操作多的场景,吞吐量更高 |
二、公平锁 vs 非公平锁
表格
| 类型 | 核心思想 | 实现示例 | 特点 |
| 公平锁 | 线程按申请锁的顺序获取锁(FIFO),保证每个线程都能拿到锁 | ReentrantLock(true) | 无线程饥饿,但吞吐量略低 |
| 非公平锁 | 线程随机抢占锁,可能导致某些线程长期拿不到锁(饥饿) | synchronized、ReentrantLock(false) | 吞吐量更高,但存在饥饿风险 |
三、独享锁(互斥锁) vs 共享锁
| 类型 | 别名 | 核心特性 | 读写权限 |
| 独享锁 | 排它锁 / 写锁 | 一次只能被一个线程持有,其他线程加锁会阻塞 | 可读可写 |
| 共享锁 | S 锁 / 读锁 | 可被多个线程持有,只允许读不允许写 | 只读,不可修改 |
四、可重入锁 vs 不可重入锁
| 类型 | 核心特性 | 实现示例 | 优势 |
| 可重入锁 | 外层加锁后,内层可再次获取同一锁,不会死锁 | synchronized、ReentrantLock | 避免嵌套调用时的死锁 |
| 不可重入锁 | 已持有锁的线程再次获取时会被阻塞 | 早期锁实现 | 容易产生死锁,现已较少使用 |
五、自旋锁 & 分段锁 & 死锁
-
自旋锁:线程获取锁失败时,循环重试(不进入阻塞态),减少上下文切换开销,但会消耗 CPU。
-
分段锁:将数据分段加锁,细化锁粒度,提升并发量(如 ConcurrentHashMap 底层实现)。
-
死锁:多个线程互相等待对方释放资源,导致无限阻塞,需通过破坏死锁四大条件避免。
💡 核心总结
-
写多场景:用悲观锁(如 synchronized)保证数据安全。
-
读多场景:用乐观锁(如 CAS)或共享锁提升吞吐量。
-
避免饥饿:公平锁可保证线程公平获取,但会牺牲部分性能。
-
避免死锁:优先使用可重入锁,控制锁的获取顺序。
ReentrantLock公平锁和非公平锁设计剖析
🧵 ReentrantLock 公平锁 vs 非公平锁 面试核心知识点
一、ReentrantLock 是公平锁还是非公平锁?
-
可配置:ReentrantLock 通过构造参数控制,既可以是公平锁,也可以是非公平锁
// 非公平锁(默认) ReentrantLock lock1 = new ReentrantLock(); // 公平锁 ReentrantLock lock2 = new ReentrantLock(true);
-
底层实现:内部类 Sync 继承 AQS,分为 FairSync(公平锁)和 NonfairSync(非公平锁)两个子类
-
默认行为:默认使用非公平锁
-
核心区别:公平锁的 lock() 方法多了 hasQueuedPredecessors() 限制,保证先到先得
二、什么是「锁饥饿」?
当多个线程争抢锁时:
-
部分线程一直能抢到锁
-
另一些线程(通常优先级较低)长期得不到 CPU 调度,永远拿不到锁
-
这种现象就是 锁饥饿,非公平锁更容易引发
三、公平锁 vs 非公平锁 优缺点对比
| 维度 | 公平锁 | 非公平锁 |
| 核心逻辑 | 线程按申请顺序(FIFO)获取锁 | 线程随机抢占锁 |
| 锁饥饿 | 无,保证每个线程都能拿到锁 | 可能存在,低优先级线程易饥饿 |
| 性能 | 较低(涉及线程休眠 / 恢复、用户态 / 内核态切换) | 更高(避免线程休眠恢复,CPU 利用率更高) |
| 适用场景 | 需保证线程公平获取锁的场景 | 追求高吞吐量、业务性能优先的场景 |
四、为什么 Java 锁默认用非公平锁?
-
性能优先:非公平锁避免了线程休眠和恢复的开销,CPU 利用率更高,吞吐量更大
-
设计哲学:Java 锁默认更看重性能,公平锁则更看重锁资源的平均分配
-
结论:两者各有优劣,需根据业务实际场景选择:
-
追求公平、避免饥饿 → 用公平锁
-
追求性能、高并发 → 用非公平锁
-
💡 面试回答模板
面试官:ReentrantLock 是公平锁还是非公平锁?
-
ReentrantLock 可以通过构造参数控制,既支持公平锁也支持非公平锁,默认是非公平锁。
-
它内部通过继承 AQS 的 FairSync 和 NonfairSync 实现两种模式,公平锁会用 hasQueuedPredecessors() 保证先到先得。
面试官:什么是锁饥饿?
-
锁饥饿是指在多线程争抢锁时,部分线程长期无法获取锁,一直处于等待状态,本质是线程调度和锁抢占机制导致的资源分配不均。
面试官:为什么设计公平锁和非公平锁?
-
公平锁保证线程按顺序获取锁,避免饥饿,但性能较低,因为涉及线程休眠和恢复;
-
非公平锁性能更高,CPU 利用率更好,但可能产生锁饥饿;
-
Java 默认用非公平锁,是因为业务场景更看重吞吐量,公平锁则用于需要严格保证公平性的场景。
synchoronized对象锁和类锁性能差异实践
一、核心概念区分
| 锁类型 | 关键字位置 | 锁对象 | 作用范围 | 核心特点 |
| 对象锁(实例锁) | synchronized 加在实例方法上 | 当前对象实例(this) | 同一个对象内部的同步方法 | 多线程访问不同对象时互不干扰 |
| 类锁(静态锁) | synchronized 加在静态方法上 或 synchronized(XXX.class) | 类的 Class 对象(如 Computer.class) | 该类所有实例的静态同步代码块 | 多线程访问该类任何对象时都需竞争同一把锁 |
二、典型场景对比
对象锁(synchronized method)
public synchronized void watchMovie() { // 对象锁 System.out.println("看电影"); }
示例:
效果:同一时刻,同一个 Computer 对象只能有一个线程执行 watchMovie;不同对象的线程可并行执行。
类锁(static synchronized method)
public static synchronized void watchXdclass() { // 类锁 System.out.println("学课程"); }
示例:
效果:同一时刻,无论创建了多少个 Computer 对象,只有一个线程能执行该静态方法。
混合使用(类锁 + 对象锁)
// 线程1持有类锁(3秒) new Thread(computer::watchXdclass, "冰冰").start(); // 线程2同时可执行对象锁方法(无阻塞) new Thread(computer::watchMovie, "大利").start();
类锁与对象锁是两把不同的锁,可以同时持有,互不影响。
示例:
三、7 大案例问题结论
| 问题 | 结论 | 原因 |
| 1. 同对象的 synchronized method | 串行执行 | 共用同一把对象锁 |
| 2. 同对象,一个带睡眠的 synchronized method | 后执行的线程需等待 | 同一对象锁被占用,阻塞等待 |
| 3. 两个对象的 synchronized method | 并行执行 | 各自拥有独立对象锁,无竞争 |
| 4. 同类的 static synchronized method | 串行执行 | 共用同一把类锁 |
| 5. 同类,一个带睡眠的 static synchronized method | 后执行线程需等待 | 类锁被占用,阻塞等待 |
| 6. 两个类对象的 static synchronized method | 串行执行 | 类锁属于类,所有实例共享 |
| 7. 同类对象,混合 static 与 实例方法 | 可并行执行 | 类锁 & 对象锁 是两把独立的锁 |
四、阿里巴巴《Java 开发手册》核心强制规范
锁粒度最小化
能用无锁数据结构,就不要用锁;
能用对象锁,就不要用类锁;
能锁代码块,就不要锁整个方法。
原因:减少锁竞争范围,降低锁持有时间,提升高并发场景的吞吐量。
锁代码块与 RPC 调用
尽可能使加锁的代码块工作量最小;
严禁在锁代码块中调用 RPC 方法(远程调用耗时高,会长期持有锁,导致系统阻塞)。
多资源加锁顺序
同时对多个资源、数据库表、对象加锁时,保持一致的加锁顺序;
否则可能造成死锁。
💡 面试高频回答模板
问:对象锁和类锁有什么区别?
对象锁针对实例方法,锁的是当前对象,不同对象之间互不影响;
类锁针对静态方法,锁的是Class 对象,所有实例共享同一把锁。
它们是两把独立的锁,可以同时使用,互不干扰。
问:为什么高并发场景要尽量缩小锁粒度?
锁粒度越大,竞争越激烈,阻塞等待的线程越多,吞吐量越低;
缩小锁粒度可减少锁持有时间,提升并发性能;
但需注意:不要在锁内做 RPC 等耗时操作。
🎯 总结
-
优先对象锁:适用于大多数业务场景,并发性能更好。
-
谨慎类锁:适用于全局状态控制、静态资源管理等场景,可能引发性能瓶颈。
-
核心原则:能锁对象就不锁类,能锁代码块就不锁方法,减少锁竞争。
synchronized底层锁原理分析实战
🧵 synchronized 核心原理与字节码实现
一、synchronized 本质定位
synchronized 是 Java 解决线程安全的核心关键字,本质是非公平、可重入的悲观锁:
-
每个对象都有一个锁(monitor)和等待队列
-
锁只能被一个线程持有,其他线程需阻塞等待
-
锁释放后,从等待队列唤醒线程,唤醒顺序不确定,不保证公平性
-
支持可重入:同一线程可多次获取同一把锁
二、两种使用形式 & 底层实现
synchronized 有两种写法,底层都依赖 monitor(管程) 实现同步:
| 形式 | 字节码表现 | 核心逻辑 |
| 同步方法 | 字节码多 ACC_SYNCHRONIZED 标志位 | 线程访问方法时,检查标志位 → 获取 monitor → 执行方法 → 释放 monitor(隐式同步) |
| 同步代码块 | 字节码多 monitorenter / monitorexit 指令 | 进入代码块执行 monitorenter(计数器 + 1),退出执行 monitorexit(计数器 – 1),计数器为 0 时释放 monitor(显式同步) |
关键细节:
-
monitor 维护一个计数器:未被持有 → 0;同一线程重入 → 计数器递增
-
只有计数器为 0 时,monitor 才会被释放,其他线程可竞争获取
-
两种形式本质无区别,只是方法级同步是隐式实现,无需手动写字节码指令
三、查看字节码的方法
编译生成 .class 文件:
javac XXX.java
反编译查看详细字节码:
javap -v XXX.class
四、核心总结
-
底层依赖 monitor:无论是方法级还是代码块级,最终都是通过对象的 monitor 实现互斥访问
-
可重入性:通过 monitor 计数器实现,同一线程多次加锁不会死锁
-
非公平性:唤醒等待线程时随机选择,不保证先到先得
-
悲观锁:默认认为数据会被修改,加锁后独占访问
💡 面试高频考点
问:synchronized 是公平锁吗?
不是,它是非公平锁,锁释放后唤醒线程的顺序是随机的,不保证等待时间最长的线程先获取锁。
问:synchronized 为什么可重入?
因为 monitor 维护了一个计数器,同一线程每次获取锁时计数器 + 1,释放时 – 1,计数器为 0 才释放锁,避免了嵌套调用时的死锁。
问:同步方法和同步代码块底层有区别吗?
没有本质区别,都是基于 monitor 实现,只是同步方法通过 ACC_SYNCHRONIZED 标志位隐式实现,同步代码块通过 monitorenter/monitorexit 显式实现。
多线程里面的死锁案例和排查工具实战
🧨 死锁(DeadLock)核心原理、排查与解决方案
一、什么是死锁?
死锁是指两个或两个以上的线程在执行过程中,因竞争资源或彼此通信等待而造成的一种阻塞现象。
-
本质:多个线程互相持有对方需要的锁,形成循环等待,若无外力干预,程序将永远阻塞无法继续执行。
-
线上风险:死锁发生诡异,且难以通过日志排查,是高并发系统的 “隐形杀手”。
二、死锁的 4 个必要条件(破坏其一即可避免死锁)
只有 4 个条件同时满足,才会发生死锁。只要破坏任意一个,死锁就不会发生。
| 必要条件 | 核心解释 |
| 互斥条件 | 资源是独占的,同一时刻只能被一个线程持有(如一把锁只能被一个线程拿到)。 |
| 请求与保持条件 | 线程已经持有了一部分锁,同时又请求其他线程持有的锁,且不释放已占有的锁。 |
| 不可抢占条件 | 资源一旦被线程拿到,不能被系统强制回收,只能由线程自己执行完释放。 |
| 循环等待条件 | 多个线程形成环形依赖:线程 1 等线程 2 的锁,线程 2 等线程 3 的锁,线程 3 又等线程 1 的锁。 |
三、案例代码:经典死锁场景
public class DeadLockDemo { // 两个共享资源(对象锁) private static Object lockA = new Object(); private static Object lockB = new Object(); public void methodA() { synchronized (lockA) { // 线程1 获取 lockA System.out.println("我是A方法中获得了锁A"); try { Thread.sleep(2000); // 模拟业务耗时,保持锁不释放 } catch (InterruptedException e) { e.printStackTrace(); } // 尝试获取 lockB,但 lockB 可能被线程2占用 synchronized (lockB) { System.out.println("我是A方法中获得了锁B"); } } } public void methodB() { synchronized (lockB) { // 线程2 获取 lockB System.out.println("我是B方法中获得了锁B"); try { Thread.sleep(2000); // 模拟业务耗时 } catch (InterruptedException e) { e.printStackTrace(); } // 尝试获取 lockA,但 lockA 可能被线程1占用 synchronized (lockA) { System.out.println("我是B方法中获得了锁A"); } } } }
死锁原因:线程 1 持有 lockA 等待 lockB,线程 2 持有 lockB 等待 lockA,形成循环等待。
四、死锁的常见解决办法
调整申请锁的范围(缩小粒度)
-
不要在锁代码块中调用耗时操作(如 RPC、IO),缩短锁的持有时间
-
尽可能只对最小需要同步的代码块加锁,而不是整个方法
调整申请锁的顺序(破坏循环等待)
-
核心原则:让所有线程按照相同的顺序获取锁
-
示例:如果线程必须同时获取锁 A 和锁 B,那么所有线程都必须先申请锁 A,再申请锁 B。
// 正确写法:统一按顺序获取锁 synchronized (lockA) { synchronized (lockB) { // 业务逻辑 } }
定时锁等待(破坏不可抢占)
-
使用 ReentrantLock.tryLock(timeout) 替代内置锁,超时后自动释放锁或退出。
做好代码 Review
-
高并发场景下,避免嵌套同步、多资源混合加锁。
五、死锁排查方式(面试必问)
方式一:JPS + Jstack(命令行神器)
查找进程 ID:
jps
找到运行中的 Java 进程号(PID)。
生成堆栈快照:
jstack <PID>
输出中会看到类似以下的 死锁提示:
Found one Java-level deadlock: ============================= "Thread-1": waiting to lock monitor 0x00000000xxxx (object 0x00000000xxxx, a java.lang.Object), which is held by "Thread-0" "Thread-0": waiting to lock monitor 0x00000000xxxx (object 0x00000000xxxx, a java.lang.Object), which is held by "Thread-1"
方式二:JConsole(图形化工具)
-
打开命令行输入 jconsole 连接进程。
-
切换到 【线程】 标签页,会直接显示 【检测到死锁】 提示,并展示死锁线程详情。
💡 核心总结
-
死锁本质:互斥、循环等待、不可抢占、请求保持,四条件同时成立。
-
解决核心:控制锁的获取顺序(保持一致)是最高效的手段。
-
排查手段:生产环境优先使用 jps + jstack 或 JConsole 快速定位。

