多线程学习(1)产生死锁的条件和解决办法

70db8eccfe81db6ab347208afbd952b2ad3.jpg

死锁概念及产生原理

    概念:多个并发线程因争夺系统资源而产生相互等待的现象。

    原理: 在多线程环境下,某些资源具有互斥性,当被一个线程占用后,其他线程无法获取该资源;而一个线程为了完成一个完整的事务需要持有至少两个资源,且当线程已持有一个资源后发现其他所需资源被其他线程持有了,它只能选择等待其他线程释放资源,它再获取。而其他线程刚巧也在等待它释放占用的资源,那么它们就互相持有了对方需要的资源,且互相等待对方释放资源,导致死锁现象发生。

发生死锁的代码

/**
 * 测试多线程并发 死锁
 * 
 */
public class Test {
	public void method1() {
		synchronized (String.class) {
			System.out.println("print String.class");
			synchronized (Integer.class) {
				System.out.println("print Integer.class");
			}
		}
	}

	public void method2() {
		synchronized (Integer.class) {
			System.out.println("print Integer.class");
			synchronized (String.class) {
				System.out.println("print String.class");
			}
		}
	}

	public static void main(String[] args) {
		Thread1 thread1 = new Thread1();
		Thread2 thr2 = new Thread2();
		Thread thread2 = new Thread(thr2);
		thread1.start();
		thread2.start();
	}
}

public class Thread1 extends Thread {

	@Override
	public void run() {
		Test test1 = new Test();
		for (int i = 0; i < 100; i++) {
			test1.method1();
		}
	}
}

public class Thread2 implements Runnable {

	@Override
	public void run() {
		Test test2 = new Test();
		for (int i = 0; i < 100; i++) {
			test2.method2();
		}
	}

}

上面的例子中,经常容易发生死锁的情况,是同步块中嵌套同步快。 可以看到线程1想要锁定对象2,它由线程2持有,而线程2想要锁定对象1,它由线程1保持。由于没有线程愿意放弃,所以存在死锁并且Java程序被卡住了。

如何解决上面的死锁,可以通过调整资源的访问顺序,使method1和method2访问资源顺序一致,就不会造成彼此死锁。

死锁产生的4个必要条件

    1、互斥:某种资源一次只允许一个进程访问,即该资源一旦分配给某个进程,其他进程就不能再访问,直到该进程访问结束。
    2、占有且等待:一个进程本身占有资源(一种或多种),同时还有资源未得到满足,正在等待其他进程释放该资源。
    3、不可剥夺:别人已经占有了某项资源,你不能因为自己也需要该资源,就去把别人的资源抢过来。
    4、循环等待:在发生死锁时必然存在一个进程等待队列{P1,P2,…,Pn},其中P1等待P2占有的资源,P2等待P3占有的资源,…,Pn等待P1占有的资源,形成一个进程等待环路,环路中每一个进程所占有的资源同时被另一个申请,也就是前一个进程占有后一个进程所深情地资源。
       当以上四个条件均满足,必然会造成死锁,发生死锁的进程无法进行下去,它们所持有的资源也无法释放。这样会导致CPU的吞吐量下降。所以死锁情况是会浪费系统资源和影响计算机的使用性能的。那么,解决死锁问题就是相当有必要的了。

        数据库的设计中会检测死锁的发生,并尝试解决死锁。而JAVA虚拟机,没办法自动解决死锁,死锁的线程会占用性能,知道项目重新启动。我们可以通过人工使用虚拟机工具会查到发生死锁的线程,并通过虚拟机命令强制结束发生死锁的线程。

避免死锁的方法

1、死锁预防 ----- 确保系统永远不会进入死锁状态
     产生死锁需要四个条件,那么,只要这四个条件中至少有一个条件得不到满足,就不可能发生死锁了。由于互斥条件是非共享资源所必须的,不仅不能改变,还应加以保证,所以,主要是破坏产生死锁的其他三个条件。
