6.11锁问题的定位和修复
目录介绍
- 01.代码模拟产生死锁
- 1.1 代码描述
- 1.2 产生死锁代码
- 1.3 死锁发生的场景
- 02.Java中导致死锁的原因
- 2.1 造成死锁原因
- 2.2 死锁的危害
- 03.出现死锁需要满足条件
- 3.1 死锁问题条件
- 3.2 如何预防死锁
- 04.死锁诊断步骤
- 4.1 如何定位死锁
- 05.死锁修复解决方案
- 5.1 死锁修复方案
- 06.手写死锁代码
- 07.死锁的三种类型案例
- 7.1 死锁产生的条件
- 7.2 静态的锁顺序死锁
- 7.3 动态的锁顺序死锁
- 7.4 协作对象之间发生的死锁
首先思考问题
- 死锁的概念和产生死锁的根本原因是什么?死锁的预防策略中资源有序分配策略是什么。
- 理解产生死锁的必要条件--以下四个条件同时具备:互斥条件、不可抢占条件、占有且申请条件、循环等待条件。这几个条件之间有什么关系。
- 死锁的检测及恢复的思想。死锁是如何解决和避免的。结合具体案例分析一下思路?
- Android实际开发中是否有遇到过死锁,在什么情景下遇到的,当时是怎么排查问题并且解决的?
01.代码模拟产生死锁
1.1 代码描述
- 启动两个线程,设置两个线程监听对象obj1 、obj2。
- 线程1启动的时候,先获取obj1锁,暂停1秒,然后获取obj2锁。
- 线程2启动时,先获取obj2,再获取obj1
- obj1和obj2如何产生死锁
- 当线程2启动的时候获取obj2成功,然后去获取obj1的时候,obj1被线程1占用,此时就等待。
- 线程1秒后去获取obj2,此时obj2锁被线程2握着,产生死锁,互相无法获取。
1.2 产生死锁代码
- 代码如下所示
- 这种方法将直接导致死锁
private final Object obj1 = new Object(); private final Object obj2 = new Object(); private void test1() { new Thread(){ @Override public void run() { synchronized (obj1){ try { System.out.println("yc---Thread1 obj1"); Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } synchronized (obj2){ System.out.println("yc---Thread1 obj2"); } } } }.start(); new Thread(){ @Override public void run() { synchronized (obj2){ System.out.println("yc---Thread2 obj2"); synchronized (obj1){ System.out.println("yc---Thread2 obj1"); } } } }.start(); } //打印结果: 10-18 17:11:34.255 30427-30478/com.yc.cn.ycbaseadapter I/System.out: yc---Thread1 obj1 10-18 17:11:34.257 30427-30479/com.yc.cn.ycbaseadapter I/System.out: yc---Thread2 obj2
- 这种方法将直接导致死锁
- 接下来去掉上面代码中的Thread.sleep(1000)代码
- 注意去掉了这行代码后,开始时可以正常打印日志,多次触发test1()方法后,发现最终也会出现死锁问题。
private final Object obj1 = new Object(); private final Object obj2 = new Object(); private void test1() { new Thread(){ @Override public void run() { synchronized (obj1){ System.out.println("yc---Thread1 obj1"); synchronized (obj2){ System.out.println("yc---Thread1 obj2"); } } } }.start(); new Thread(){ @Override public void run() { synchronized (obj2){ System.out.println("yc---Thread2 obj2"); synchronized (obj1){ System.out.println("yc---Thread2 obj1"); } } } }.start(); } //打印日志 10-18 17:35:44.662 4341-4412/com.yc.cn.ycbaseadapter I/System.out: yc---Thread1 obj1 10-18 17:35:44.662 4341-4412/com.yc.cn.ycbaseadapter I/System.out: yc---Thread1 obj2 10-18 17:35:44.663 4341-4413/com.yc.cn.ycbaseadapter I/System.out: yc---Thread2 obj2 10-18 17:35:44.663 4341-4413/com.yc.cn.ycbaseadapter I/System.out: yc---Thread2 obj1 10-18 17:35:45.967 4341-4417/com.yc.cn.ycbaseadapter I/System.out: yc---Thread1 obj1 10-18 17:35:45.968 4341-4417/com.yc.cn.ycbaseadapter I/System.out: yc---Thread1 obj2 10-18 17:35:45.968 4341-4418/com.yc.cn.ycbaseadapter I/System.out: yc---Thread2 obj2 10-18 17:35:45.968 4341-4418/com.yc.cn.ycbaseadapter I/System.out: yc---Thread2 obj1 10-18 17:35:46.643 4341-4422/com.yc.cn.ycbaseadapter I/System.out: yc---Thread1 obj1 10-18 17:35:46.643 4341-4422/com.yc.cn.ycbaseadapter I/System.out: yc---Thread1 obj2 10-18 17:35:46.647 4341-4423/com.yc.cn.ycbaseadapter I/System.out: yc---Thread2 obj2 10-18 17:35:46.647 4341-4423/com.yc.cn.ycbaseadapter I/System.out: yc---Thread2 obj1 10-18 17:35:46.913 4341-4424/com.yc.cn.ycbaseadapter I/System.out: yc---Thread1 obj1 10-18 17:35:46.913 4341-4424/com.yc.cn.ycbaseadapter I/System.out: yc---Thread1 obj2 10-18 17:35:46.913 4341-4425/com.yc.cn.ycbaseadapter I/System.out: yc---Thread2 obj2 10-18 17:35:46.914 4341-4425/com.yc.cn.ycbaseadapter I/System.out: yc---Thread2 obj1 10-18 17:35:47.117 4341-4426/com.yc.cn.ycbaseadapter I/System.out: yc---Thread1 obj1 10-18 17:35:47.118 4341-4427/com.yc.cn.ycbaseadapter I/System.out: yc---Thread2 obj2
- 注意去掉了这行代码后,开始时可以正常打印日志,多次触发test1()方法后,发现最终也会出现死锁问题。
1.3 死锁发生的场景
- 死锁不仅仅是在线程之间会发生,存在资源独占的进程之间同样也可能出现死锁。
- 大多是聚焦在多线程场景中的死锁,指两个或多个线程之间,由于互相持有对方需要的锁,而永久处于阻塞的状态。
02.Java中导致死锁的原因
2.1 造成死锁原因
- 在申请锁时发生了交叉闭环申请。即线程在获得了锁1并且没有释放的情况下去申请锁2,这时,另一个线程已经获得了锁2,在释放锁2之前又要先获得锁1,因此闭环发生,陷入死锁循环。
image
2.2 死锁的危害
- 从上面死锁代码案例可以知道,当发生死锁的时候,导致彼此一直处于等待之中,而导致代码无法执行下去。只能重启,后果比较严重!
- 在死锁时,线程间相互等待资源,而又不释放自身的资源,导致无穷无尽的等待,其结果是系统任务永远无法执行完成。系统发生死锁现象不仅浪费大量的系统资源,甚至导致整个系统崩溃,带来灾难性后果。
3.出现死锁需要满足条件
3.1 死锁问题条件
- 互斥条件:一个资源每次只能被一个线程使用。
- 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
- 不剥夺条件:进程已获得的资源,在未使用完之前,不能强行剥夺。
- 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。
3.2 如何预防死锁
- 死锁发生时的四个必要条件,只要破坏这四个必要条件中的任意一个条件,死锁就不会发生。这就为我们解决死锁问题提供了可能。一般地,解决死锁的方法分为死锁的预防,避免,检测[定位死锁的位置]与恢复三种(注意:死锁的检测与恢复是一个方法)。 锁的预防是保证系统不进入死锁状态的一种策略。它的基本思想是要求进程申请资源时遵循某种协议,从而打破产生死锁的四个必要条件中的一个或几个,保证系统不会进入死锁状态。
- 打破互斥条件。即允许进程同时访问某些资源。但是,有的资源是不允许被同时访问的,像打印机等等,这是由资源本身的属性所决定的。所以,这种办法并无实用价值。
- 打破不可抢占条件。即允许进程强行从占有者那里夺取某些资源。就是说,当一个进程已占有了某些资源,它又申请新的资源,但不能立即被满足时,它必须释放所占有的全部资源,以后再重新申请。它所释放的资源可以分配给其它进程。这就相当于该进程占有的资源被隐蔽地强占了。这种预防死锁的方法实现起来困难,会降低系统性能。
- 打破占有且申请条件。可以实行资源预先分配策略。即进程在运行前一次性地向系统申请它所需要的全部资源。如果某个进程所需的全部资源得不到满足,则不分配任何资源,此进程暂不运行。只有当系统能够满足当前进程的全部资源需求时,才一次性地将所申请的资源全部分配给该进程。由于运行的进程已占有了它所需的全部资源,所以不会发生占有资源又申请资源的现象,因此不会发生死锁。但是,这种策略也有如下缺点:
- 在许多情况下,一个进程在执行之前不可能知道它所需要的全部资源。这是由于进程在执行时是动态的,不可预测的;
- 资源利用率低。无论所分资源何时用到,一个进程只有在占有所需的全部资源后才能执行。即使有些资源最后才被该进程用到一次,但该进程在生存期间却一直占有它们,造成长期占着不用的状况。这显然是一种极大的资源浪费;
- 降低了进程的并发性。因为资源有限,又加上存在浪费,能分配到所需全部资源的进程个数就必然少了。
- 打破循环等待条件,实行资源有序分配策略。采用这种策略,即把资源事先分类编号,按号分配,使进程在申请,占用资源时不会形成环路。所有进程对资源的请求必须严格按资源序号递增的顺序提出。进程占用了小号资源,才能申请大号资源,就不会产生环路,从而预防了死锁。这种策略与前面的策略相比,资源的利用率和系统吞吐量都有很大提高,但是也存在以下缺点:
- 限制了进程对资源的请求,同时给系统中所有资源合理编号也是件困难事,并增加了系统开销;
- 为了遵循按编号申请的次序,暂不使用的资源也需要提前申请,从而增加了进程对资源的占用时间。
4.死锁诊断步骤
4.1 如何定位死锁
- 定位死锁最常用的工具就是利用jstack等工具获取线程栈,然后定位相互之间的依赖关系,进而找到死锁。如果是比较明显的死锁,往往jstack工具就能直接定位,类似JConsole甚至可以在图形界面进行有限的死锁检测。
- 如果程序运行时发生了死锁,绝大多数情况下都是无法在线解决的,只能重启、修正程序本身问题。所以,代码开发阶段相互审查,或者利用工具进行预防性排查,也是很重要的。
- 1.首先,可以使用 jps 或者系统的 ps 命令、任务管理器等工具,确定进程 ID。
- 2.调用 jstack 获取线程栈:jstack your-pid
- 3.然后看看日志,思考怎么用studio将日志打印出来呢?
5.死锁修复解决方案
5.1 死锁修复方案
- 如果在死锁检查时发现了死锁情况,那么就要努力消除死锁,使系统从死锁状态中恢复过来。消除死锁的几种方式:
- 1.最简单、最常用的方法就是进行系统的重新启动,不过这种方法代价很大,它意味着在这之前所有的进程已经完成的计算工作都将付之东流,包括参与死锁的那些进程,以及未参与死锁的进程;
- 2.撤消进程,剥夺资源。终止参与死锁的进程,收回它们占有的资源,从而解除死锁。这时又分两种情况:一次性撤消参与死锁的全部进程,剥夺全部资源;或者逐步撤消参与死锁的进程,逐步收回死锁进程占有的资源。一般来说,选择逐步撤消的进程时要按照一定的原则进行,目的是撤消那些代价最小的进程,比如按进程的优先级确定进程的代价;考虑进程运行时的代价和与此进程相关的外部作业的代价等因素;
- 3.进程回退策略,即让参与死锁的进程回退到没有发生死锁前某一点处,并由此点处继续执行,以求再次执行时不再发生死锁。虽然这是个较理想的办法,但是操作起来系统开销极大,要有堆栈这样的机构记录进程的每一步变化,以便今后的回退,有时这是无法做到的。
6.手写死锁代码
- 死锁的简单代码
- 思路是创建两个字符串a和b,再创建两个线程A和B,让每个线程都用synchronized锁住字符串(A先锁a,再去锁b;B先锁b,再锁a),如果A锁住a,B锁住b,A就没办法锁住b,B也没办法锁住a,这时就陷入了死锁。
- 打印结果:可以看到,Lock1获取obj1,Lock2获取obj2,但是它们都没有办法再获取另外一个obj,因为它们都在等待对方先释放锁,这时就是死锁。
public class DeadLock { public static String obj1 = "obj1"; public static String obj2 = "obj2"; public static void main(String[] args){ Thread a = new Thread(new Lock1()); Thread b = new Thread(new Lock2()); a.start(); b.start(); } } class Lock1 implements Runnable{ @Override public void run(){ try{ System.out.println("Lock1 running"); while(true){ synchronized(DeadLock.obj1){ System.out.println("Lock1 lock obj1"); Thread.sleep(3000);//获取obj1后先等一会儿,让Lock2有足够的时间锁住obj2 synchronized(DeadLock.obj2){ System.out.println("Lock1 lock obj2"); } } } }catch(Exception e){ e.printStackTrace(); } } } class Lock2 implements Runnable{ @Override public void run(){ try{ System.out.println("Lock2 running"); while(true){ synchronized(DeadLock.obj2){ System.out.println("Lock2 lock obj2"); Thread.sleep(3000); synchronized(DeadLock.obj1){ System.out.println("Lock2 lock obj1"); } } } }catch(Exception e){ e.printStackTrace(); } } }
- 如果我们只运行Lock1呢?修改一下main函数,把线程b注释掉。
07.死锁的三种类型案例
7.1 死锁产生的条件
- 一般来说,要出现死锁问题需要满足以下条件:
- 1.互斥条件:一个资源每次只能被一个线程使用。
- 2.请求与保持条件:一个线程因请求资源而阻塞时,对已获得的资源保持不放。
- 3.不剥夺条件:线程已获得的资源,在未使用完之前,不能强行剥夺。
- 4.循环等待条件:若干线程之间形成一种头尾相接的循环等待资源关系。
- 在JAVA编程中,有3种典型的死锁类型:
- 静态的锁顺序死锁,动态的锁顺序死锁,协作对象之间发生的死锁。
7.2 静态的锁顺序死锁
- a和b两个方法都需要获得A锁和B锁。
- 一个线程执行a方法且已经获得了A锁,在等待B锁;另一个线程执行了b方法且已经获得了B锁,在等待A锁。这种状态,就是发生了静态的锁顺序死锁。
//可能发生静态锁顺序死锁的代码 class StaticLockOrderDeadLock { private final Object lockA = new Object(); private final Object lockB = new Object(); public void a() { synchronized (lockA) { synchronized (lockB) { System.out.println("function a"); } } } public void b() { synchronized (lockB) { synchronized (lockA) { System.out.println("function b"); } } } }
- 一个线程执行a方法且已经获得了A锁,在等待B锁;另一个线程执行了b方法且已经获得了B锁,在等待A锁。这种状态,就是发生了静态的锁顺序死锁。
- 解决静态的锁顺序死锁的方法就是:所有需要多个锁的线程,都要以相同的顺序来获得锁。
//正确的代码 class StaticLockOrderDeadLock { private final Object lockA = new Object(); private final Object lockB = new Object(); public void a() { synchronized (lockA) { synchronized (lockB) { System.out.println("function a"); } } } public void b() { synchronized (lockA) { synchronized (lockB) { System.out.println("function b"); } } } }
7.3 动态的锁顺序死锁
- 动态的锁顺序死锁是指两个线程调用同一个方法时,传入的参数颠倒造成的死锁。
- 如下代码,一个线程调用了transferMoney方法并传入参数accountA,accountB;另一个线程调用了transferMoney方法并传入参数accountB,accountA。此时就可能发生在静态的锁顺序死锁中存在的问题,即:第一个线程获得了accountA锁并等待accountB锁,第二个线程获得了accountB锁并等待accountA锁。
//可能发生动态锁顺序死锁的代码 class DynamicLockOrderDeadLock { public void transefMoney(Account fromAccount, Account toAccount, Double amount) { synchronized (fromAccount) { synchronized (toAccount) { //... fromAccount.minus(amount); toAccount.add(amount); //... } } } }
- 如下代码,一个线程调用了transferMoney方法并传入参数accountA,accountB;另一个线程调用了transferMoney方法并传入参数accountB,accountA。此时就可能发生在静态的锁顺序死锁中存在的问题,即:第一个线程获得了accountA锁并等待accountB锁,第二个线程获得了accountB锁并等待accountA锁。
- 动态的锁顺序死锁解决方案如下:使用System.identifyHashCode来定义锁的顺序。确保所有的线程都以相同的顺序获得锁。
//正确的代码 class DynamicLockOrderDeadLock { private final Object myLock = new Object(); public void transefMoney(final Account fromAccount, final Account toAccount, final Double amount) { class Helper { public void transfer() { //... fromAccount.minus(amount); toAccount.add(amount); //... } } int fromHash = System.identityHashCode(fromAccount); int toHash = System.identityHashCode(toAccount); if (fromHash < toHash) { synchronized (fromAccount) { synchronized (toAccount) { new Helper().transfer(); } } } else if (fromHash > toHash) { synchronized (toAccount) { synchronized (fromAccount) { new Helper().transfer(); } } } else { synchronized (myLock) { synchronized (fromAccount) { synchronized (toAccount) { new Helper().transfer(); } } } } } }
7.4 协作对象之间发生的死锁
- 有时,死锁并不会那么明显,比如两个相互协作的类之间的死锁
- 比如下面的代码:一个线程调用了Taxi对象的setLocation方法,另一个线程调用了Dispatcher对象的getImage方法。此时可能会发生,第一个线程持有Taxi对象锁并等待Dispatcher对象锁,另一个线程持有Dispatcher对象锁并等待Taxi对象锁。
//可能发生死锁 class Taxi { private Point location, destination; private final Dispatcher dispatcher; public Taxi(Dispatcher dispatcher) { this.dispatcher = dispatcher; } public synchronized Point getLocation() { return location; } public synchronized void setLocation(Point location) { this.location = location; if (location.equals(destination)) dispatcher.notifyAvailable(this);//外部调用方法,可能等待Dispatcher对象锁 } } class Dispatcher { private final Set<Taxi> taxis; private final Set<Taxi> availableTaxis; public Dispatcher() { taxis = new HashSet<Taxi>(); availableTaxis = new HashSet<Taxi>(); } public synchronized void notifyAvailable(Taxi taxi) { availableTaxis.add(taxi); } public synchronized Image getImage() { Image image = new Image(); for (Taxi t : taxis) image.drawMarker(t.getLocation());//外部调用方法,可能等待Taxi对象锁 return image; } }
- 上面的代码中, 我们在持有锁的情况下调用了外部的方法,这是非常危险的(可能发生死锁)。为了避免这种危险的情况发生, 我们使用开放调用。如果调用某个外部方法时不需要持有锁,我们称之为开放调用。
- 比如下面的代码:一个线程调用了Taxi对象的setLocation方法,另一个线程调用了Dispatcher对象的getImage方法。此时可能会发生,第一个线程持有Taxi对象锁并等待Dispatcher对象锁,另一个线程持有Dispatcher对象锁并等待Taxi对象锁。
- 解决协作对象之间发生的死锁:需要使用开放调用,即避免在持有锁的情况下调用外部的方法。
//正确的代码 class Taxi { private Point location, destination; private final Dispatcher dispatcher; public Taxi(Dispatcher dispatcher) { this.dispatcher = dispatcher; } public synchronized Point getLocation() { return location; } public void setLocation(Point location) { boolean flag = false; synchronized (this) { this.location = location; flag = location.equals(destination); } if (flag) dispatcher.notifyAvailable(this);//使用开放调用 } } class Dispatcher { private final Set<Taxi> taxis; private final Set<Taxi> availableTaxis; public Dispatcher() { taxis = new HashSet<Taxi>(); availableTaxis = new HashSet<Taxi>(); } public synchronized void notifyAvailable(Taxi taxi) { availableTaxis.add(taxi); } public Image getImage() { Set<Taxi> copy; synchronized (this) { copy = new HashSet<Taxi>(taxis); } Image image = new Image(); for (Taxi t : copy) image.drawMarker(t.getLocation());//使用开放调用 return image; } }
7.5 3种死锁的总结
综上,是常见的3种死锁的类型。
即:静态的锁顺序死锁,动态的锁顺序死锁,协作对象之间的死锁。在写代码时,要确保线程在获取多个锁时采用一致的顺序。同时,要避免在持有锁的情况下调用外部方法。
什么情况下Java程序会产生死锁?如何定位、修复?
- https://time.geekbang.org/column/article/9266