Java并发编程实战 04死锁了怎么办?

Java并发编程文章系列

Java并发编程实战 01并发编程的Bug源头
Java并发编程实战 02Java如何解决可见性和有序性问题
Java并发编程实战 03互斥锁 解决原子性问题

前提

在第三篇文章最后的例子当中,需要获取到两个账户的锁后进行转账操作,这种情况有可能会发生死锁,我把上一章的代码片段放到下面:

public class Account { // 余额 private Long money; public synchronized void transfer(Account target, Long money) { synchronized(this) { (1) synchronized (target) { (2) this.money -= money; if (this.money < 0) { // throw exception } target.money += money; } } } }

若账户A转账给账户B100元,账户B同时也转账给账户A100元,当账户A转帐的线程A执行到了代码(1)处时,获取到了账户A对象的锁,同时账户B转账的线程B也执行到了代码(1)处时,获取到了账户B对象的锁。当线程A和线程B执行到了代码(2)处时,他们都在互相等待对方释放锁来获取,可是synchronized是阻塞锁,没有执行完代码块是不会释放锁的,就这样,线程A和线程B死死的对着,谁也不放过谁。等到了你去重启应用的那一天。。。这个现象就是死锁。
死锁的定义:一组互相竞争资源的线程因互相等待,导致“永久”阻塞的现象。
如下图:

死锁1.jpg

查找死锁信息

这里我先以一个基本会发生死锁的程序为例,创建两个线程,线程A获取到锁A后,休眠1秒后去获取锁B;线程B获取到锁B后 ,休眠1秒后去获取锁A。那么这样基本都会发生死锁的现象,代码如下:

public class DeadLock extends Thread { private String first; private String second; public DeadLock(String name, String first, String second) { super(name); // 线程名 this.first = first; this.second = second; } public void run() { synchronized (first) { System.out.println(this.getName() + " 获取到锁: " + first); try { Thread.sleep(1000L); //线程休眠1秒 synchronized (second) { System.out.println(this.getName() + " 获取到锁: " + second); } } catch (InterruptedException e) { // Do nothing } } } public static void main(String[] args) throws InterruptedException { String lockA = "lockA"; String lockB = "lockB"; DeadLock threadA = new DeadLock("ThreadA", lockA, lockB); DeadLock threadB = new DeadLock("ThreadB", lockB, lockA); threadA.start(); threadB.start(); threadA.join(); //等待线程1执行完 threadB.join(); } }

运行程序后将发生死锁,然后使用jps命令(jps.exe在jdk/bin目录下),命令如下:

C:\Program Files\Java\jdk1.8.0_221\bin>jps -l 24416 sun.tools.jps.Jps 24480 org.jetbrains.kotlin.daemon.KotlinCompileDaemon 1624 20360 org.jetbrains.jps.cmdline.Launcher 9256 9320 page2.DeadLock 18188

可以发现发生死锁的进程id 9320,然后使用jstack(jstack.exe在jdk/bin目录下)命令查看死锁信息。

C:\Program Files\Java\jdk1.8.0_221\bin>jstack 9320 "ThreadB" #13 prio=5 os_prio=0 tid=0x000000001e48c800 nid=0x51f8 waiting for monitor entry [0x000000001f38f000] java.lang.Thread.State: BLOCKED (on object monitor) at page2.DeadLock.run(DeadLock.java:19) - waiting to lock <0x000000076b99c198> (a java.lang.String) - locked <0x000000076b99c1d0> (a java.lang.String) "ThreadA" #12 prio=5 os_prio=0 tid=0x000000001e48c000 nid=0x3358 waiting for monitor entry [0x000000001f28f000] java.lang.Thread.State: BLOCKED (on object monitor) at page2.DeadLock.run(DeadLock.java:19) - waiting to lock <0x000000076b99c1d0> (a java.lang.String) - locked <0x000000076b99c198> (a java.lang.String)

这样我们就可以看到发生死锁的信息。虽然发现了死锁,但是解决死锁只能是重启应用了。

如何避免死锁的发生 1.固定的顺序来获得锁

如果所有线程以固定的顺序来获得锁,那么在程序中就不会出现锁顺序死锁问题。(取自《Java并发编程实战》一书)
要想验证锁顺序的一致性,有很多种方式,如果锁定的对象含有递增的id字段(唯一、不可变、具有可比性的),那么就好办多了,获取锁的顺序以id由小到大来排序。还是用转账的例子来解释,代码如下:

public class Account { // id (递增) private Integer id; // 余额 private Long money; public synchronized void transfer(Account target, Long money) { Account account1; Account account2; if (this.id < target.id) { account1 = this; account2 = target; } else { account1 = target; account2 = this; } synchronized(account1) { synchronized (account2) { this.money -= money; if (this.money < 0) { // throw exception } target.money += money; } } } }

若该对象并没有唯一、不可变、具有可比性的的字段(如:递增的id),那么可以使用 System.identityHashCode() 方法返回的哈希值来进行比较。比较方式可以和上面的例子一类似。System.identityHashCode()虽然会出现散列冲突,但是发生冲突的概率是非常低的。因此这项技术以最小的代价,换来了最大的安全性。
提示: 不管你是否重写了对象的hashCode方法,System.identityHashCode() 方法都只会返回默认的哈希值。

2.一次性申请所有资源

内容版权声明:除非注明,否则皆为本站原创文章。

转载注明出处:https://www.heiqu.com/zwgxfx.html