a、破坏“占有且等待”条件
     方法1:所有的进程在开始运行之前,必须一次性地申请其在整个运行过程中所需要的全部资源。
         优点:简单易实施且安全。
         缺点:因为某项资源不满足,进程无法启动,而其他已经满足了的资源也不会得到利用,严重降低了资源的利用率,造成资源浪费。使进程经常发生饥饿现象。
     方法2:该方法是对第一种方法的改进,允许进程只获得运行初期需要的资源,便开始运行,在运行过程中逐步释放掉分配到的已经使用完毕的资源,然后再去请求新的资源。这样的话,资源的利用率会得到提高,也会减少进程的饥饿问题。

第一种方法静态分配即每个进程在开始执行时就申请他所需要的全部资源。第二种是动态分配即每个进程在申请所需要的资源时他本身不占用系统资源。

b、破坏“不可抢占”条件
        一个进程不能获得所需要的全部资源时便处于等待状态,等待期间他占有的资源将被隐式的释放重新加入到 系统的资源列表中,可以被其他的进程使用,而等待的进程只有重新获得自己原有的资源以及新申请的资源才可以重新启动,执行。
      该种方法实现起来比较复杂,且代价也比较大。释放已经保持的资源很有可能会导致进程之前的工作失效等,反复的申请和释放资源会导致进程的执行被无限的推迟,这不仅会延长进程的周转周期,还会影响系统的吞吐量。
c、破坏“循环等待”条件
     可以通过定义资源类型的线性顺序来预防,可将每个资源编号,当一个进程占有编号为i的资源时,那么它下一次申请资源只能申请编号大于i的资源。如图所示:
4f7aaebd2abccc5e537acdb65ffabfd7b05.jpg

这样虽然避免了循环等待,但是这种方法是比较低效的,资源的执行速度回变慢,并且可能在没有必要的情况下拒绝资源的访问,比如说,进程c想要申请资源1,如果资源1并没有被其他进程占有,此时将它分配个进程c是没有问题的,但是为了避免产生循环等待,该申请会被拒绝,这样就降低了资源的利用率。

2、避免死锁 ----- 在使用前进行判断,只允许不会产生死锁的进程申请资源
死锁避免的基本思想:系统对进程发出的每一个系统能够满足的资源申请进行动态检查,并根据检查结果决定是否分配资源,如果分配后系统可能发生死锁,则不予分配,否则予以分配,这是一种保证系统不进入死锁状态的动态策略。

如果操作系统能保证所有进程在有限时间内得到需要的全部资源,则系统处于安全状态否则系统是不安全的。
安全状态是指:如果系统存在 由所有的安全序列{P1,P2,…Pn},则系统处于安全状态。一个进程序列是安全的,如果对其中每一个进程Pi(i >=1 && i <= n)他以后尚需要的资源不超过系统当前剩余资源量与所有进程Pj(j < i)当前占有资源量之和,系统处于安全状态则不会发生死锁。
不安全状态:如果不存在任何一个安全序列,则系统处于不安全状态。他们之间的对对应关系如下图所示: 
 è¿éåå¾çæè¿°
下面我们来通过一个例子对安全状态和不安全状态进行更深的了解 

å®å¨ç¶æ

如上图所示系统处于安全状态,系统剩余3个资源,可以把其中的2个分配给P3,此时P3已经获得了所有的资源,执行完毕后还能还给系统4个资源,此时系统剩余5个资源所以满足(P2所需的资源不超过系统当前剩余量与P3当前占有资源量之和),同理P1也可以在P2执行完毕后获得自己需要的资源。 
如果P1提出再申请一个资源的要求,系统从剩余的资源中分配一个给进程P1,此时系统剩余2个资源,新的状态图如下:那么是否仍是安全序列呢那我们来分析一下 

è¿éåå¾çæè¿°

系统当前剩余2个资源,分配给P3后P3执行完毕还给系统4个资源,但是P2需要5个资源,P1需要6个资源,他们都无法获得资源执行完成,因此找不到一个安全序列。此时系统转到了不安全状态。

 

