java多线程

多线程之死锁

2019-11-09  本文已影响0人  初夏时的猫

一:死锁问题
所谓死锁是指多个线程因竞争资源而造成的一种僵局(互相等待),若无外力作用,这些进程将无法向前推进。
ps:看着很难懂,下面有代码解释
1.死锁产生的原因
(1)系统资源竞争
通常系统中拥有的不可剥夺的资源,其数量不足以满足多个进程运行的需要,使得进程在 运行过程中,会因争夺资源而陷入僵局,如磁带机、打印机等。只有对不可剥夺资源的竞争 才可能产生死锁,对可剥夺资源的竞争是不会引起死锁的。
(2)线程推进顺序非法
进程在运行过程中,请求和释放资源的顺序不当,也同样会导致死锁。例如,并发进程 P1、P2分别保持了资源R1、R2,而进程P1申请资源R2,进程P2申请资源R1时,两者都 会因为所需资源被占用而阻塞。
(3)信号量使用不当也会造成死锁。
进程间彼此相互等待对方发来的消息,结果也会使得这 些进程间无法继续向前推进。例如,进程A等待进程B发的消息,进程B又在等待进程A 发的消息,可以看出进程A和B不是因为竞争同一资源,而是在等待对方的资源导致死锁。
2.死锁产生的必要条件
产生死锁必须同时满足以下四个条件,只要其中任一条件不成立,死锁就不会发生。

(1)互斥条件:进程要求对所分配的资源(如打印机)进行排他性控制,即在一段时间内某 资源仅为一个进程所占有。此时若有其他进程请求该资源,则请求进程只能等待。
(2)不剥夺条件:进程所获得的资源在未使用完毕之前,不能被其他进程强行夺走,即只能 由获得该资源的进程自己来释放(只能是主动释放)。
(3)请求和保持条件:进程已经保持了至少一个资源,但又提出了新的资源请求,而该资源 已被其他进程占有,此时请求进程被阻塞,但对自己已获得的资源保持不放。
(4)循环等待条件:存在一种进程资源的循环等待链,链中每一个进程已获得的资源同时被 链中下一个进程所请求。即存在一个处于等待状态的进程集合{Pl, P2, ..., pn},其中Pi等 待的资源被P(i+1)占有(i=0, 1, ..., n-1),Pn等待的资源被P0占有。
死锁例子:

//A锁住了obj1,企图锁住obj2
//B锁住obj2,企图锁住obj3
//C锁住obj3,企图锁住obj1
class DeadLock{
    Object obj1;
    Object obj2;
    public DeadLock(Object obj1,Object obj2){
        this.obj1 = obj1;
        this.obj2 = obj2;
    }
    public void ex1(){
        synchronized (obj1){
            System.out.println(Thread.currentThread().getName()+"obj1");
            synchronized (obj2){
                System.out.println(Thread.currentThread().getName()+"obj2");
            }
        }
    }

}
public class ThreadTest {
    public static void main(String[] args) {
        Object obj1 = "obj1";
        Object obj2 = "obj2";
        Object obj3 = "obj3";
        new Thread(()->{new DeadLock(obj1,obj2).ex1();},"A").start();
        new Thread(()->{new DeadLock(obj2,obj3).ex1();},"B").start();
        new Thread(()->{new DeadLock(obj3,obj1).ex1();},"C").start();
    }
}

三:如何避免死锁
在有些情况下死锁可以避免
(1).加锁顺序
(2).加锁时限
(3).死锁检测
1.加锁顺序
当多个线程需要相同的一些锁,按照不同的顺序加锁,死锁就很容易发生。如果能确保所有的线程按照相同的顺序获得锁,那么死锁就不会发生。
使用加锁顺序前

Thread1:
         lock A
         lock B
Thread2:
        lock B 
        lock A

使用加锁顺序方法后

Thread 1:
  lock A 
  lock B

Thread 2:
   wait for A
   lock C (when A locked)

Thread 3:
   wait for A
   wait for B
   wait for C

如果一个线程(比如线程3)需要一些锁,那么它必须按照确定的顺序获取锁。它只有获得了从顺序上排在前面的锁之后,才能获取后面的锁。

例如,线程2和线程3只有在获取了锁A之后才能尝试获取锁C(获取锁A是获取锁C的必要条件)。因为线程1已经拥有了锁A,所以线程2和3需要一直等到锁A被释放。然后在它们尝试对B或C加锁之前,必须成功地对A加了锁。

按照顺序加锁是一种有效的死锁预防机制。但是,这种方式需要你事先知道所有可能会用到的锁(并对这些锁做适当的排序),但总有些时候是无法预知的。
对于例子中的死锁问题,我们可以用一个生活中的例子来解决,银行转账是一个很常见的场景。其中涉资到同时申请两个锁的方法,对于转钱和收钱的人,可以使他们对账号的加锁顺序一致,从而避免死锁。比如:先获取卡号大的的账户的锁。
2.加锁时限
通俗说就是设定个超时时间,比如Timeout(超时时间)=5秒,这样如果某个线程5秒内没有获得预期的锁,执行超时对应的业务逻辑(回退等)。除判断死锁外,在很多线程获取同一资源时,则这些线程超时的概率就会很高。
这种机制存在一个问题,在Java中不能对synchronized同步块设置超时时间。你需要创建一个自定义锁,或使用Java5中java.util.concurrent包下的工具。
3.死锁检测
死锁检测是一个更好的死锁预防机制,它主要是针对那些不可能实现按序加锁并且锁超时也不可行的场景。

每当一个线程获得了锁,会在线程和锁相关的数据结构中(map、graph等等)将其记下。除此之外,每当有线程请求锁,也需要记录在这个数据结构中。当一个线程请求锁失败时,这个线程可以遍历锁的关系图看看是否有死锁发生。例如,线程A请求锁2,但是锁2这个时候被线程B持有,这时线程A就可以检查一下线程B是否已经请求了线程A当前所持有的锁。如果线程B确实有这样的请求,那么就是发生了死锁(线程A拥有锁1,请求锁2;线程B拥有锁2,请求锁1)。

当然,死锁一般要比两个线程互相持有对方的锁这种情况要复杂的多。线程A等待线程B,线程B等待线程C,线程C等待线程D,线程D又在等待线程A。线程A为了检测死锁,它需要递进地检测所有被B请求的锁。从线程B所请求的锁开始,线程A找到了线程C,然后又找到了线程D,发现线程D请求的锁被线程A自己持有着。这是它就知道发生了死锁。

下面是一幅关于四个线程(A,B,C和D)之间锁占有和请求的关系图。像这样的数据结构就可以被用来检测死锁。


图片.png
上一篇下一篇

猜你喜欢

热点阅读