精华内容
下载资源
问答
  • 多线程以及多进程改善了系统资源的利用率并提高了系统 的处理能力。然而,并发执行也带来了新的问题——死锁死锁是指两个或两个以上的进程(线程)在运行过程中因争夺资源而造成的一种僵局(Deadly-Embrace) ) ,...

    一、什么是死锁

    多线程以及多进程改善了系统资源的利用率并提高了系统 的处理能力。然而,并发执行也带来了新的问题——死锁。
    死锁是指两个或两个以上的进程(线程)在运行过程中因争夺资源而造成的一种僵局(Deadly-Embrace) ) ,若无外力作用,这些进程(线程)都将无法向前推进。

    下面我们通过一些实例来说明死锁现象。

    先看生活中的一个实例,2个人一起吃饭但是只有一双筷子,2人轮流吃(同时拥有2只筷子才能吃)。某一个时候,一个拿了左筷子,一人拿了右筷子,2个人都同时占用一个资源,等待另一个资源,这个时候甲在等待乙吃完并释放它占有的筷子,同理,乙也在等待甲吃完并释放它占有的筷子,这样就陷入了一个死循环,谁也无法继续吃饭。。。
    在计算机系统中也存在类似的情况。例如,某计算机系统中只有一台打印机和一台输入设备,进程P1正占用输入设备,同时又提出使用打印机的请求,但此时打印机正被进程P2 所占用,而P2在未释放打印机之前,又提出请求使用正被P1占用着的输入设备。这样两个进程相互无休止地等待下去,均无法继续执行,此时两个进程陷入死锁状态。

    关于死锁的一些结论:

    •     参与死锁的进程数至少为两个
    •     参与死锁的所有进程均等待资源
    •     参与死锁的进程至少有两个已经占有资源
    •     死锁进程是系统中当前进程集合的一个子集
    •     死锁会浪费大量系统资源,甚至导致系统崩溃。

    二、死锁与饥饿

    饥饿(Starvation)指一个进程一直得不到资源。

    死锁和饥饿都是由于进程竞争资源而引起的。饥饿一般不占有资源,死锁进程一定占有资源。

    三、资源的类型

    3.1 可重用资源和消耗性资源

    3.1.1 可重用资源(永久性资源)

    可被多个进程多次使用,如所有硬件。

    •     只能分配给一个进程使用,不允许多个进程共享。
    •     进程在对可重用资源的使用时,须按照请求资源、使用资源、释放资源这样的顺序。
    •     系统中每一类可重用资源中的单元数目是相对固定的,进程在运行期间,既不能创建,也不能删除。

    3.1.2 消耗性资源(临时性资源)

    又称临时性资源,是由进程在运行期间动态的创建和消耗的。

    •     消耗性资源在进程运行期间是可以不断变化的,有时可能为0。
    •     进程在运行过程中,可以不断地创造可消耗性资源的单元,将它们放入该资源类的缓冲区中,以增加该资源类的单元数目。
    •     进程在运行过程中,可以请求若干个可消耗性资源单元,用于进程自己消耗,不再将它们返回给该资源类中。

    可消耗资源通常是由生产者进程创建,由消费者进程消耗。最典型的可消耗资源是用于进程间通信的消息。

    3.2 可抢占资源和不可抢占资源

    3.2.1 可抢占资源

    可抢占资源指某进程在获得这类资源后,该资源可以再被其他进程或系统抢占。对于这类资源是不会引起死锁的。

    CPU 和主存均属于可抢占性资源。

    3.2.2 不可抢占资源

    一旦系统把某资源分配给该进程后,就不能将它强行收回,只能在进程用完后自行释放。

    磁带机、打印机等属于不可抢占性资源。

    四、死锁产生的原因

    •   竞争不可抢占资源引起死锁
    •   通常系统中拥有的不可抢占资源,其数量不足以满足多个进程运行的需要,使得进程在运行过程中,会因争夺资源而陷入僵局,如磁带机、打印机等。只有对不可抢占资源的竞争 才可能产生死锁,对可抢占资源的竞争是不会引起死锁的。
    •   竞争可消耗资源引起死锁
    •   进程推进顺序不当引起死锁
    •   进程在运行过程中,请求和释放资源的顺序不当,也同样会导致死锁。例如,并发进程 P1、P2分别保持了资源R1、R2,而进程P1申请资源R2,进程P2申请资源R1时,两者都会因为所需资源被占用而阻塞。
    •   信号量使用不当也会造成死锁。进程间彼此相互等待对方发来的消息,结果也会使得这 些进程间无法继续向前推进。例如,进程A等待进程B发的消息,进程B又在等待进程A 发的消息,可以看出进程A和B不是因为竞争同一资源,而是在等待对方的资源导致死锁。

    4.1 竞争不可抢占资源引起死锁

    如:共享文件时引起死锁
    系统中拥有两个进程P1和P2,它们都准备写两个文件F1和F2。而这两者都属于可重用和不可抢占性资源。如果进程P1在打开F1的同时,P2进程打开F2文件,当P1想打开F2时由于F2已结被占用而阻塞,当P2想打开1时由于F1已结被占用而阻塞,此时就会无线等待下去,形成死锁。

    4.2 竞争可消耗资源引起死锁

    如:进程通信时引起死锁
    系统中拥有三个进程P1、P2和P3,m1、m2、m3是3可消耗资源。进程P1一方面产生消息m1,将其发送给P2,另一方面要从P3接收消息m3。而进程P2一方面产生消息m2,将其发送给P3,另一方面要从P1接收消息m1。类似的,进程P3一方面产生消息m3,将其发送给P1,另一方面要从P2接收消息m2。
    如果三个进程都先发送自己产生的消息后接收别人发来的消息,则可以顺利的运行下去不会产生死锁,但要是三个进程都先接收别人的消息而不产生消息则会永远等待下去,产生死锁。

    4.3 进程推进顺序不当引起死锁

    上图中,如果按曲线1的顺序推进,两个进程可顺利完成;如果按曲线2的顺序推进,两个进程可顺利完成;如果按曲线3的顺序推进,两个进程可顺利完成;如果按曲线4的顺序推进,两个进程将进入不安全区D中,此时P1保持了资源R1,P2保持了资源R2,系统处于不安全状态,如果继续向前推进,则可能产生死锁。

    五、产生死锁的四个必要条件

    5.1 互斥条件:

    进程要求对所分配的资源(如打印机)进行排他性控制,即在一段时间内某资源仅为一个进程所占有。此时若有其他进程请求该资源,则请求进程只能等待。

    5.2 不可剥夺条件:

    进程所获得的资源在未使用完毕之前,不能被其他进程强行夺走,即只能由获得该资源的进程自己来释放(只能是主动释放)。

    5.3 请求与保持条件:

    进程已经保持了至少一个资源,但又提出了新的资源请求,而该资源已被其他进程占有,此时请求进程被阻塞,但对自己已获得的资源保持不放。

    5.4 循环等待条件:

    存在一种进程资源的循环等待链,链中每一个进程已获得的资源同时被 链中下一个进程所请求。即存在一个处于等待状态的进程集合{Pl, P2, …, pn},其中Pi等 待的资源被P(i+1)占有(i=0, 1, …, n-1),Pn等待的资源被P0占有,如图2-15所示。

    直观上看,循环等待条件似乎和死锁的定义一样,其实不然。按死锁定义构成等待环所 要求的条件更严,它要求Pi等待的资源必须由P(i+1)来满足,而循环等待条件则无此限制。 例如,系统中有两台输出设备,P0占有一台,PK占有另一台,且K不属于集合{0, 1, …, n}。

    Pn等待一台输出设备,它可以从P0获得,也可能从PK获得。因此,虽然Pn、P0和其他 一些进程形成了循环等待圈,但PK不在圈内,若PK释放了输出设备,则可打破循环等待, 如图2-16所示。因此循环等待只是死锁的必要条件。

    资源分配图含圈而系统又不一定有死锁的原因是同类资源数大于1。但若系统中每类资 源都只有一个资源,则资源分配图含圈就变成了系统出现死锁的充分必要条件。

    以上这四个条件是死锁的必要条件,只要系统发生死锁,这些条件必然成立,而只要上述条件之一不满足,就不会发生死锁。

    产生死锁的一个例子:

    /**
     * 一个简单的死锁类
     * 当DeadLock类的对象flag==1时(td1),先锁定o1,睡眠500毫秒
     * 而td1在睡眠的时候另一个flag==0的对象(td2)线程启动,先锁定o2,睡眠500毫秒
     * td1睡眠结束后需要锁定o2才能继续执行,而此时o2已被td2锁定;
     * td2睡眠结束后需要锁定o1才能继续执行,而此时o1已被td1锁定;
     * td1、td2相互等待,都需要得到对方锁定的资源才能继续执行,从而死锁。
     */
    public class DeadLock implements Runnable {
        public int flag = 1;  
        //静态对象是类的所有对象共享的  
        private static Object o1 = new Object(), o2 = new Object();  
        @Override  
        public void run() {  
            System.out.println("flag=" + flag);  
            if (flag == 1) {  
                synchronized (o1) {  
                    try {  
                        Thread.sleep(500);  
                    } catch (Exception e) {  
                        e.printStackTrace();  
                    }  
                    synchronized (o2) {  
                        System.out.println("1");  
                    }  
                }  
            }  
            if (flag == 0) {  
                synchronized (o2) {  
                    try {  
                        Thread.sleep(500);  
                    } catch (Exception e) {  
                        e.printStackTrace();  
                    }  
                    synchronized (o1) {  
                        System.out.println("0");  
                    }  
                }  
            }  
        }  
    
        public static void main(String[] args) {
            DeadLock td1 = new DeadLock();
            DeadLock td2 = new DeadLock();
            td1.flag = 1;
            td2.flag = 0;
            //td1,td2都处于可执行状态,但JVM线程调度先执行哪个线程是不确定的。  
            //td2的run()可能在td1的run()之前运行  
            new Thread(td1).start();  
            new Thread(td2).start();
        }  
    }  

    六、处理死锁的方法

    •   预防死锁:通过设置某些限制条件,去破坏产生死锁的四个必要条件中的一个或几个条件,来防止死锁的发生。
    •   避免死锁:在资源的动态分配过程中,用某种方法去防止系统进入不安全状态,从而避免死锁的发生。
    •   检测死锁:允许系统在运行过程中发生死锁,但可设置检测机构及时检测死锁的发生,并采取适当措施加以清除。
    •   解除死锁:当检测出死锁后,便采取适当措施将进程从死锁状态中解脱出来。

    6.1 预防死锁

    1.破坏“互斥”条件:

    破坏“互斥”条件,就是在系统里取消互斥。若资源不被一个进程独占使用,那么死锁是肯定不会发生的。但一般来说在所列的四个条件中,“互斥”条件是无法破坏的。因此,在死锁预防里主要是破坏其他几个必要条件,而不去涉及破坏“互斥”条件。
    注意:互斥条件不能被破坏,否则会造成结果的不可再现性。

    2.破坏“占有并等待”条件:
    破坏“占有并等待”条件,就是在系统中不允许进程在已获得某种资源的情况下,申请其他资源。即要想出一个办法,阻止进程在持有资源的同时申请其他资源。
    方法一:创建进程时,要求它申请所需的全部资源,系统或满足其所有要求,或什么也不给它。这是所谓的 “ 一次性分配”方案。
    方法二:要求每个进程提出新的资源申请前,释放它所占有的资源。这样,一个进程在需要资源S时,须先把它先前占有的资源R释放掉,然后才能提出对S的申请,即使它可能很快又要用到资源R。

    3.破坏“不可抢占”条件:
    破坏“不可抢占”条件就是允许对资源实行抢夺。
    方法一:如果占有某些资源的一个进程进行进一步资源请求被拒绝,则该进程必须释放它最初占有的资源,如果有必要,可再次请求这些资源和另外的资源。
    方法二:如果一个进程请求当前被另一个进程占有的一个资源,则操作系统可以抢占另一个进程,要求它释放资源。只有在任意两个进程的优先级都不相同的条件下,方法二才能预防死锁。

    4.破坏“循环等待”条件:
    破坏“循环等待”条件的一种方法,是将系统中的所有资源统一编号,进程可在任何时刻提出资源申请,但所有申请必须按照资源的编号顺序(升序)提出。这样做就能保证系统不出现死锁。

    6.2 避免死锁

    理解了死锁的原因,尤其是产生死锁的四个必要条件,就可以最大可能地避免、预防和解除死锁。所以,在系统设计、进程调度等方面注意如何让这四个必要条件不成立,如何确定资源的合理分配算法,避免进程永久占据系统资源。此外,也要防止进程在处于等待状态的情况下占用资源。因此,对资源的分配要给予合理的规划。

    预防死锁和避免死锁的区别:
    预防死锁是设法至少破坏产生死锁的四个必要条件之一,严格的防止死锁的出现,而避免死锁则不那么严格的限制产生死锁的必要条件的存在,因为即使死锁的必要条件存在,也不一定发生死锁。避免死锁是在系统运行过程中注意避免死锁的最终发生。

    6.2.1 常用避免死锁的方法

    6.2.1.1 有序资源分配法

    这种算法资源按某种规则系统中的所有资源统一编号(例如打印机为1、磁带机为2、磁盘为3、等等),申请时必须以上升的次序。系统要求申请进程:
    1、对它所必须使用的而且属于同一类的所有资源,必须一次申请完;
    2、在申请不同类资源时,必须按各类设备的编号依次申请。例如:进程PA,使用资源的顺序是R1,R2; 进程PB,使用资源的顺序是R2,R1;若采用动态分配有可能形成环路条件,造成死锁。
    采用有序资源分配法:R1的编号为1,R2的编号为2;
    PA:申请次序应是:R1,R2
    PB:申请次序应是:R1,R2
    这样就破坏了环路条件,避免了死锁的发生。

    6.2.1.2 银行家算法

    详见银行家算法.

    6.2.2 常用避免死锁的技术

    •  加锁顺序(线程按照一定的顺序加锁)
    •  加锁时限(线程尝试获取锁的时候加上一定的时限,超过时限则放弃对该锁的请求,并释放自己占有的锁)
    •  死锁检测

    6.2.2.1 加锁顺序

    当多个线程需要相同的一些锁,但是按照不同的顺序加锁,死锁就很容易发生。

    如果能确保所有的线程都是按照相同的顺序获得锁,那么死锁就不会发生。看下面这个例子:
     

    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加了锁。

    按照顺序加锁是一种有效的死锁预防机制。但是,这种方式需要你事先知道所有可能会用到的锁(译者注:并对这些锁做适当的排序),但总有些时候是无法预知的。

    6.2.2.2 加锁时限

    另外一个可以避免死锁的方法是在尝试获取锁的时候加一个超时时间,这也就意味着在尝试获取锁的过程中若超过了这个时限该线程则放弃对该锁请求。若一个线程没有在给定的时限内成功获得所有需要的锁,则会进行回退并释放所有已经获得的锁,然后等待一段随机的时间再重试。这段随机的等待时间让其它线程有机会尝试获取相同的这些锁,并且让该应用在没有获得锁的时候可以继续运行(译者注:加锁超时后可以先继续运行干点其它事情,再回头来重复之前加锁的逻辑)。

    以下是一个例子,展示了两个线程以不同的顺序尝试获取相同的两个锁,在发生超时后回退并重试的场景:
     

    Thread 1 locks A
    Thread 2 locks B
    Thread 1 attempts to lock B but is blocked
    Thread 2 attempts to lock A but is blocked
    Thread 1’s lock attempt on B times out
    Thread 1 backs up and releases A as well
    Thread 1 waits randomly (e.g. 257 millis) before retrying.
    Thread 2’s lock attempt on A times out
    Thread 2 backs up and releases B as well
    Thread 2 waits randomly (e.g. 43 millis) before retrying.

    在上面的例子中,线程2比线程1早200毫秒进行重试加锁,因此它可以先成功地获取到两个锁。这时,线程1尝试获取锁A并且处于等待状态。当线程2结束时,线程1也可以顺利的获得这两个锁(除非线程2或者其它线程在线程1成功获得两个锁之前又获得其中的一些锁)。

    需要注意的是,由于存在锁的超时,所以我们不能认为这种场景就一定是出现了死锁。也可能是因为获得了锁的线程(导致其它线程超时)需要很长的时间去完成它的任务。

    此外,如果有非常多的线程同一时间去竞争同一批资源,就算有超时和回退机制,还是可能会导致这些线程重复地尝试但却始终得不到锁。如果只有两个线程,并且重试的超时时间设定为0到500毫秒之间,这种现象可能不会发生,但是如果是10个或20个线程情况就不同了。因为这些线程等待相等的重试时间的概率就高的多(或者非常接近以至于会出现问题)。
    (译者注:超时和重试机制是为了避免在同一时间出现的竞争,但是当线程很多时,其中两个或多个线程的超时时间一样或者接近的可能性就会很大,因此就算出现竞争而导致超时后,由于超时时间一样,它们又会同时开始重试,导致新一轮的竞争,带来了新的问题。)

    这种机制存在一个问题,在Java中不能对synchronized同步块设置超时时间。你需要创建一个自定义锁,或使用Java5中java.util.concurrent包下的工具。写一个自定义锁类不复杂,但超出了本文的内容。后续的Java并发系列会涵盖自定义锁的内容。

    6.2.2.3 死锁检测

    死锁检测是一个更好的死锁预防机制,它主要是针对那些不可能实现按序加锁并且锁超时也不可行的场景。

    每当一个线程获得了锁,会在线程和锁相关的数据结构中(map、graph等等)将其记下。除此之外,每当有线程请求锁,也需要记录在这个数据结构中。

    当一个线程请求锁失败时,这个线程可以遍历锁的关系图看看是否有死锁发生。例如,线程A请求锁7,但是锁7这个时候被线程B持有,这时线程A就可以检查一下线程B是否已经请求了线程A当前所持有的锁。如果线程B确实有这样的请求,那么就是发生了死锁(线程A拥有锁1,请求锁7;线程B拥有锁7,请求锁1)。

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

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

    6.3 检测死锁

    一般来说,由于操作系统有并发,共享以及随机性等特点,通过预防和避免的手段达到排除死锁的目的是很困难的。这需要较大的系统开销,而且不能充分利用资源。为此,一种简便的方法是系统为进程分配资源时,不采取任何限制性措施,但是提供了检测和解脱死锁的手段:能发现死锁并从死锁状态中恢复出来。因此,在实际的操作系统中往往采用死锁的检测与恢复方法来排除死锁。
    死锁检测与恢复是指系统设有专门的机构,当死锁发生时,该机构能够检测到死锁发生的位置和原因,并能通过外力破坏死锁发生的必要条件,从而使得并发进程从死锁状态中恢复出来。
    这时进程P1占有资源R1而申请资源R2,进程P2占有资源R2而申请资源R1,按循环等待条件,进程和资源形成了环路,所以系统是死锁状态。进程P1,P2是参与死锁的进程。
    下面我们再来看一看死锁检测算法。算法使用的数据结构是如下这些:
    占有矩阵A:n*m阶,其中n表示并发进程的个数,m表示系统的各类资源的个数,这个矩阵记录了每一个进程当前占有各个资源类中资源的个数。
    申请矩阵R:n*m阶,其中n表示并发进程的个数,m表示系统的各类资源的个数,这个矩阵记录了每一个进程当前要完成工作需要申请的各个资源类中资源的个数。
    空闲向量T:记录当前m个资源类中空闲资源的个数。
    完成向量F:布尔型向量值为真(true)或假(false),记录当前n个并发进程能否进行完。为真即能进行完,为假则不能进行完。
    临时向量W:开始时W:=T。
    算法步骤:
    (1)W:=T,
    对于所有的i=1,2,…,n,
    如果A[i]=0,则F[i]:=true;否则,F[i]:=false
    (2)找满足下面条件的下标i:
    F[i]:=false并且R[i]〈=W
    如果不存在满足上面的条件i,则转到步骤(4)。
    (3)W:=W+A[i]
    F[i]:=true
    转到步骤(2)
    (4)如果存在i,F[i]:=false,则系统处于死锁状态,且Pi进程参与了死锁。什么时候进行死锁的检测取决于死锁发生的频率。如果死锁发生的频率高,那么死锁检测的频率也要相应提高,这样一方面可以提高系统资源的利用率,一方面可以避免更多的进程卷入死锁。如果进程申请资源不能满足就立刻进行检测,那么每当死锁形成时即能被发现,这和死锁避免的算法相近,只是系统的开销较大。为了减小死锁检测带来的系统开销,一般采取每隔一段时间进行一次死锁检测,或者在CPU的利用率降低到某一数值时,进行死锁的检测。

    6.4 解除死锁

    一旦检测出死锁,就应立即釆取相应的措施,以解除死锁。
    死锁解除的主要方法有:
    1) 资源剥夺法。挂起某些死锁进程,并抢占它的资源,将这些资源分配给其他的死锁进程。但应防止被挂起的进程长时间得不到资源,而处于资源匮乏的状态。
    2) 撤销进程法。强制撤销部分、甚至全部死锁进程并剥夺这些进程的资源。撤销的原则可以按进程优先级和撤销进程代价的高低进行。
    3) 进程回退法。让一(多)个进程回退到足以回避死锁的地步,进程回退时自愿释放资源而不是被剥夺。要求系统保持进程的历史信息,设置还原点。
     

    展开全文
  • 大家都知道多线程并发的时候,我们一般会用到锁。但是锁用不好,就会导致死锁。那么,死锁是如何产生的呢? 一般造成死锁必须同时满足如下4个条件: 互斥条件:线程使用的资源必须至少有一个是不能共享的。 请求...

    大家都知道多线程并发的时候,我们一般会用到锁。但是锁用不好,就会导致死锁。那么,死锁是如何产生的呢?

    一般造成死锁必须同时满足如下4个条件:

    1. 互斥条件:线程使用的资源必须至少有一个是不能共享的。
    2. 请求与保持条件:至少有一个线程必须持有一个资源并且正在等待获取一个当前被其他线程持有的资源。
    3. 非剥夺条件:分配的资源不能从相应的线程中被强制剥夺。
    4. 循环等待条件:第一个线程等待其他线程,后者又在等待第一个线程。

    接下来我们来看一段手写的死锁代码

    package com.sync;
    
    /**
     * 手写一个死锁
     *
     * @author 小辉GE/小辉哥
     * <p>
     * 2019年8月10日 下午18:30:00
     */
    public class DeadLock {
        public static void main(String[] args) {
            Object obj1 = new Object();
            Object obj2 = new Object();
            Runner r1 = new Runner(obj1, obj2, 5000);
            Runner r2 = new Runner(obj2, obj1, 5000);
            new Thread(r1, "r1").start();
            new Thread(r2, "r2").start();
        }
    }
    
    class Runner implements Runnable {
        private Object o1;
        private Object o2;
        private int sleepTime;
    
        public Runner(Object o1, Object o2, int sleepTime) {
            this.o1 = o1;
            this.o2 = o2;
            this.sleepTime = sleepTime;
        }
    
        public void run() {
            System.out.println("当前线程" + Thread.currentThread().getName() + "获得锁");
            synchronized (o1) {
                try {
                    //  这里主要是放大效果
                    Thread.sleep(sleepTime);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (o2) {
                    System.out.println();
                }
            }
        }
    }

    测试输出结果如下:

    结果分析:

    其实我们不难看出,线r1启动时候先获得obj1对象锁且需要获得obj2对象锁,线程r2启动时先获得obj2对象锁且需要获取obj1对象锁,这就同时满足了死锁的条件,也就出现了死锁。

    以上代码仅供参考,如有不当之处,欢迎指出!!!

    更多干货,欢迎大家关注和联系我。期待和大家一起更好的交流、探讨技术!!!

     

     

    展开全文
  • 一、导致线程死锁的原因 个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放,而该资源又被其他线程锁定,从而导致每一个线程都得等其它线程释放其锁定的资源,造成了所有线程都无法正常结束。这是从...

    欢迎大家关注我的公众号【老周聊架构】,Java后端主流技术栈的原理、源码分析、架构以及各种互联网高并发、高性能、高可用的解决方案。

    一、导致线程死锁的原因

    多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放,而该资源又被其他线程锁定,从而导致每一个线程都得等其它线程释放其锁定的资源,造成了所有线程都无法正常结束。这是从网上其他文档看到的死锁产生的四个必要条件:

    1、互斥使用,即当资源被一个线程使用(占有)时,别的线程不能使用。
    2、不可抢占,资源请求者不能强制从资源占有者手中夺取资源,资源只能由资源占有者主动释放。
    3、请求和保持,即当资源请求者在请求其他的资源的同时保持对原有资源的占有。
    4、循环等待,即存在一个等待队列:P1占有P2的资源,P2占有P3的资源,P3占有P1的资源。这样就形成了一个等待环路。

    当上述四个条件都成立的时候,便形成死锁。当然,死锁的情况下如果打破上述任何一个条件,便可让死锁消失。下面用java代码来模拟一下死锁的产生。

    模拟两个资源:

    class ThreadResource {
        public static Object resource1 = new Object();
        public static Object resource2 = new Object();
    }
    

    模拟线程1占用资源1并申请获得资源2的锁:

    class Thread1 implements Runnable {
    
        @Override
        public void run() {
            try {
                System.out.println("Thread1 is running");
                synchronized (ThreadResource.resource1) {
                    System.out.println("Thread1 lock resource1");
                    Thread.sleep(2000);//休眠2s等待线程2锁定资源2
                    synchronized (ThreadResource.resource2) {
                        System.out.println("Thread1 lock resource2");
                    }
                    System.out.println("Thread1 release resource2");
                }
                System.out.println("Thread1 release resource1");
            } catch (Exception e) {
                System.out.println(e.getMessage());
            }
            System.out.println("Thread1 is stop");
        }
    }
    

    模拟线程2占用资源2并申请获得资源1的锁:

    class Thread2 implements Runnable {
    
        @Override
        public void run() {
            try {
                System.out.println("Thread2 is running");
                synchronized (ThreadResource.resource2) {
                    System.out.println("Thread2 lock resource2");
                    Thread.sleep(2000);//休眠2s等待线程1锁定资源1
                    synchronized (ThreadResource.resource1) {
                        System.out.println("Thread2 lock resource1");
                    }
                    System.out.println("Thread2 release resource1");
                }
                System.out.println("Thread2 release resource2");
            } catch (Exception e) {
                System.out.println(e.getMessage());
            }
            System.out.println("Thread2 is stop");
        }
    }
    

    同时运行俩个线程:

    public class ThreadTest {
    
        public static void main(String[] args) {
            new Thread(new Thread1()).start();
            new Thread(new Thread2()).start();
        }
    
    }
    

    完整代码:

    package com.concurrent.deadLock;
    
    /**
     * @author riemann
     * @date 2019/08/14 22:44
     */
    public class ThreadTest {
    
        public static void main(String[] args) {
            new Thread(new Thread1()).start();
            new Thread(new Thread2()).start();
        }
    
    }
    
    class ThreadResource {
        public static Object resource1 = new Object();
        public static Object resource2 = new Object();
    }
    
    class Thread1 implements Runnable {
    
        @Override
        public void run() {
            try {
                System.out.println("Thread1 is running");
                synchronized (ThreadResource.resource1) {
                    System.out.println("Thread1 lock resource1");
                    Thread.sleep(2000);//休眠2s等待线程2锁定资源2
                    synchronized (ThreadResource.resource2) {
                        System.out.println("Thread1 lock resource2");
                    }
                    System.out.println("Thread1 release resource2");
                }
                System.out.println("Thread1 release resource1");
            } catch (Exception e) {
                System.out.println(e.getMessage());
            }
            System.out.println("Thread1 is stop");
        }
    }
    
    class Thread2 implements Runnable {
    
        @Override
        public void run() {
            try {
                System.out.println("Thread2 is running");
                synchronized (ThreadResource.resource2) {
                    System.out.println("Thread2 lock resource2");
                    Thread.sleep(2000);//休眠2s等待线程1锁定资源1
                    synchronized (ThreadResource.resource1) {
                        System.out.println("Thread2 lock resource1");
                    }
                    System.out.println("Thread2 release resource1");
                }
                System.out.println("Thread2 release resource2");
            } catch (Exception e) {
                System.out.println(e.getMessage());
            }
            System.out.println("Thread2 is stop");
        }
    }
    

    输出结果:

    Thread1 is running
    Thread1 lock resource1
    Thread2 is running
    Thread2 lock resource2
    

    并且程序一直无法结束。这就是由于线程1占用了资源1,此时线程2已经占用资源2,。这个时候线程1想要使用资源2,线程2想要使用资源1。两个线程都无法让步,导致程序死锁。

    二、怎么解除线程死锁

    由上面的例子可以看出当线程在同步某个对象里,再去锁定另外一个对象的话,就和容易发生死锁的情况。最好是线程每次只锁定一个对象并且在锁定该对象的过程中不再去锁定其他的对象,这样就不会导致死锁了。比如将以上的线程改成下面这种写法就可以避免死锁:

    public void run()
    {
        try
        {
            System.out.println("Thread1 is running");
            synchronized (ThreadResource.resource1)
            {
                System.out.println("Thread1 lock resource1");
                Thread.sleep(2000);//休眠2s等待线程2锁定资源2
            }
            System.out.println("Thread1 release resource1");
            synchronized (ThreadResource.resource2)
            {
                System.out.println("Thread1 lock resource2");
            }
            System.out.println("Thread1 release resource2");
        }
        catch (Exception e)
        {
            System.out.println(e.getMessage());
        }
        System.out.println("Thread1 is stop");
    }
    

    但是有的时候业务需要同时去锁定两个对象,比如转账业务:A给B转账,需要同时锁定A、B两个账户。如果A、B相互同时转账的话就会出现死锁的情况。这时可以定义一个规则:锁定账户先后的规则。根据账户的某一个属性(比如id或者hasCode),判断锁定的先后。即每一次转账业务都是先锁定A再锁定B(或者先锁定B在锁定A),这样也不会导致死锁的发生。比如按照上面的例子,需要同时锁定两个资源,可以根据资源的hashcode值大小来判断先后锁定顺序。可以这样改造线程:

    class Thread3 implements Runnable {
    
        @Override
        public void run() {
            try {
                System.out.println("Thread is running");
                if (ThreadResource.resource1.hashCode() > ThreadResource.resource2.hashCode()) {
                    //先锁定resource1
                    synchronized (ThreadResource.resource1) {
                        System.out.println("Thread lock resource1");
                        Thread.sleep(2000);
                        synchronized (ThreadResource.resource2)
                        {
                            System.out.println("Thread lock resource2");
                        }
                        System.out.println("Thread release resource2");
                    }
                    System.out.println("Thread release resource1");
                } else {
                    //先锁定resource2
                    synchronized (ThreadResource.resource2)
                    {
                        System.out.println("Thread lock resource2");
                        Thread.sleep(2000);
                        synchronized (ThreadResource.resource1)
                        {
                            System.out.println("Thread lock resource1");
                        }
                        System.out.println("Thread release resource1");
                    }
                    System.out.println("Thread release resource2");
                }
            } catch (Exception e) {
                System.out.println(e.getMessage());
            }
            System.out.println("Thread1 is stop");
        }
    }
    

    输出结果:

    Thread is running
    Thread lock resource2
    Thread lock resource1
    Thread release resource1
    Thread release resource2
    Thread1 is stop
    

    三、如何避免死锁

    在有些情况下死锁是可以避免的。三种用于避免死锁的技术:

    1、加锁顺序
    2、加锁时限
    3、死锁检测

    加锁顺序

    当多个线程需要相同的一些锁,但是按照不同的顺序加锁,死锁就容易发生。

    按照顺序加锁是一种有效的死锁预防机制。但是,这种方式需要事先知道所有可能会用到的锁,但总有些时候是无法预知的。

    加锁时限

    当一个线程在尝试获取锁的过程中超过了这个时限则该线程应该放弃对该锁进行请求。

    若一个线程没有在给定的时限内成功获得所有需要的锁,则会进行回退并释放所有已经获得的锁,然后等待一段随机的时间再重试。这段随机的等待时间让其它线程有机会尝试获取相同的这些锁,并且让该应用在没有获得锁的时候可以继续运行。

    需要注意的是,由于存在锁的超时,所以我们不能认为这种场景就一定是出现了死锁。也可能是因为获得了锁的线程(导致其它线程超时)需要很长的时间去完成它的任务。

    此外,如果有非常多的线程同一时间去竞争同一批资源,就算有超时和回退机制,还是可能会导致这些线程重复地尝试但却始终得不到锁。如果只有两个线程,并且重试的超时时间设定为0到500毫秒之间,这种现象可能不会发生,但是如果是10个或20个线程情况就不同了。因为这些线程等待相等的重试时间的概率就高的多(或者非常接近以至于会出现问题)。

    死锁检测

    死锁检测是一个更好的死锁预防机制,它主要是针对那些不可能实现按序加锁并且锁超时也不可行的场景。

    每当一个线程获得了锁,会在线程和锁相关的数据结构中(map、graph等等)将其记下。除此之外,每当有线程请求锁,也需要记录在这个数据结构中。

    当一个线程请求锁失败时,这个线程可以遍历锁的关系图看看是否有死锁发生。

    那么当检测出死锁时,这些线程该做些什么呢?

    一个可行的做法是释放所有锁,回退,并且等待一段随机的时间后重试。这个和简单的加锁超时类似,不一样的是只有死锁已经发生了才回退,而不会是因为加锁的请求超时了。虽然有回退和等待,但是如果有大量的线程竞争同一批锁,它们还是会重复地死锁(编者注:原因同超时类似,不能从根本上减轻竞争)。

    一个更好的方案是给这些线程设置优先级,让一个(或几个)线程回退,剩下的线程就像没发生死锁一样继续保持着它们需要的锁。如果赋予这些线程的优先级是固定不变的,同一批线程总是会拥有更高的优先级。为避免这个问题,可以在死锁发生的时候设置随机的优先级。

    总结:死锁常见于,线程在锁定对象还没释放时,又需要锁定另一个对象,并且此时该对象可能被另一个线程锁定。这种时候很容易导致死锁。因此在开发时需要慎重使用锁,尤其是需要注意尽量不要在锁里又加锁。

    展开全文
  • 多线程死锁以及解决方法

    千次阅读 2017-06-09 23:11:44
    解决死锁没有简单的方法,这是因为线程产生死锁都各有各的原因,而且往往具有很的负载。大多数软件测试产生不了足够的负载,所以不可能暴露所有的线程错误。在这里中,下面将讨论开发过程常见的4类典型的死锁和...
    解决死锁没有简单的方法,这是因为线程产生死锁都各有各的原因,而且往往具有很高的负载。大多数软件测试产生不了足够多的负载,所以不可能暴露所有的线程错误。在这里中,下面将讨论开发过程常见的4类典型的死锁和解决对策。

    (1)数据库死锁 

      在数据库中,如果一个连接占用了另一个连接所需的数据库锁,则它可以阻塞另一个连接。如果两个或两个以上的连接相互阻

    塞,则它们都不能继续执行,这种情况称为数据库死锁。数据库死锁问题不易处理,通常数据行进行更新时,需要锁定该数据行,

    执行更新,然后在提交或回滚封闭事务时释放锁。由于数据库平台、配置的隔离级以及查询提示的不同,获取的锁可能是细粒度或

    粗粒度的,它会阻塞(或不阻塞)其他对同一数据行、表或数据库的查询。基于数据库模式,读写操作会要求遍历或更新多个索

    引、验证约束、执行触发器等。每个要求都会引入更多锁。此外,其他应用程序还可能正在访问同一数据库模式中的某些对象,

    获取不同应用程序所具有的锁。所有这些因素综合在一起,数据库死锁几乎不可能被消除了。值得庆幸的是,数据库死锁通常是可

    恢复的:当数据库发现死锁时,它会强制销毁一个连接(通常是使用最少的连接),并回滚其事务。这将释放所有与已经结束的事

    务相关联的锁,至少允许其他连接中有一个可以获取它们正在被阻塞的锁。

    由于数据库具有这种典型的死锁处理行为,所以当出现数据库死锁问题时,数据库常常只能重试整个事务。当数据库连接被销

    毁时,会抛出可被应用程序捕获的异常,并标识为数据库死锁。如果允许死锁异常传播到初始化该事务的代码层之外,则该代码层

    可以启动一个新事务并重做先前所有工作。当出现问题就重试,由于数据库可以自由地获取锁,所以几乎不可能保证两个或两个以

    上的线程不发生数据库死锁。此方法至少能保证在出现某些数据库死锁情况时,应用程序能正常运行。


    (2)资源池耗尽死锁

      客户端的增加导致资源池耗尽死锁是由于负载而造成的,即资源池太小,而每个线程需要的资源超过了池中的可用资源。假设

    连接池最多有10个连接,同时有10个对外部并发调用。这些线程中每一个都需要一个数据库连接用来清空池。现在,每个线程都执

    行嵌套的调用。则所有线程都不能继续,但又都不放弃自己的第一个数据库连接。这样,10个线程都将被死锁。

      研究此类死锁,会发现线程存储中有大量等待获取资源的线程,以及同等数量的空闲且未阻塞的活动数据库连接。当应用程序

    死锁时,如果可以在运行时检测连接池,就能确认连接池实际上已空。

      修复此类死锁的方法包括:增加连接池的大小或者重构代码,以便单个线程不需要同时使用很多数据库连接。或者可以设置内

    部调用使用不同的连接池,即使外部调用的连接池为空,内部调用也能使用自己的连接池继续。


    (3)单线程、多冲突数据库连接死锁

      对同一线程执行嵌套的调用有时出现死锁,此情形即使在非高负载系统中通常也会发生。当第一个(外部)连接已获取第二个

    (内部)连接所需要的数据库锁,则第二个连接将永久阻塞第一个连接,并等待第一个连接被提交或回滚,这就出现了死锁情形。

    因为数据库没有注意到两个连接之间的关系,所以数据库不会将此情形检测为死锁。这样即使不存在并发,此代码也将导致死锁。

    此情形有多种具体的变种,可以涉及多个线程和两个以上的数据库连接。


    (4)Java虚拟机锁与数据库锁冲突

      这种情形发生在数据库锁与Java虚拟机锁并存的时候。在这种情况下,一个线程占有一个数据库锁并尝试获取Java虚拟机锁。

    同时,另一个线程占有Java虚拟机锁并尝试获取数据库锁。此时,数据库发现一个连接阻塞了另一个连接,但由于无法阻止连接继

    续,所以不会检测到死锁。Java虚拟机发现同步的锁中有一个线程,并有另一个尝试进入的线程,所以即使Java虚拟机能检测到死

    锁并对它们进行处理,它还是不会检测到这种情况。

      总而言之,JAVA应用程序中的死锁是一个大问题——它能导致整个应用程序慢慢终止,还很难被分离和修复,尤其是当开发人员

    不熟悉如何分析死锁环境的时候。


    (5). 死锁的经验法则  

        (1) 对大多数的Java程序员来说最简单的防止死锁的方法是对竞争的资源引入序号,如果一个线程需要几个资源,那么它必须先得到

    小序号的资源,再申请大序号的资源。可以在Java代码中增加同步关键字的使用,这样可以减少死锁,但这样做也会影响性能。如果

    负载过重,数据库内部也有可能发生死锁。

        (2)了解数据库锁的发生行为。假定任何数据库访问都有可能陷入数据库死锁状况,但是都能正确进行重试。例如了解如何从应用

    服务器获取完整的线程转储以及从数据库获取数据库连接列表(包括互相阻塞的连接),知道每个数据库连接与哪个Java线程相关

    联。了解Java线程和数据库连接之间映射的最简单方法是向连接池访问模式添加日志记录功能。

        (3)当进行嵌套的调用时,了解哪些调用使用了与其它调用同样的数据库连接。即使嵌套调用运行在同一个全局事务中,它仍将使

    用不同的数据库连接,而不会导致嵌套死锁。

        (4)确保在峰值并发时有足够大的资源池。

        (5)避免执行数据库调用或在占有Java虚拟机锁时,执行其他与Java虚拟机无关的操作。 
      

        最重要的是,多线程设计虽然是困难的,但在开始编程之前详细设计系统能够帮助你避免难以发现死锁的问题。死锁在语言层面

    上不能解决,就需要一个良好设计来避免死锁。

    展开全文
  • 最近项目中用到Job+多线程并发执行更新同一行数据,通过主键id更新的场景,但是任务一执行就出现dead lock异常,经过分析和排查,发现多线程并发读取同一行数据获取共享锁(Shard Lo)
  • 多线程高并发的常见面试题整理

    千次阅读 2020-07-12 11:46:03
    1.线程实现方式 1继承Thread类 定义Thread类的子类,并重写Thread类的run()方法,创建子类对象(即线程对象),调用线程对象的start()方法来启动该线程 2.实现Runnable接口 并重写该接口的run()方法,该run()方法...
  • log4j 多线程死锁问题 此案例研究描述了影响Weblogic Portal 10.0生产环境的Apache Log4j线程争用问题的完整根本原因分析和解决方案。 它还将展示在开发和支持Java EE应用程序时适当的Java类加载器知识的重要性。 ...
  • 多线程死锁的产生以及如何避免死锁

    万次阅读 多人点赞 2016-07-13 11:07:45
    多线程以及多进程改善了系统资源的利用率并提高了系统 的处理能力。然而,并发执行也带来了新的问题——死锁。所谓死锁是指多个线程因竞争资源而造成的一种僵局(互相等待),若无外力作用,这些进程都将无法向前...
  • 我们对多线程已经介绍了很多的内容,针对多线程高并发的分布式系统,编写的代码一定会有问题,而且问题比较多。那么有哪些问题是比较常见的问题呢? 死锁就是其中一个比较常见的问题,这篇博文我们介绍什么是死锁...
  • 1.1 在做高并发秒杀中创建订单、减库存步骤时出现异常:MySQLTransactionRollbackException: Deadlock found when trying to get lock,也就是出现了死锁。 1.2 我们在学多线程时,知道死锁发生在两个线程互相去...
  • 多线程锁定同一资源会造成死锁 线程池中的任务使用当前线程池也可能出现死锁 参考连接: https://blog.csdn.net/qq_35064774/article/details/51793656 情况一: 死锁是两个或多个线程互相等待对方所有用的资源...
  • 1、在java中守护线程和本地线程区别? java中的线程分为两种:守护线程(Daemon)和用户线程(User)。 任何线程都可以设置为守护线程和用户线程,通过方法Thread.setDaemon(bool on);true则把该线程设置为守护...
  • 多线程死锁详解

    千次阅读 多人点赞 2019-10-13 12:45:51
    当两个或两个以上的线程在执行过程中,因为争夺资源而造成的一种相互等待的状态,由于存在一种环路的锁依赖关系而永远地等待下去,如果没有外部干涉,他们将永远等待下去,此时的这个状态称之为死锁。 经典的 “哲学...
  • 多线程高并发笔记

    千次阅读 多人点赞 2020-09-13 10:14:52
    1. 创建线程的三种方式 实现Runnable 重写run方法 继承Thread 重写run方法 线程池创建 Executors.newCachedThreadPool() 2. Thread线程操作方法 Thread.sleep([mills]) 当前线程睡眠指定mills毫秒 Thread....
  • 多线程以及多进程改善了系统资源的利用率并提高了系统的处理能力。然而,并发执行也带来了新的问题–死锁。所谓死锁是指多个线程因竞争资源而造成的一种僵局(互相等待),若无外力作用,这些进程将无法向前推进。 ...
  • 多线程高并发介绍

    万次阅读 2021-06-06 05:00:46
    多线程高并发介绍 文章目录多线程高并发介绍前言一、什么是多线程?1.多线程介绍2.多线程实现原理?3.白话文解释多线程4.多线程存在的问题二、什么是高并发?1.高并发介绍2.如何提升系统的并发能力三、多线程...
  • Java面试题——多线程高并发

    千次阅读 2018-12-28 14:36:33
    什么是线程? 线程是进程中的一个实体,是被系统独立调度和分派的基本单位,它被包含在进程之中,是进程中的实际运作单位。线程自己不拥有系统资源,...我们通过多线程编程,能更高效的提高系统内多个程序间并发执行...
  • C++高并发多线程学习(一)

    千次阅读 2019-10-24 11:00:13
    与 C++11 多线程相关的头文件:3.各个主要功能模块概述3.1线程std::thread3.2互斥变量std::mutex,std::lock_guard,std::unique_lock3.3原子变量std::atomic3.4 条件变量condition_variable3.5获取方 std::future3.6 ...
  • 多线程高并发基本概念

    万次阅读 2018-07-26 23:27:52
    1.同步(Synchronous)与异步(Asynchronous) 同步和异步通常形容一次方法的调用。同步方法调用开始后调用者必须等到方法调用返回才能进行后续行为。... 并发和并行都能表示两个或个任务一起执...
  • mysql多线程update发生死锁

    千次阅读 2018-11-14 10:47:06
    mysql使用InnoDB引擎,在多线程并发的情况下,发现对数据库表中的数据进行更新操作时发生了死锁 基础知识 mysql 引擎 1、MyISAM:默认表类型,它是基于传统的ISAM类型,ISAM是Indexed Sequential Access Method (有...
  • 一文看懂JUC多线程高并发

    千次阅读 多人点赞 2020-03-12 14:38:56
    本文主要介绍JUC多线程以及高并发 如有需要,可以参考 如有帮助,不忘 点赞 ❥
  • 死锁 死锁-必要条件 (1)互斥条件 进程对所分配到的资源进行排他性的使用,即在一段时间内某个资源只由一个进程占用,如果此时还有其他进程请求资源,那么请求者只能等待,直到占用资源的进程将资源释放。 (2)...
  • 多线程面试题(值得收藏)

    万次阅读 多人点赞 2019-08-16 09:41:18
    金九银十快到了,即将进入找工作的高峰期,最新整理的最全多线程并发面试47题和答案总结,希望对想进BAT的同学有帮助,由于篇幅较长,建议收藏后细看~ 1、并发编程三要素? 1)原子性 原子性指的是一个或者多个操作,...
  • 1、方法中加入了注解式事物,且存在次操作数据库中同一条记录,场景:创建用户接口,需要同事创建用户、印章、用户印章关联关系记录,一个用户可以有个印章,但是只能有一个默认印章,接口如下: /** * 1、创建...
  • 这种update语句在事务中,高并发调用时。也会造成死锁! 因为surplus_count = surplus_count - 1, draw_count = draw_count + 1,update_time = now(),要先读出surplus_count、draw_count,now() 这种写法会先去...
  • 互不相让: 当两个或者更线程(或进程)相互持有对方所需要的资源, 又不主动释放锁, 导致所有人都无法继续执行下去, 导致程序陷入无尽的阻塞, 这就是死锁. 例如下图,线程A持有锁1 ,想去拿锁2, 线程B持有锁2, 想去拿...
  • 在某些情况下,可以防止死锁。我将在本文中描述三种技术: 锁排序 当线程需要相同的锁但以不同的顺序获取它们时,就会发生死锁。 如果确保任何线程始终以相同的顺序获取所有锁,则不会出现死锁
  • 一、线程与进程 ① 线程与进程的定义 线程 线程是进程的基本执行单元,一个进程的所有任务都在线程中执行; 进程要想执行任务,必须得有线程,进程至少要有一条线程; 程序启动会默认开启一条线程,这条线程被称为...
  • JAVA多线程高并发的处理经验

    万次阅读 多人点赞 2019-02-24 17:36:59
    java中的线程:java中,每个线程都有一个调用栈存放在线程栈之中,一个java应用总是从main()函数开始运行,被称为主线程。一旦创建一个新的线程,就会产生一个线程栈。线程总体分为:用户线程和守护线程,当所有...
  • JAVA高并发多线程必须懂的50个问题

    万次阅读 多人点赞 2017-06-30 14:28:19
    ImportNew 首页所有文章资讯Web架构基础技术书籍教程Java小组...2014/08/21 | 分类: 基础技术 | 27 条评论 | 标签: 多线程, 面试题 分享到: 692 本文由 ImportNew - 李 广 翻译自 javarevisited。欢迎加入翻译小

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 64,886
精华内容 25,954
关键字:

高并发导致多线程死锁