Java synchronized死锁陷阱与内存可见性解析
synchronized是Java实现线程互斥的关键字,但其隐式的对象锁机制若使用不当,极易在异步回调或跨方法等待场景中引发死锁。同时,作为Java内存模型(JMM)的核心组件,它通过Happens-Before规则确保了线程间的内存可见性,防止因指令重排序导致的数据读取错误。
结果与目标
理解synchronized在并发编程中的双重作用:一是作为互斥锁防止数据竞争,二是通过内存屏障保证变量的可见性。重点排查因锁机制误解导致的程序卡死(死锁)现象,并掌握其底层的内存语义。
准备条件
需要基本的Java多线程知识,了解线程启动、run方法执行及基本同步概念。熟悉JDK环境下的代码调试能力。
真实操作顺序:死锁成因剖析
第1步:重现隐式锁导致的死锁场景
目的:验证为何在同一对象的两个synchronized方法间相互等待会引发死锁。
背景:synchronized修饰实例方法时,隐式锁定的是当前实例对象(this)。持有该锁的线程在未退出同步块前,其他任何试图获取同一对象锁的线程都将被阻塞。
常见错误代码结构如下,主线程调用方法a,其中包含异步回调等待;异步线程调用方法b以解除等待:
synchronized void a(){
CountDownLatch countDownLatch = new CountDownLatch(1);
// 执行部分业务逻辑
// ...
// 触发异步线程执行b,并在此处阻塞等待
countDownLatch.await();
}
synchronized void b(){
countDownLatch.countDown();
}
现象:主线程执行a后卡住,无论等待多久,程序都不会继续。即使b由独立的异步线程执行,也无法运行。
第2步:分析锁竞争机制
入口:理解synchronized的作用范围。
动作:将上述方法拆解为显式同步块进行对比:
- 方法a执行时,首先获取对象锁(mutex)。
- 进入内部逻辑后,发起对方法b的调用请求。
- 方法b也是synchronized方法,进入时必须再次获取同一个对象锁。
关键参数/状态:a持有的锁尚未释放(因为await()发生在同步方法体内,方法未结束),但a又在等待b执行完毕。b在等待获取a持有的锁。这就形成了“A等B,B等A”的经典死锁环路。
这证明了放在方法声明前的synchronized,其底层行为等价于在方法体开头和结尾自动包裹了 synchronized(this) { ... }。
第3步:验证锁的对象实质
入口:编写测试代码验证实例方法的隐式锁对象。
动作:创建一个类,定义一个synchronized方法m1和一个在方法内使用synchronized(this)的代码块方法m2。启动两个线程分别调用m1和m2。
public class MultiThreadSync {
public synchronized void m1() throws InterruptedException {
System.out.println("m1 call");
Thread.sleep(2000);
System.out.println("m1 call done");
}
public void m2() throws InterruptedException {
synchronized (this) {
System.out.println("m2 call");
Thread.sleep(2000);
System.out.println("m2 call done");
}
}
public static void main(String[] args) {
final MultiThreadSync thisObj = new MultiThreadSync();
Thread t1 = new Thread(() -> {
try { thisObj.m1(); } catch (InterruptedException e) { e.printStackTrace(); }
});
Thread t2 = new Thread(() -> {
try { thisObj.m2(); } catch (InterruptedException e) { e.printStackTrace(); }
});
t1.start();
t2.start();
}
}
可观察结果:控制台先输出 m1 call 和 m1 call done,随后才输出 m2 call 和 m2 call done。
结论:m2执行时等待了m1的完成,证实两者锁住了同一个对象(实例本身)。对于static方法,锁定的则是Class对象实例。
结果与目标:内存可见性与Happens-Before
第1步:识别无同步状态下的内存可见性问题
目的:展示不使用synchronized时,线程间可能读不到最新变量值。
场景:写线程修改变量,读线程循环等待标识位。
public class NoVisibility {
private static boolean ready = false;
private static int number = 0;
private static class ReaderThread extends Thread {
@Override
public void run() {
while (!ready) {
Thread.yield();
}
System.out.println(number); // 可能输出0,也可能不输出
}
}
public static void main(String[] args) {
new ReaderThread().start();
number = 42;
ready = true;
}
}
风险提醒:JVM编译器和CPU指令可能会进行重排序。若ready=true先于number=42执行,读线程可能看到ready为true但number仍为默认值0。更严重的是,由于每个线程拥有独立的工作内存(Cache/寄存器),写线程更新后的值可能始终未同步回主存,导致死循环。
第2步:利用synchronized恢复可见性
入口:引入共享锁对象。
动作:在读写两侧均使用synchronized块保护共享变量访问。
private static Object lock = new Object();
// ... 读取端
synchronized (lock) {
while (!ready) { Thread.yield(); }
System.out.println(number);
}
// ... 写入端
synchronized (lock) {
number = 42;
ready = true;
}
原理:JMM规定,解锁操作 happens-before 后续的加锁操作。当ThreadA释放锁时,其工作内存中所有变量的更新都会刷新到主存。ThreadB在获取同一把锁时,会强制失效自己的工作内存缓存,重新从主存加载变量。从而确保读到最新值。
完成判断
通过添加正确的synchronized同步代码,原本可能输出0或无限等待的程序,稳定输出42。死锁问题解决后,程序按预期流程正常结束。
排错指南
若遇到程序“假死”且堆栈显示大量线程BLOCKED在synchronized代码段,请检查:
1. 是否在同一实例的不同synchronized方法间存在直接或间接的互相等待?
2. 异步回调中是否调用了当前线程持有的锁对应的synchronized方法?
3. 是否误以为synchronized只锁代码块内部,忽略了方法级同步对整个对象锁的占用?
简要总结
本站声明
本文内容仅供参考,不代表本站观点。转载或引用请注明出处。如发现内容问题,请通过联系我们页面反馈。













