死锁
线程A持有锁A 线程B持有锁B 在线程A还没有释放锁A的情况下 线程B试图去获取锁A 在线程B还没有释放锁B的情况下 线程A去获取锁B 就形成了相互持有对方所需要的锁 而处于永久阻塞的状态 即为死锁
public class DeadLockSample extends Thread {
private String first;
private String second;
public DeadLockSample(String name, String first, String second) {
super(name);
this.first = first;
this.second = second;
}
public void run() {
synchronized (first) {
System.out.println(this.getName() + " obtained: " + first);
try {
Thread.sleep(1000L); // 模拟一些操作
synchronized (second) {
System.out.println(this.getName() + " obtained: " + second);
}
} catch (InterruptedException e) {
// Do nothing
}
}
}
public static void main(String[] args) throws InterruptedException {
String lockA = "lockA";
String lockB = "lockB";
DeadLockSample t1 = new DeadLockSample("Thread1", lockA, lockB);
DeadLockSample t2 = new DeadLockSample("Thread2", lockB, lockA);
t1.start();
t2.start();
t1.join();
t2.join();
}
}
这个程序定义了一个DeadLockSample类,它继承了Thread类并覆盖了run方法。在run方法中,线程首先尝试获取first对象的锁,然后尝试获取second对象的锁。在main方法中,创建了两个DeadLockSample实例,它们分别以不同的顺序尝试获取两个锁。这种不同顺序的锁获取可能导致死锁,因为每个线程都在等待另一个线程释放它所持有的锁。
关于死锁的避免,根据您提供的内容,以下是一些建议:
避免滥用锁。同一把锁的加锁和解锁应该放在同一个方法中。尽量避免在持有一把锁的情况下获取另一把锁。
调用 jstack 获取线程栈:
如果我们是开发自己的管理工具,需要用更加程序化的方式扫描服务进程、定位死锁,可以考虑使用 Java 提供的标准管理 API,ThreadMXBean,其直接就提供了 findDeadlockedThreads() 方法用于定位。为方便说明,我修改了 DeadLockSample,请看下面的代码片段。
public static void main(String[] args) throws InterruptedException {
ThreadMXBean mbean = ManagementFactory.getThreadMXBean();
Runnable dlCheck = new Runnable() {
@Override
public void run() {
long[] threadIds = mbean.findDeadlockedThreads();
if (threadIds != null) {
ThreadInfo[] threadInfos = mbean.getThreadInfo(threadIds);
System.out.println("Detected deadlock threads:");
for (ThreadInfo threadInfo : threadInfos) {
System.out.println(threadInfo.getThreadName());
}
}
}
};
ScheduledExecutorService scheduler =Executors.newScheduledThreadPool(1);
// 稍等5秒,然后每10秒进行一次死锁扫描
scheduler.scheduleAtFixedRate(dlCheck, 5L, 10L, TimeUnit.SECONDS);
// 死锁样例代码…
}
重新编译执行,你就能看到死锁被定位到的输出。在实际应用中,就可以据此收集进一步的信息,然后进行预警等后续处理。但是要注意的是,对线程进行快照本身是一个相对重量级的操作,还是要慎重选择频度和时机。
如何在编程中尽量预防死锁呢?
首先,我们来总结一下前面例子中死锁的产生包含哪些基本元素。基本上死锁的发生是因为:
a.互斥条件,类似 Java 中 Monitor 都是独占的,要么是我用,要么是你用。
b.互斥条件是长期持有的,在使用结束之前,自己不会释放,也不能被其他线程抢占。
c.循环依赖关系,两个或者多个个体之间出现了锁的链条环。
所以,我们可以据此分析可能的避免死锁的思路和方法。
1.如果可能的话,尽量避免使用多个锁,并且只有需要时才持有锁。
2.如果必须使用多个锁,尽量设计好锁的获取顺序。
3.使用带超时的方法,为程序带来更多可控性。
类似 Object.wait(…) 或者 CountDownLatch.await(…),都支持所谓的 timed_wait,我们完全可以就不假定该锁一定会获得,指定超时时间,并为无法得到锁时准备退出逻辑。并发 Lock 实现,如 ReentrantLock 还支持非阻塞式的获取锁操作 tryLock(),这是一个插队行为(barging),并不在乎等待的公平性,如果执行时对象恰好没有被独占,则直接获取锁。有时,我们希望条件允许就尝试插队,不然就按照现有公平性规则等待,一般采用下面的方法:
if (lock.tryLock() || lock.tryLock(timeout, unit)) { // ... }
4.业界也有一些其他方面的尝试,比如通过静态代码分析(如 FindBugs)去查找固定的模式,进而定位可能的死锁或者竞争情况。实践证明这种方法也有一定作用