syn-ed
锁优化
自旋锁
重量级锁竞争时,尝试获取锁的线程不会立即阻塞,可以使用自旋(默认 10 次)来进行优化,采用循环的方式去尝试获取锁
注意:
- 自旋占用 CPU 时间,单核 CPU 自旋就是浪费时间,因为同一时刻只能运行一个线程,多核 CPU 自旋才能发挥优势
- 自旋失败的线程会进入阻塞状态
优点:不会进入阻塞状态,减少线程上下文切换的消耗
缺点:当自旋的线程越来越多时,会不断的消耗 CPU 资源
自旋锁是什么?
想象一下你去银行办业务:
- 你发现只有一个窗口(相当于“锁”),现在有人正在办理(相当于“线程 A 拿着锁”)。
- 你想办业务(相当于“线程 B 想拿锁”),但窗口被占了。
传统做法(普通锁):你说:“那我先回家等会儿,等叫到我再回来。”
→ 这就是 线程阻塞:你(线程)被挂起,不占 CPU,但等会儿要重新叫你起床(上下文切换),很耗时间。
自旋锁的做法:你说:“我不走,我就站这儿盯着窗口,每秒问一次‘办完了吗?’”这就是 自旋:你一直站着(占用 CPU),不断循环检查锁有没有释放。
但你不能一直问啊!你最多问 10 次,比如:“1次…2次…3次…”,问到第10次还没轮到你,你就说:“算了,我去旁边坐着等叫号吧。”这就是:自旋失败后进入阻塞状态。
什么时候用自旋锁最划算?
多核 CPU:你在1号窗口等,别人在2号窗口办业务,你们互不影响。你“自旋”是在浪费自己的CPU时间,但不影响别人干活。
单核 CPU:只有一个窗口,你站着问“办完了吗”,其实别人根本没法办业务(因为CPU被你占着),你就是在瞎忙,纯浪费时间。
所以,自旋锁只在多核 CPU下才有意义。
编码模拟自旋过程:
public class SpinLock {
// 标志位:谁拿着锁?null 表示没人,Thread 表示某个线程拿着
private Thread owner = null;
// 尝试加锁(自旋)
public void lock() {
Thread current = Thread.currentThread();
// 开始“自旋”:不断尝试
while (!compareAndSet(null, current)) {
// 自旋中... 一直问:“锁释放了吗?”
// 可以加个计数,比如最多自旋10次
// 这里简化,无限自旋,实际JVM有默认限制(如10次)
System.out.println(current.getName() + " 正在自旋等待...");
Thread.yield(); // 礼让一下CPU,避免太霸道
}
System.out.println(current.getName() + " 成功拿到锁!");
}
// 解锁
public void unlock() {
Thread current = Thread.currentThread();
if (owner == current) {
owner = null;
System.out.println(current.getName() + " 释放了锁");
}
}
// 模拟 CAS 操作:只有当前值是 expect 时,才设置为 update
private synchronized boolean compareAndSet(Thread expect, Thread update) {
if (owner == expect) {
owner = update;
return true;
}
return false;
}
}
// 测试类
public class SpinLockDemo {
public static void main(String[] args) {
SpinLock lock = new SpinLock();
Thread t1 = new Thread(() -> {
lock.lock();
try {
System.out.println("t1 正在执行任务...");
Thread.sleep(2000); // 模拟干活
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}, "t1");
Thread t2 = new Thread(() -> {
lock.lock(); // t1 拿着锁,t2 开始自旋
try {
System.out.println("t2 正在执行任务...");
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}, "t2");
t1.start();
t2.start();
}
}
自旋锁情况
自旋成功的情况:

初始状态
- 线程1和线程2都未开始执行。
- 对象的Mark字段为
10(重量锁),表示当前对象处于重量级锁状态,并且有一个指向重量锁的指针。
线程1获取锁并执行同步块
-
线程1访问同步块,获取monitor:
- 线程1尝试获取对象的monitor(即锁)。此时,对象的Mark字段仍然是
10(重量锁)重量锁指针。
- 线程1尝试获取对象的monitor(即锁)。此时,对象的Mark字段仍然是
-
线程1成功加锁:
- 线程1成功获取了锁,可以开始执行同步块中的代码。此时,对象的Mark字段保持不变,因为锁已经被线程1持有。
-
线程1执行同步块:
- 线程1在核心1上执行同步块中的代码。在此期间,对象的Mark字段仍然显示为
10(重量锁)重量锁指针,表明锁被线程1持有。
- 线程1在核心1上执行同步块中的代码。在此期间,对象的Mark字段仍然显示为
线程2尝试获取锁并自旋
-
线程2访问同步块,获取monitor:
- 在线程1执行同步块的过程中,线程2也尝试获取同一个对象的monitor。由于锁已经被线程1持有,线程2无法立即获取锁。
-
线程2自旋重试:
- 线程2进入自旋状态,不断检查对象的Mark字段,看锁是否已被释放。在这个过程中,线程2不会被阻塞,而是占用CPU时间进行循环检查。
-
线程1执行完毕并解锁:
- 线程1完成同步块中的任务后,释放锁。此时,对象的Mark字段变为
01(无锁),表示锁已被释放。
- 线程1完成同步块中的任务后,释放锁。此时,对象的Mark字段变为
自旋成功:线程2获取锁
-
线程2成功加锁:
- 由于线程1已经释放了锁,线程2在自旋过程中检测到锁可用,成功获取了锁。此时,对象的Mark字段再次变为
10(重量锁)重量锁指针,但这次是指向线程2持有的重量锁。
- 由于线程1已经释放了锁,线程2在自旋过程中检测到锁可用,成功获取了锁。此时,对象的Mark字段再次变为
-
线程2执行同步块:
- 线程2开始在核心2上执行同步块中的代码。在此期间,对象的Mark字段保持为
10(重量锁)重量锁指针,表明锁被线程2持有。
- 线程2开始在核心2上执行同步块中的代码。在此期间,对象的Mark字段保持为
总结
通过上述步骤,我们可以看到自旋锁成功的情况:
- 当一个线程(如线程1)持有锁并在执行同步块时,其他线程(如线程2)会进入自旋状态,不断检查锁是否可用。
- 如果持有锁的线程很快释放了锁,自旋中的线程可以立即获取锁,避免了进入阻塞状态和上下文切换的开销。
- 这种机制在多核CPU环境下特别有效,因为它允许线程在等待锁时继续占用CPU资源,而不是被挂起。

初始状态
- 线程1和线程2都未开始执行。
- 对象的Mark字段为
10(重量锁),表示当前对象处于重量级锁状态,并且有一个指向重量锁的指针。
线程1获取锁并执行同步块
-
线程1访问同步块,获取monitor:
- 线程1尝试获取对象的monitor(即锁)。此时,对象的Mark字段仍然是
10(重量锁)重量锁指针。
- 线程1尝试获取对象的monitor(即锁)。此时,对象的Mark字段仍然是
-
线程1成功加锁:
- 线程1成功获取了锁,可以开始执行同步块中的代码。此时,对象的Mark字段保持不变,因为锁已经被线程1持有。
-
线程1执行同步块:
- 线程1在核心1上执行同步块中的代码。在此期间,对象的Mark字段仍然显示为
10(重量锁)重量锁指针,表明锁被线程1持有。
- 线程1在核心1上执行同步块中的代码。在此期间,对象的Mark字段仍然显示为
线程2尝试获取锁并自旋
-
线程2访问同步块,获取monitor:
- 在线程1执行同步块的过程中,线程2也尝试获取同一个对象的monitor。由于锁已经被线程1持有,线程2无法立即获取锁。
-
线程2自旋重试:
- 线程2进入自旋状态,不断检查对象的Mark字段,看锁是否已被释放。在这个过程中,线程2不会被阻塞,而是占用CPU时间进行循环检查。
-
线程2自旋多次但未成功:
- 假设线程2进行了多次自旋重试(例如默认的10次),但在这期间线程1仍未释放锁。因此,线程2的自旋尝试均未成功。
自旋失败:线程2进入阻塞状态
- 线程2阻塞:
- 由于自旋次数达到上限(例如10次)后仍未获取到锁,线程2将停止自旋并进入阻塞状态。此时,线程2会被挂起,等待操作系统调度器将其唤醒。
通过上述步骤,我们可以看到自旋锁失败的情况:
- 当一个线程(如线程1)持有锁并在执行同步块时,其他线程(如线程2)会进入自旋状态,不断检查锁是否可用。
- 如果持有锁的线程长时间未释放锁,导致自旋中的线程达到自旋次数上限(例如10次),则自旋失败。
- 自旋失败后,线程将进入阻塞状态,不再占用CPU资源,等待锁被释放后再被调度器唤醒。
自旋锁说明
- 在 Java 6 之后自旋锁是自适应的,比如对象刚刚的一次自旋操作成功过,那么认为这次自旋成功的可能性会高,就多自旋几次;反之,就少自旋甚至不自旋,比较智能
- Java 7 之后不能控制是否开启自旋功能,由 JVM 控制
手写自旋锁:
import java.util.concurrent.atomic.AtomicReference; public class SpinLock { // 原子引用,保存当前持有锁的线程 private AtomicReference<Thread> atomicReference = new AtomicReference<>(); // 加锁方法 public void lock() { Thread currentThread = Thread.currentThread(); System.out.println(currentThread.getName() + " 来了,准备抢锁"); // 开始“自旋”:不断尝试用 CAS 把 null 改成自己 while (!atomicReference.compareAndSet(null, currentThread)) { // 锁被别人占着,我就在这儿“打转”等待 System.out.println(currentThread.getName() + " 正在自旋中..."); // 为了演示效果,加个 sleep(1000),实际中不会这么干(太耗CPU) try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } System.out.println(currentThread.getName() + " 成功抢到锁!"); } // 解锁方法 public void unlock() { Thread currentThread = Thread.currentThread(); // 把锁设置为 null,表示释放 if (atomicReference.compareAndSet(currentThread, null)) { System.out.println(currentThread.getName() + " 释放了锁"); } } // ================== 测试 ================== public static void main(String[] args) throws InterruptedException { SpinLock lock = new SpinLock(); // 线程 t1:先抢锁,占着10秒 new Thread(() -> { lock.lock(); try { System.out.println("t1 正在执行任务..."); Thread.sleep(10000); // 模拟工作10秒 } catch (InterruptedException e) { e.printStackTrace(); } finally { lock.unlock(); } }, "t1").start(); // 主线程睡1秒,确保 t1 先运行并抢到锁 Thread.sleep(1000); // 线程 t2:发现锁被占,开始自旋等待 new Thread(() -> { lock.lock(); try { System.out.println("t2 正在执行任务..."); Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } finally { lock.unlock(); } }, "t2").start(); } }最后输出如下:
AtomicReference<Thread>是干啥的?它是一个可以原子操作的引用变量,用来保存当前持有锁的线程。 compareAndSet(null, thread)是啥意思?CAS(Compare And Set):如果当前值是 null(没人拿锁),就把值设为当前线程,表示“我抢到了”。为啥叫“自旋”? 因为 while循环一直在跑,像陀螺一样“ spinning ”,直到抢到锁为止。实际开发能这么写吗? 不能! 这只是教学 demo。真实场景用 synchronized或ReentrantLock即可,它们内部已经高度优化。
Java 中的自旋锁进化史
Java 6 之前:
- 自旋次数是固定的,比如最多自旋 10 次。
- 不管上次有没有成功,都一样处理,比较“傻”。
Java 6 之后:自适应自旋锁
JVM 变聪明了!它会“记性”:
- 如果某个锁上次自旋成功了,说明这次也可能很快拿到,那就多自旋几次。
- 如果上次自旋失败了,说明竞争激烈,这次就少自旋甚至不自旋,直接放弃,进入阻塞。
就像你在银行发现:
- 昨天你等了3分钟就办上了 → 今天你愿意多等一会儿。
- 昨天等了半小时都没办上 → 今天你直接拿号去休息区坐着。
这就是“自适应”——根据历史表现动态调整策略。
Java 7 之后:
- 开发者不能手动控制是否开启自旋。
- 全部交给 JVM 自动管理,你说了不算。
所以:你写 synchronized 的时候,JVM 自己决定要不要自旋、自旋几次。
锁清除
锁消除是指对于被检测出不可能存在竞争的共享数据的锁进行消除,这是 JVM 即时编译器的优化
锁消除主要是通过逃逸分析来支持,如果堆上的共享数据不可能逃逸出去被其它线程访问到,那么就可以把它们当成私有数据对待,也就可以将它们的锁进行消除(同步消除:JVM 逃逸分析)
锁消除,就是 JVM 发现你加了个“锁”,但其实根本没人跟你抢,那它就干脆把这把锁直接“删了”。你写了代码,JVM 发现你“多此一举”,就帮你优化掉了。
为什么会有锁消除?我们写代码的时候,有时候为了线程安全,会用 synchronized 给代码块或者方法加锁。但是!有些情况下,这个数据其实只有你自己在用,别的线程根本碰不到它。这时候,加锁就完全没有必要,反而拖慢速度(锁是有开销的)。于是,JVM 就说:“我来帮你分析一下,这个对象是不是真的会被多个线程访问?如果不会,那锁就别加了。”这个分析过程,就叫 逃逸分析。
什么是逃逸分析?
JVM 会分析一个对象:
- 是不是只在一个方法里用?
- 是不是没传给别的线程?
- 是不是没被别的对象引用?
如果都没,那这个对象就“没逃出去”,JVM 就认为它是线程私有的,即使你加了锁,也可以安全地去掉。比如以下这个代码:
public class LockEliminationDemo {
public static void main(String[] args) {
// 开始一个方法
someMethod();
}
public static void someMethod() {
// 这个 StringBuffer 是在方法内部创建的
StringBuffer sb = new StringBuffer();
// 你用了 synchronized 方法(内部有锁)
sb.append("Hello");
sb.append(" ");
sb.append("World");
// toString() 后,sb 就结束了
String result = sb.toString();
System.out.println(result);
}
}
StringBuffer 的 append() 方法是 synchronized 的,也就是说每次调用都会上锁。但你看这个 sb:
- 在
someMethod()里创建 - 没传给别的方法
- 没被别的线程拿到
- 方法一结束就没了
所以,JVM 通过逃逸分析发现:这个 sb 只有当前线程能访问,不可能有线程安全问题!于是,JVM 在编译成机器码的时候,直接把那些 synchronized 锁给“消除”了!结果:代码运行得更快,没有锁的开销!
锁粗化
对相同对象多次加锁,导致线程发生多次重入,频繁的加锁操作就会导致性能损耗,可以使用锁粗化方式优化
如果虚拟机探测到一串的操作都对同一个对象加锁,将会把加锁的范围扩展(粗化)到整个操作序列的外部
锁粗化,就是 JVM 发现你“反反复复”地给同一个东西加锁、解锁,太啰嗦了,干脆帮你“合并成一次锁”,锁的时间长一点,但次数少很多!
就像你进厨房做饭,不是每拿一次菜就锁一次门,而是一次性进去,做完再出来,效率更高。
为什么要锁粗化?
一些看起来没有加锁的代码,其实隐式的加了很多锁:
public static String concatString(String s1, String s2, String s3) { return s1 + s2 + s3; }String 是一个不可变的类,编译器会对 String 的拼接自动优化。在 JDK 1.5 之前,转化为 StringBuffer 对象的连续 append() 操作,每个 append() 方法中都有一个同步块
public static String concatString(String s1, String s2, String s3) { StringBuffer sb = new StringBuffer(); sb.append(s1); sb.append(s2); sb.append(s3); return sb.toString(); }扩展到第一个 append() 操作之前直至最后一个 append() 操作之后,只需要加锁一次就可以
我们都知道,加锁和解锁本身是有开销的,尤其是 synchronized,涉及线程竞争、进入/退出 monitor 等操作。如果在一个方法里,连续多次对同一个对象加锁,比如:
synchronized(obj) { ... }
synchronized(obj) { ... }
synchronized(obj) { ... }
这就像你:
- 进厨房 → 拿个鸡蛋 → 出来锁门
- 再进厨房 → 拿个番茄 → 出来锁门
- 再进厨房 → 开火 → 出来锁门
累不累?烦不烦?JVM 就说:“你这不是来回折腾吗?干脆一次性进去,把所有事做完,再出来锁门!”这就是锁粗化:把多个小锁,合并成一个大锁。
场景:字符串拼接(JDK 1.5 之前)
在早期 Java 中,String 拼接会被编译器转成 StringBuffer.append(),而 StringBuffer 的 append() 是 synchronized 的!
public static String concatString(String s1, String s2, String s3) {
return s1 + s2 + s3; // 看起来没加锁?
}
上面这行代码,编译后其实是这样的:
public static String concatString(String s1, String s2, String s3) {
StringBuffer sb = new StringBuffer();
sb.append(s1); // synchronized!
sb.append(s2); // synchronized!
sb.append(s3); // synchronized!
return sb.toString();
}
每次 append() 都要加锁、解锁一次,连续加了3次锁!如果 sb 是局部变量,没人抢,你还这么频繁加锁,性能就浪费了。
JVM 发现:
- 这几个
append()都是对同一个对象(sb)操作 - 而且是连续操作
- 完全可以把锁的范围扩大
于是 JVM 把它优化成这样:
public static String concatString(String s1, String s2, String s3) {
StringBuffer sb = new StringBuffer();
// JVM 自动“粗化”:只锁一次,包住所有 append
synchronized(sb) {
sb.append(s1);
sb.append(s2);
sb.append(s3);
}
return sb.toString();
}
结果:从 3次加锁 + 3次解锁 → 变成 1次加锁 + 1次解锁!这就是锁粗化:把多个细碎的锁,合并成一个“粗”的大锁。
多把锁
多把不相干的锁:一间大屋子有两个功能睡觉、学习,互不相干。现在一人要学习,一人要睡觉,如果只用一间屋子(一个对象锁)的话,那么并发度很低
将锁的粒度细分:
- 好处,是可以增强并发度
- 坏处,如果一个线程需要同时获得多把锁,就容易发生死锁
解决方法:准备多个对象锁
public static void main(String[] args) { BigRoom bigRoom = new BigRoom(); new Thread(() -> { bigRoom.study(); }).start(); new Thread(() -> { bigRoom.sleep(); }).start(); } class BigRoom { private final Object studyRoom = new Object(); private final Object sleepRoom = new Object(); public void sleep() throws InterruptedException { synchronized (sleepRoom) { System.out.println("sleeping 2 小时"); Thread.sleep(2000); } } public void study() throws InterruptedException { synchronized (studyRoom) { System.out.println("study 1 小时"); Thread.sleep(1000); } } }
多把锁,就是把“一间大屋一把锁”改成“睡觉有睡房锁,学习有书房锁”,各干各的,互不打扰,提升效率!你想睡觉?你去睡你的。我想学习?我去学我的。咱俩不用抢同一个屋子,并发度就上去了!
举个生活大例子:一个家,两个功能假设你家只有一个房间,这房间既当卧室又当书房。问题来了(一把锁的坏处):你朋友来你家 学习,占了房间,上了锁:“别打扰我!”结果你困了,想 睡觉,但进不去——房间被占着!虽然一个是学习,一个是睡觉,根本不冲突,但因为只有一把锁,你就得等!这就是:并发度低,资源利用率差。
怎么办?—— 搞“多把锁”!
把房间分成两个小区域:
- 一个床(睡觉专用)
- 一个书桌(学习专用)
然后:
- 床上加一把锁:
sleepLock - 书桌上加一把锁:
studyLock
现在:
- 你朋友在书桌学习(锁了书桌)
- 你可以在床上睡觉(锁了床)
两人同时干自己的事,互不干扰!这就是多把锁的核心思想:把大锁拆成小锁,让不相干的事可以并发执行。
public class MultiLockDemo {
public static void main(String[] args) {
BigRoom bigRoom = new BigRoom();
// 线程1:去睡觉
new Thread(() -> {
bigRoom.sleep();
}, "睡觉线程").start();
// 线程2:去学习
new Thread(() -> {
bigRoom.study();
}, "学习线程").start();
}
}
// 大房间类
class BigRoom {
// 两把独立的锁:各管一摊事
private final Object sleepRoom = new Object(); // 睡觉专用锁
private final Object studyRoom = new Object(); // 学习专用锁
// 睡觉方法:只锁睡觉区域
public void sleep() {
synchronized (sleepRoom) {
System.out.println(Thread.currentThread().getName() + " 开始睡觉,要睡 2 小时...");
try {
Thread.sleep(2000); // 模拟睡2秒(代表2小时)
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " 睡醒了!");
}
}
// 学习方法:只锁学习区域
public void study() {
synchronized (studyRoom) {
System.out.println(Thread.currentThread().getName() + " 开始学习,要学 1 小时...");
try {
Thread.sleep(1000); // 模拟学1秒
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " 学完了!");
}
}
}
输出结果如下:

两个线程同时运行,没有互相等待。
多把锁的坏处:容易死锁
死锁例子:需要同时用两个房间
假设现在有个线程,它要:
- 先锁住“床” → 睡觉
- 再锁住“书桌” → 学习
另一个线程反过来:
- 先锁住“书桌” → 学习
- 再锁住“床” → 睡觉
如果两个线程同时运行,就可能:
- A 线程锁了床,等着书桌
- B 线程锁了书桌,等着床
- 谁也不让,互相等,死锁了!
// 危险代码:可能死锁
public void sleepAndStudy() {
synchronized (sleepRoom) {
System.out.println("先睡觉...");
try { Thread.sleep(1000); } catch (Exception e) {}
synchronized (studyRoom) {
System.out.println("再学习...");
}
}
}
public void studyAndSleep() {
synchronized (studyRoom) {
System.out.println("先学习...");
try { Thread.sleep(1000); } catch (Exception e) {}
synchronized (sleepRoom) {
System.out.println("再睡觉...");
}
}
}
如何避免死锁?
- 固定加锁顺序:大家都先锁
sleepRoom,再锁studyRoom,不能乱来。 - 使用 tryLock(ReentrantLock):尝试拿锁,拿不到就放弃,避免无限等。
- 减少多锁操作:能不用多把锁,就不用。
活跃性
死锁
形成
死锁:多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放,由于线程被无限期地阻塞,因此程序不可能正常终止。Java 死锁产生的四个必要条件:
- 互斥条件,即当资源被一个线程使用(占有)时,别的线程不能使用
- 不可剥夺条件,资源请求者不能强制从资源占有者手中夺取资源,资源只能由资源占有者主动释放
- 请求和保持条件,即当资源请求者在请求其他的资源的同时保持对原有资源的占有
- 循环等待条件,即存在一个等待循环队列:p1 要 p2 的资源,p2 要 p1 的资源,形成了一个等待环路
四个条件都成立的时候,便形成死锁。死锁情况下打破上述任何一个条件,便可让死锁消失
“死锁”——互相等待,无限卡住,程序就卡死了,再也动不了。
死锁不是随便发生的,必须同时满足 4 个条件:
| 条件 | 大白话解释 | 生活例子 |
|---|---|---|
| 1. 互斥条件 | 一个资源,一次只能一个人用 | 桥一次只能过一辆车 |
| 2. 不可剥夺 | 不能抢别人手里的资源,只能等他自己放 | 你不能把对方车推开,只能等他退 |
| 3. 请求和保持 | 我已经占着一个资源,还想去拿另一个 | 车A已经上了桥,还想让车B退 |
| 4. 循环等待 | A等B,B等A,形成一个“死循环” | A要B退,B要A退,互相等 |
编码实现双线程死锁例子:
public class DeadlockDemo {
// 两把锁(两个资源)
public static final Object resource1 = new Object();
public static final Object resource2 = new Object();
public static void main(String[] args) {
// 线程1:先占 resource1,再请求 resource2
Thread t1 = new Thread(() -> {
synchronized (resource1) {
System.out.println("线程1:已占用 resource1,准备请求 resource2...");
try {
Thread.sleep(1000); // 模拟处理时间,让t2有机会先占resource2
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("线程1:尝试获取 resource2...");
synchronized (resource2) {
System.out.println("线程1:成功获得 resource2!");
}
}
}, "线程1");
// 线程2:先占 resource2,再请求 resource1
Thread t2 = new Thread(() -> {
synchronized (resource2) {
System.out.println("线程2:已占用 resource2,准备请求 resource1...");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("线程2:尝试获取 resource1...");
synchronized (resource1) {
System.out.println("线程2:成功获得 resource1!");
}
}
}, "线程2");
// 启动两个线程
t1.start();
t2.start();
}
}
观察控制台,其实是卡死不动的,就是死锁:

如何解决死锁?
方法1:固定加锁顺序(打破“循环等待”)大家都约定:先锁 resource1,再锁 resource2。
// 线程2 改成:先请求 resource1,再请求 resource2
Thread t2 = new Thread(() -> {
synchronized (resource1) { // 先锁 resource1
System.out.println("线程2:已占用 resource1");
try { Thread.sleep(1000); } catch (Exception e) {}
synchronized (resource2) {
System.out.println("线程2:已占用 resource2");
}
}
});
方法2:使用 tryLock(打破“请求和保持”)用 ReentrantLock 的 tryLock(),尝试拿锁,拿不到就放弃或重试。
import java.util.concurrent.locks.ReentrantLock;
ReentrantLock lock1 = new ReentrantLock();
ReentrantLock lock2 = new ReentrantLock();
Thread t1 = new Thread(() -> {
lock1.lock();
try {
System.out.println("线程1 拿到 lock1");
Thread.sleep(1000);
if (lock2.tryLock()) { // 尝试拿 lock2
try {
System.out.println("线程1 拿到 lock2");
} finally {
lock2.unlock();
}
} else {
System.out.println("线程1 拿不到 lock2,放弃!");
}
} catch (Exception e) {
e.printStackTrace();
} finally {
lock1.unlock();
}
});
方法3:设置超时(打破“无限等待”)
boolean locked = lock2.tryLock(3, TimeUnit.SECONDS);
if (!locked) {
// 超时了,放弃或重试
}
死锁的危害
- 程序假死,不报错也不结束
- 线程卡住,资源不释放
- 严重时导致系统瘫痪
定位
定位死锁的方法:
使用 jps 定位进程 id,再用
jstack id定位死锁,找到死锁的线程去查看源码,解决优化"Thread-1" #12 prio=5 os_prio=0 tid=0x000000001eb69000 nid=0xd40 waiting formonitor entry [0x000000001f54f000] java.lang.Thread.State: BLOCKED (on object monitor) #省略 "Thread-1" #12 prio=5 os_prio=0 tid=0x000000001eb69000 nid=0xd40 waiting for monitor entry [0x000000001f54f000] java.lang.Thread.State: BLOCKED (on object monitor) #省略 Found one Java-level deadlock: =================================================== "Thread-1": waiting to lock monitor 0x000000000361d378 (object 0x000000076b5bf1c0, a java.lang.Object), which is held by "Thread-0" "Thread-0": waiting to lock monitor 0x000000000361e768 (object 0x000000076b5bf1d0, a java.lang.Object), which is held by "Thread-1" Java stack information for the threads listed above: =================================================== "Thread-1": at thread.TestDeadLock.lambda$main$1(TestDeadLock.java:28) - waiting to lock <0x000000076b5bf1c0> (a java.lang.Object) - locked <0x000000076b5bf1d0> (a java.lang.Object) at thread.TestDeadLock$$Lambda$2/883049899.run(Unknown Source) at java.lang.Thread.run(Thread.java:745) "Thread-0": at thread.TestDeadLock.lambda$main$0(TestDeadLock.java:15) - waiting to lock <0x000000076b5bf1d0> (a java.lang.Object) - locked <0x000000076b5bf1c0> (a java.lang.Object) at thread.TestDeadLock$$Lambda$1/495053715
Linux 下可以通过 top 先定位到 CPU 占用高的 Java 进程,再利用
top -Hp 进程id来定位是哪个线程,最后再用 jstack 的输出来看各个线程栈避免死锁:避免死锁要注意加锁顺序
可以使用 jconsole 工具,在
jdk\bin目录下
定位死锁,就是当你的程序“卡死了”,你要像个“侦探”一样,找出到底是哪两个线程在“互不相让”,然后去代码里“抓现行”!
这就需要两个工具:jps 和 jstack。
| 工具 | 作用 | 大白话 |
|---|---|---|
jps | 查看当前所有 Java 进程的 ID | 告诉我哪个 Java 程序在跑 |
jstack <进程ID> | 查看某个 Java 进程的所有线程状态和调用栈 | 把每个线程在干啥列出来 |
第一步:用 jps 找到进程 ID

第二步:用 jstack 查看线程状态

它会输出一大堆信息,全是线程的“现场记录”。我们要找的关键信息是:
关键词1:BLOCKED (on object monitor)
表示这个线程被锁住了,正在等别人释放锁。
"Thread-1" #12 prio=5 ...
java.lang.Thread.State: BLOCKED (on object monitor)
关键词2:waiting to lock ... which is held by ...
这是“死锁铁证”!它直接告诉你:
- 谁在等
- 等什么
- 被谁占着
比如:
Found one Java-level deadlock:
===================================================
"Thread-1":
waiting to lock monitor 0x000000076b5bf1c0 (a java.lang.Object),
which is held by "Thread-0"
"Thread-0":
waiting to lock monitor 0x000000076b5bf1d0 (a java.lang.Object),
which is held by "Thread-1"
- Thread-1 在等
0x000...c0这把锁,但这把锁被 Thread-0 拿着 - Thread-0 在等
0x000...d0这把锁,但这把锁被 Thread-1 拿着
关键词3:Java stack information —— 精确定位到代码行
Java stack information for the threads listed above:
===================================================
"Thread-1":
at thread.TestDeadLock.lambda$main$1(TestDeadLock.java:28)
- waiting to lock <0x000000076b5bf1c0> (a java.lang.Object)
- locked <0x000000076b5bf1d0> (a java.lang.Object)
"Thread-0":
at thread.TestDeadLock.lambda$main$0(TestDeadLock.java:15)
- waiting to lock <0x000000076b5bf1d0> (a java.lang.Object)
- locked <0x000000076b5bf1c0> (a java.lang.Object)
TestDeadLock.java:28:Thread-1 正在第 28 行等锁,它已经占了0x...d0TestDeadLock.java:15:Thread-0 正在第 15 行等锁,它已经占了0x...c0
JDK 还自带一个可视化工具:jconsole,它会弹出一个图形界面,选择你的 Java 进程连接。然后点“线程”标签,再点“检测死锁”按钮。它会自动帮你找出死锁的线程,并高亮显示。


Linux 下更高级的定位方法(CPU 高时用)
有时候死锁不一定卡住 CPU,但如果线程在“忙等”或频繁竞争,CPU 可能很高。可以用 top 找出占用高的 Java 进程:
top
看到某个 Java 进程 CPU 很高,记下它的 PID(比如 2345),然后看这个进程里哪个线程最耗 CPU:
top -Hp 2345
输出可能看到:
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
2356 user 20 0 2816664 55624 14304 R 99.0 0.7 0:10.12 java
PID 2356 的线程 CPU 占用 99%!但 jstack 用的是线程ID的十六进制,所以要把 2356 转成十六进制:
printf "%x\n" 2356
# 输出:934
然后在 jstack 2345 的输出里搜 934,就能定位到是哪个 Java 线程(比如 nid=0x934),再看它的调用栈,就知道它在干啥了。
活锁
指的是任务或者执行者没有被阻塞,由于某些条件没有满足,导致一直重复尝试—失败—尝试—失败的过程。两个线程互相改变对方的结束条件,最后谁也无法结束。
活锁,就是两个(或多个人)太“礼貌”或太“较劲”,都在主动让步,结果反而一直动来动去,谁也干不成事!
class TestLiveLock {
// 共享变量:一个数
static volatile int count = 10;
public static void main(String[] args) {
// 线程1:想把 count 减到 0 就停
new Thread(() -> {
while (count > 0) {
try {
Thread.sleep(200); // 模拟工作
} catch (InterruptedException e) {
e.printStackTrace();
}
count--; // 往目标靠近
System.out.println("线程一:把 count 减了,现在是 " + count);
}
System.out.println("线程一:终于完成了!");
}, "线程一").start();
// 线程二:想把 count 加到 20 就停
new Thread(() -> {
while (count < 20) {
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
count++; // 往目标靠近
System.out.println("线程二:把 count 加了,现在是 " + count);
}
System.out.println("线程二:终于完成了!");
}, "线程二").start();
}
}
控制台输出如下,一直在活锁状态:

饥饿
一个线程由于优先级太低,始终得不到 CPU 调度执行,也不能够结束
Java并发编程中的锁优化与活跃性问题

620

被折叠的 条评论
为什么被折叠?



