被 Java 的 synchronized 锁坑过吗?这 3 种场景必死锁

在 Java 并发编程中,synchronized 锁是常用的同步机制,但使用不当极易引发死锁。本文将详细剖析 3 种必死锁的场景:一是多线程嵌套获取不同顺序的锁,因锁获取顺序混乱导致相互等待;二是单例模式中同步代码块设计不合理,引发线程间的循环等待;三是线程池与锁结合使用时,任务依赖关系不当造成的死锁。通过对这些场景的原理分析、代码示例讲解及解决方案探讨,帮助开发者规避 synchronized 锁使用中的陷阱,提升并发程序的稳定性。​

一、多线程嵌套获取不同顺序的锁​

在 Java 并发编程里,多线程环境下若存在嵌套获取锁且顺序不一致的情况,死锁极易发生。这是因为当多个线程都持有对方需要的锁,且都在等待对方释放锁时,就会陷入无限等待的状态。​

比如,有两个线程 Thread1 和 Thread2,Thread1 先获取锁 A,再尝试获取锁 B;而 Thread2 先获取锁 B,再尝试获取锁 A。当 Thread1 持有锁 A 等待锁 B,Thread2 持有锁 B 等待锁 A 时,死锁就产生了。​

从代码层面来看,可能是这样的情况:​

public class DeadLockDemo1 {​

private static final Object lockA = new Object();​

private static final Object lockB = new Object();​

public static void main(String[] args) {​

new Thread(() -> {​

synchronized (lockA) {​

System.out.println("Thread1获取了lockA");​

try {​

Thread.sleep(1000);​

} catch (InterruptedException e) {​

e.printStackTrace();​

}​

synchronized (lockB) {​

System.out.println("Thread1获取了lockB");​

}​

}​

}).start();​

在上述代码中,两个线程分别以不同的顺序获取锁 A 和锁 B,运行后很可能出现死锁。要解决这种场景下的死锁,关键在于保证所有线程获取锁的顺序一致。例如,规定所有线程都先获取锁 A,再获取锁 B,这样就不会出现相互等待的情况。​

二、单例模式中同步代码块设计不合理​

单例模式是 Java 中常见的设计模式,为了保证线程安全,常使用 synchronized 锁。但如果同步代码块设计不合理,也可能导致死锁。​

以懒汉式单例为例,有些开发者可能会在获取实例的方法中使用双重检查锁定,但若同步代码块的范围或锁定对象设置不当,就可能引发问题。​

错误的代码示例可能如下:​

在这个例子中,构造方法中又对 Singleton.class 进行了锁定,而 getInstance 方法中的同步代码块也锁定了 Singleton.class。当一个线程在 getInstance 方法的同步块中创建实例,进入构造方法并持有 Singleton.class 锁时,另一个线程可能在 getInstance 方法的外层判断 instance 为 null 后,尝试进入同步块,此时就会等待 Singleton.class 锁,而持有锁的线程在构造方法中可能还需要完成其他操作,若此时又有依赖关系,就可能导致死锁。​

要避免这种情况,需要合理设计同步代码块,尽量避免在构造方法中进行可能引发锁竞争的操作,或者调整锁定的对象和范围,确保不会出现循环等待的情况。可以采用静态内部类的方式实现单例,这种方式能更好地保证线程安全且避免死锁。​

三、线程池与锁结合使用时任务依赖关系不当​

线程池在 Java 并发编程中被广泛使用,能提高线程的利用率。但当线程池中的任务存在依赖关系,且结合 synchronized 锁使用时,若依赖关系处理不当,极易造成死锁。​

比如,线程池中的任务 A 需要等待任务 B 的结果,而任务 B 又需要等待任务 A 释放的锁,此时就可能形成死锁。​

代码示例如下:​

import java.util.concurrent.ExecutorService;​

import java.util.concurrent.Executors;​

import java.util.concurrent.Future;​

public class ThreadPoolDeadLockDemo {​

private static final Object lock = new Object();​

private static ExecutorService executor = Executors.newFixedThreadPool(2);​

public static void main(String[] args) {​

Future<?> futureA = executor.submit(() -> {​

synchronized (lock) {​

System.out.println("任务A获取了锁");​

// 等待任务B完成​

Future<?> futureB = executor.submit(() -> {​

synchronized (lock) {​

System.out.println("任务B获取了锁");​

}​

});​

try {​

futureB.get();​

} catch (Exception e) {​

e.printStackTrace();​

}​

}​

});​

try {​

futureA.get();​

在这个例子中,任务 A 获取锁后提交了任务 B,而任务 B 需要获取同一个锁才能执行。但此时锁被任务 A 持有,任务 B 等待锁,任务 A 又等待任务 B 完成,从而形成死锁。​

解决这类死锁的关键在于合理规划任务之间的依赖关系,避免任务之间出现循环等待锁的情况。可以通过调整任务的执行顺序,或者使用更灵活的同步机制,如 Lock 接口及其相关方法,来更好地控制线程的执行和锁的获取释放。​

四、总结归纳​

synchronized 锁是 Java 并发编程中实现同步的重要手段,但在使用过程中,若不注意细节,很容易在特定场景下引发死锁。本文介绍的三种必死锁场景分别是:多线程嵌套获取不同顺序的锁、单例模式中同步代码块设计不合理、线程池与锁结合使用时任务依赖关系不当。​

对于多线程嵌套获取锁的场景,保证所有线程获取锁的顺序一致是避免死锁的有效方法;在单例模式中,要合理设计同步代码块,避免构造方法中出现可能引发锁竞争的操作;而在线程池与锁结合使用时,需规划好任务间的依赖关系,必要时采用更灵活的同步机制。​

开发者在进行 Java 并发编程时,应充分认识这些场景下死锁产生的原理,在代码设计和实现过程中加以规避,从而提高程序的稳定性和可靠性,减少因死锁导致的程序故障。​

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值