因此,我们必须尽可能防止死锁。
如果有一个线程A按照先锁a,后锁b的顺序获取锁,而另一个线程B按照先锁b,后锁a的顺序获取锁,就会出现如下情况:
用代码模拟一下,假设Spring Boot环境:
@Component publicclass DeadLock { privatestatic Object lockA = new Object(); privatestatic Object lockB = new Object(); public void deadLock() { Thread threadA = new Thread(() -> { synchronized (lockA) { System.out.println(Thread.currentThread().getName() + "Get lockA success"); try { TimeUnit.SECONDS.sleep(1); System.out.println(Thread.currentThread().getName() + "Try to get lockB "); synchronized (lockB) { System.out.println(Thread.currentThread().getName() + "Get lockB success"); } } catch (InterruptedException e) { e.printStackTrace(); } } }); Thread threadB = new Thread(() -> { synchronized (lockB) { System.out.println(Thread.currentThread().getName() + "Get lockB success"); try { TimeUnit.SECONDS.sleep(1); System.out.println(Thread.currentThread().getName() + "Try to get lockA"); synchronized (lockA) { System.out.println(Thread.currentThread().getName() + "get lockA success"); } } catch (InterruptedException e) { e.printStackTrace(); } } }); threadA.start(); threadB.start(); } }输出:
Thread-3 Get lockB success Thread-4 Get lockA success Thread-4 Try to get lockB Thread-3 Try to get lockA
可以看到到线程 3 成功获取了锁 A,但在尝试获取锁 B 时会失败。 这种相互等待对方释放锁就造成了死锁。
final ExecutorService executorService = Executors.newSingleThreadExecutor(); Future<Long> f1 = executorService.submit(new Callable<Long>() { public Long call() throws Exception { System.out.println("start f1"); Thread.sleep(1000); Future<Long> f2 = executorService.submit(new Callable<Long>() { public Long call() throws Exception { System.out.println("start f2"); return -1L; } }); System.out.println("result" + f2.get()); System.out.println("end f1"); return -1L; } });
在单线程线程池中,当任务1依赖于任务2的执行结果时,就会出现死锁。 由于单线程的特性,如果任务1没有完成,任务2将永远没有机会执行,从而导致死锁。
$ jps 13448 Jps 7321 JUnitStarter然后,使用jstack查看当前进程的堆栈信息。
$ jstack -F 7321 Attaching to process ID 7321, please wait... Debugger attached successfully. Server compiler detected. JVM version is 25.181-b13 Deadlock Detection: Found one Java-level deadlock: ============================= "Thread-4": waiting to lock Monitor@0x000000001f0134f8 (Object@0x00000007721d90f0, a java/lang/Object), which is held by "Thread-3" "Thread-3": waiting to lock Monitor@0x000000001f011ef8 (Object@0x00000007721d90e0, a java/lang/Object), which is held by "Thread-4" Found a total of 1 deadlock. Thread 21: (state = BLOCKED) - com.zero.demo.deadlock.DeadLock.lambda$deadLock$1() @bci=79, line=35 (Interpreted frame)1. Acquiring locks in the correct order. - com.zero.demo.deadlock.DeadLock$$Lambda$170.run() @bci=0 (Interpreted frame) - java.lang.Thread.run() @bci=11, line=748 (Interpreted frame) Thread 20: (state = BLOCKED) - com.zero.demo.deadlock.DeadLock.lambda$deadLock$0() @bci=79, line=20 (Interpreted frame) - com.zero.demo.deadlock.DeadLock$$Lambda$169.run() @bci=0 (Interpreted frame) - java.lang.Thread.run() @bci=11, line=748 (Interpreted frame)如何防止死锁
现在我们了解了死锁是如何发生的,我们也知道如何检查它们。如果一个线程一次只能获取一个锁,则不会出现嵌套锁获取顺序导致的死锁问题。
如果必须获取多个锁,则必须考虑不同线程获取这些锁的顺序。 上例中出现死锁的根本原因是无序获取锁,这是我们无法控制的。 在上述示例的最佳情况下,应该抽象业务逻辑并将锁获取代码放置在公共方法中。两个线程都从公共方法中获取锁。
@Component publicclass DeadLock { privatestatic Object lockA = new Object(); privatestatic Object lockB = new Object(); public void deadLock() { Thread threadA = new Thread(() -> { getLock(); }); Thread threadB = new Thread(() -> { getLock(); }); threadA.start(); threadB.start(); } private void getLock() { synchronized (lockA) { System.out.println(Thread.currentThread().getName() + "Get lockA success"); try { TimeUnit.SECONDS.sleep(1); System.out.println(Thread.currentThread().getName() + "Try to get lockB"); synchronized (lockB) { System.out.println(Thread.currentThread().getName() + "Get lockB success"); } } catch (InterruptedException e) { e.printStackTrace(); } } } }查看打印结果,我们可以观察到线程 4 成功获取了锁,允许线程 3 继续获取锁。
Thread-4 Get lockA success Thread-4 Try to get lockB Thread-4 Get lockB success Thread-3 Get lockA success Thread-3 Try to get lockB Thread-3 Get lockB success超时和放弃
当线程尝试获取锁时超时,它会放弃该尝试,从而防止发生与锁相关的死锁。当使用synchronized关键字提供的内置锁时,如果线程无法获得锁,就会无限期地等待。但是,Lock 接口提供了一个方法 boolean tryLock(long time, TimeUnit unit) throws InterruptedException,该方法允许线程等待固定的时间来获取锁。
这使得线程可以在超时后主动释放之前获取的所有锁,有效避免死锁。
当两个任务以不正确的顺序不合理地争用资源时,就会发生死锁。 因此,为了减少死锁,应用程序必须以正确的顺序处理资源获取。有的时候,死锁可能不会立即在应用程序中显现出来。 通常,它们会在应用程序在生产环境中运行一段时间后逐渐浮出水面。