两种避免办法:
    1、如果一个进程的请求会导致死锁,则不启动该进程
    2、如果一个进程的增加资源请求会导致死锁 ,则拒绝该申请。

避免死锁的具体实现通常利用银行家算法
    银行家算法
a、银行家算法的相关数据结构
    可利用资源向量Available:用于表示系统里边各种资源剩余的数目。由于系统里边拥有的资源通常都是有很多种(假设有m种),所以,我们用一个有m个元素的数组来表示各种资源。数组元素的初始值为系统里边所配置的该类全部可用资源的数目,其数值随着该类资源的分配与回收动态地改变。
    最大需求矩阵Max:用于表示各个进程对各种资源的额最大需求量。进程可能会有很多个(假设为n个),那么,我们就可以用一个nxm的矩阵来表示各个进程多各种资源的最大需求量
    分配矩阵Allocation:顾名思义,就是用于表示已经分配给各个进程的各种资源的数目。也是一个nxm的矩阵。
    需求矩阵Need:用于表示进程仍然需要的资源数目,用一个nxm的矩阵表示。系统可能没法一下就满足了某个进程的最大需求(通常进程对资源的最大需求也是只它在整个运行周期中需要的资源数目,并不是每一个时刻都需要这么多),于是,为了进程的执行能够向前推进,通常,系统会先分配个进程一部分资源保证进程能够执行起来。那么,进程的最大需求减去已经分配给进程的数目,就得到了进程仍然需要的资源数目了。

银行家算法通过对进程需求、占有和系统拥有资源的实时统计,确保系统在分配给进程资源不会造成死锁才会给与分配。
死锁避免的优点:不需要死锁预防中的抢占和重新运行进程,并且比死锁预防的限制要少。
死锁避免的限制:
    必须事先声明每个进程请求的最大资源量
    考虑的进程必须无关的,也就是说,它们执行的顺序必须没有任何同步要求的限制
    分配的资源数目必须是固定的。
    在占有资源时,进程不能退出

3、死锁检测与解除 ----- 在检测到运行系统进入死锁,进行恢复。

    允许系统进入到死锁状态

    死锁检测

下图截自《操作系统--精髓与设计原理》

20180513224342642.

死锁的解除
如果利用死锁检测算法检测出系统已经出现了死锁 ,那么,此时就需要对系统采取相应的措施。常用的解除死锁的方法:
1、抢占资源:从一个或多个进程中抢占足够数量的资源分配给死锁进程,以解除死锁状态。
2、终止(或撤销)进程:终止或撤销系统中的一个或多个死锁进程,直至打破死锁状态。
    a、终止所有的死锁进程。这种方式简单粗暴,但是代价很大,很有可能会导致一些已经运行了很久的进程前功尽弃。
    b、逐个终止进程,直至死锁状态解除。该方法的代价也很大,因为每终止一个进程就需要使用死锁检测来检测系统当前是否处于死锁状态。另外,每次终止进程的时候终止那个进程呢?每次都应该采用最优策略来选择一个“代价最小”的进程来解除死锁状态。一般根据如下几个方面来决定终止哪个进程:
    进程的优先级
    进程已运行时间以及运行完成还需要的时间
    进程已占用系统资源
    进程运行完成还需要的资源
    终止进程数目
    进程是交互还是批处理

避免死锁的方式

  既然可能产生死锁,那么接下来,讲一下如何避免死锁。

1、让程序每次至多只能获得一个锁。当然,在多线程环境下,这种情况通常并不现实

2、设计时考虑清楚锁的顺序,尽量减少嵌在的加锁交互数量

3、既然死锁的产生是两个线程无限等待对方持有的锁,那么只要等待时间有个上限不就好了。当然synchronized不具备这个功能,但是我们可以使用Lock类中的tryLock方法去尝试获取锁,这个方法可以指定一个超时时限,在等待超过该时限之后变回返回一个失败信息

转载于:https://my.oschina.net/xiaoyoung/blog/3029795

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值