并发编程 --锁 --死锁

谈谈你对死锁的理解?

发生场景

死锁一定发生在并发场景中。我们为了保证线程安全,有时会给程序使用各种能保证并发安全的工具,尤其是锁,但是如果在使用过程中处理不得当,就有可能会导致发生死锁的情况。

定义

死锁是指两个或两个以上的线程在执行过程中,因争夺资源而造成的互相等待的现象,在无外力作用的情况下,这些线程会一直相互等待而无法继续运行下去。---《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 }
View Code

运行结果:

  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       });
View Code

运行结果:

  上代码让在线程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才会被从阻塞状态变为激活状态。所以资源的有序性破坏了资源的请求并持有条件和环路等待条件,因此避免了死锁。

posted @ 2020-04-01 09:45  JustJavaIt  阅读(170)  评论(0)    收藏  举报