并发编程 --锁 --死锁
谈谈你对死锁的理解?
发生场景
死锁一定发生在并发场景中。我们为了保证线程安全,有时会给程序使用各种能保证并发安全的工具,尤其是锁,但是如果在使用过程中处理不得当,就有可能会导致发生死锁的情况。
定义
死锁是指两个或两个以上的线程在执行过程中,因争夺资源而造成的互相等待的现象,在无外力作用的情况下,这些线程会一直相互等待而无法继续运行下去。---《Java并发编程之美》

图中线程A己经持有了资源2,它同时还想申请资源l,线程B已经持有了资源l它同时还想申请资源2,所以线程l和线程2就因为相互等待对方已经持有的资源,而进入了死锁状态。
影响
死锁的影响在不同系统中是不一样的,影响的大小一部分取决于当前这个系统或者环境对死锁的处理能力。
数据库中
例如,在数据库系统软件的设计中,考虑了监测死锁以及从死锁中恢复的情况。在执行一个事务的时候可能需要获取多把锁,并一直持有这些锁直到事务完成。在某个事务中持有的锁可能在其他事务中也需要,因此在两个事务之间有可能发生死锁的情况,一旦发生了死锁,如果没有外部干涉,那么两个事务就会永远的等待下去。但数据库系统不会放任这种情况发生,当数据库检测到这一组事务发生了死锁时,根据策略的不同,可能会选择放弃某一个事务,被放弃的事务就会释放掉它所持有的锁,从而使其他的事务继续顺利进行。此时程序可以重新执行被强行终止的事务,而这个事务现在就可以顺利执行了,因为所有跟它竞争资源的事务都已经在刚才执行完毕,并且释放资源了。
JVM 中
在 JVM 中,对于死锁的处理能力就不如数据库那么强大了。如果在 JVM 中发生了死锁,JVM 并不会自动进行处理,所以一旦死锁发生,就会陷入无穷的等待。
死锁发生必须满足的条件?
必须满足以下四个条件:
(1)互斥条件:线程对己经获取到的资源进行排它性使用,即该资源同时只由一个线程占用。如果此时还有其他线程请求获取该资源,则请求者只能等待,直至占有资源的线程释放该资源。
(2)请求并持有条件:指一个线程己经持有了至少一个资源,但又提出了新的资源请求,而新资源己被其他线程占有,所以当前线程会被阻塞,但阻塞的同时并不释放自己己经获取的资源。
(3)不可剥夺条件:指线程获取到的资源在自己使用完之前不能被其他线程抢占,只有在自己使用完毕后才由自己释放该资源。
(4)环路等待条件:指在发生死锁时,必然存在一个线程→资源的环形链,即线程集合{TO,T1,T2,…,Tn}中的TO正在等待一个Tl占用的资源,Tl正在等待T2占用的资源,……Tn正在等待己被TO占用的资源。下面通过一个例子来说明线程死锁:
避免死锁最简单的方法就是阻止循环等待条件,将系统中所有的资源设置标志位、排序,规定所有的进程申请资源必须以一定的顺序(升序或降序)做操作来避免死锁。
手动模拟一个死锁?
1 public class DeadLockTest { 2 public static Object resourceA = new Object(); 3 public static Object resourceB = new Object(); 4 5 public static void main(String[] args) { 6 7 //创建线程A 8 Thread threadA = new Thread(new Runnable() { 9 @Override 10 public void run() { 11 synchronized (resourceA){ 12 System.out.println(Thread.currentThread() + "get resourceA"); 13 try { 14 Thread.sleep(1000); 15 } catch (InterruptedException e) { 16 e.printStackTrace(); 17 } 18 System.out.println(Thread.currentThread() + "waiting get resourceB"); 19 synchronized (resourceB){ 20 System.out.println(Thread.currentThread() + "get resourceB"); 21 } 22 } 23 } 24 }); 25 26 //创建线程B 27 Thread threadB = new Thread(new Runnable() { 28 @Override 29 public void run() { 30 synchronized (resourceB){ 31 System.out.println(Thread.currentThread() + "get ResourceB"); 32 try { 33 Thread.sleep(1000); 34 } catch (InterruptedException e) { 35 e.printStackTrace(); 36 } 37 System.out.println(Thread.currentThread() + "waiting get resourceA"); 38 synchronized (resourceA){ 39 System.out.println(Thread.currentThread() + "get resourceA"); 40 } 41 } 42 43 } 44 }); 45 46 47 threadA.start(); 48 threadB.start(); 49 } 50 51 }
运行结果:

Thread-0是线程A,Thread-1是线程B,代码首先创建了两个资源,并创建了两个线程。从输出结果可以知道,线程调度器先调度了线程A,也就是把CPU资源分配了线程A,线程A使用synchronized(resourceA)方法获取到了resourceA的监视器锁,然后调用sleep数休眠ls休眠ls是为了保证线程A在获取resourceB对应的锁前让线程B抢占到CPU,获取到资源resourceB上的锁。线程A调用sleep方法后线程B会执行synchronized (resourceB)方法,这代表线程B获取到了resourceB对象监视器锁资源,然后调用sleep函数休眠ls。好了,到了这里线程A获取到了resourceA资源,线程B获取到了resourceB资源。线程A休眠结束后会企图获取resourceB资源,而resourceB资源被线程B所持有,所以线程A会被阻塞而等待。而同时线程B休眠结束后会企图获取resourceA资源,而resourceA源己经被线程A持有,所以线程A和程B就陷入了相互等待的状态,也就产生了死锁。
下面谈谈本例是如满足死锁的四个条件的。
首先,resourceA和rsourceB都是互斥资源,当线程A调用synchronized(resourceA)方法获取到resourceA上的监视器锁并释放前,线程B再调用synchronized(resourceA)方法尝试获取该资源会被阻塞,只有线程A主动释放该锁,线程B才能获得,这满足了资源互斥条件。
线程A首先通过synchronized(resourceA)方法获取到resourceA上的监视器锁资源,然后通过synchronized (resourceB)方法等待获取resourceB上的监视器锁资源,这就构成了请求并持有条件。
线程A在获取resourceA上的监视器锁资源后,该资源不会被线程B掠夺走,只有线程A自己主动释放reourceA资源时,它才会放弃对该资源的持有权,这构成了资源的不可剥夺条件。
线程A持有objectA资源并等待获取objectB资源,而线程B持有objectB资源并等待objectA资源,这构成了环路等待条件。所以线程A和线程就进入了死锁状态。
如何避免线程死锁
要想避免死锁,只需要破坏掉至少一个构造死锁的必要条件即可,但是学操作系统的读者应该都知道,目前只有请求并持有和环路等待条件是可以被破坏的。
造成死锁的原因其实和申请资源的顺序有很大关系,使用资源申请的有序性原则就可以避免死锁,那么什么是资源申请的有序性呢?我们对上面线程B的代码进行如下修改:
1 Thread threadB = new Thread(new Runnable() { 2 @Override 3 public void run() { 4 synchronized (resourceA){ 5 System.out.println(Thread.currentThread() + "get ResourceA"); 6 try { 7 Thread.sleep(1000); 8 } catch (InterruptedException e) { 9 e.printStackTrace(); 10 } 11 System.out.println(Thread.currentThread() + "waiting get resourceB"); 12 synchronized (resourceB){ 13 System.out.println(Thread.currentThread() + "get resourceB"); 14 } 15 } 16 17 } 18 });
运行结果:

上代码让在线程B中获取资源的顺序和在线程A中获取资源的顺序保持一致,其实资源分配有序性就是指,假如线程A和线程B都需要资源1,2,3,...,n时,对资源进行排序,线程A和线程B只有在获取了资源n-1时才能去获取资源n。
我们可以简单分析一下为何资源的有序分配会避免死锁,比如上面的代码,假如线程A和线程B同时执行到了synchronized(resourceA),只有一个线程可以获取到resourceA上的监视器锁,假如线程A获取到了,那么线程B就会被阻塞而不会再去获取资源B,线程A获取到resourceA的监视器锁后会去申请resourceB的监视器锁资源,这时候线程A是可以获取到的,线程A获取到resourceB资源并使用后会放弃对资源resourceB的持有,然后再释放对resourceA的持有,释放resourceA后线程B才会被从阻塞状态变为激活状态。所以资源的有序性破坏了资源的请求并持有条件和环路等待条件,因此避免了死锁。

浙公网安备 33010602011771号