多线程之死锁

作者: 初夏时的猫 | 来源:发表于2019-11-09 17:40 被阅读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

    相关文章

      网友评论

        本文标题:多线程之死锁

        本文链接:https://www.haomeiwen.com/subject/vcsnbctx.html