美文网首页Java
线程之活跃度失败(死锁、活锁、饥饿)

线程之活跃度失败(死锁、活锁、饥饿)

作者: 安仔夏天勤奋 | 来源:发表于2019-04-29 14:12 被阅读5次

    线程活跃度

    活跃度问题是指线程或进程长时间得不到cpu占用。《Java并发编程实战》中提到,无论执行计算密集操作还是执行某个可能阻塞的操作,如果持有锁的时间过长,都会带来活跃性或性能问题。

    活跃度失败有那几种

    • 死锁也就是互相等着对方释放资源,结果谁也得不到。

    • 活锁可能发生让某一个线程一直处于等待状态,其他线程都可以调用到。

    • 饥饿我就感觉用抢占式说好说,每次来就执行优先级高的,那么优先级低的可能永远执行不到。

    死锁

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

    死锁产生的原因

    • 系统资源的竞争

      通常系统中拥有的不可剥夺资源,其数量不足以满足多个进程运行的需要,使得进程在 运行过程中,会因争夺资源而陷入僵局,如磁带机、打印机等。只有对不可剥夺资源的竞争 才可能产生死锁,对可剥夺资源的竞争是不会引起死锁的。

    • 进程推进顺序非法

      进程在运行过程中,请求和释放资源的顺序不当,也同样会导致死锁。例如,并发进程 P1、P2分别保持了资源R1、R2,而进程P1申请资源R2,进程P2申请资源R1时,两者都 会因为所需资源被占用而阻塞。

      信号量使用不当也会造成死锁。进程间彼此相互等待对方发来的消息,结果也会使得这 些进程间无法继续向前推进。例如,进程A等待进程B发的消息,进程B又在等待进程A 发的消息,可以看出进程A和B不是因为竞争同一资源,而是在等待对方的资源导致死锁

    死锁产生的必要四个条件

    产生死锁必须同时满足以下四个条件,只要其中任一条件不成立,死锁就不会发生。

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

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

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

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

    写一个死锁的例子

    /**
     * @Author 安仔夏天勤奋
     * Create Date is  2019/4/29
     * Des
     */
    public class LockThread1 implements Runnable {
        @Override
        public void run() {
            ////模拟线程1占用资源1并申请获得资源2的锁
            try {
                System.out.println("LockThread1 is running");
                synchronized (ThreadResource.resource1) {
                    System.out.println("LockThread1 lock resource1");
                    Thread.sleep(2000);//休眠2s等待线程2锁定资源2
                    synchronized (ThreadResource.resource2){
                        System.out.println("LockThread1 lock resource2");
                    }
                    System.out.println("LockThread1 release resource2");
                }
                System.out.println("LockThread1 release resource1");
            } catch (Exception e) {
                System.out.println(e.getMessage());
            }
            System.out.println("LockThread1 is stop");
        }
    }
    
    /**
     * @Author 安仔夏天勤奋
     * Create Date is  2019/4/29
     * Des
     */
    public class LockThread2 implements Runnable {
        @Override
        public void run() {
            //模拟线程2占用资源2并申请获得资源1的锁:
            try {
                System.out.println("LockThread2 is running");
                synchronized (ThreadResource.resource2) {
                    System.out.println("LockThread2 lock resource2");
                    Thread.sleep(2000);//休眠2s等待线程1锁定资源1
                    synchronized (ThreadResource.resource1) {
                        System.out.println("LockThread2 lock resource1");
                    }
                    System.out.println("LockThread2 release resource1");
                }
                System.out.println("LockThread2 release resource2");
            } catch (Exception e) {
                System.out.println(e.getMessage());
            }
            System.out.println("LockThread2 is stop");
        }
    }
    
    /**
     * @Author 安仔夏天勤奋
     * Create Date is  2019/4/29
     * Des
     */
    public class ThreadResource {
        public static Object resource1 = new Object();
        public static Object resource2 = new Object();
    }
    
    /**
     * @Author 安仔夏天勤奋
     * Create Date is  2019/4/29
     * Des
     */
    public class DeadLock{
        public static void main(String[] args) {
            new Thread(new LockThread1()).start();
            new Thread(new LockThread2()).start();
        }
    }
    

    运行结果

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

    如何避免死锁

    死锁是可以避免的。用于避免死锁的技术三种方式:

    • 加锁顺序(线程按照一定的顺序加锁)

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

    • 死锁检测

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

    /**
     * @Author 安仔夏天勤奋
     * Create Date is  2019/4/29
     * Des
     */
    public class LockThread1 implements Runnable {
        @Override
        public void run() {
            try {
                System.out.println("LockThread1 is running");
                synchronized (ThreadResource.resource1) {
                    System.out.println("LockThread1 lock resource1");
                    Thread.sleep(2000);//休眠2s等待线程2锁定资源2
                }
                System.out.println("LockThread1 release resource1");
                synchronized (ThreadResource.resource2) {
                    System.out.println("LockThread1 lock resource2");
                }
                System.out.println("LockThread1 release resource2");
            } catch (Exception e) {
                System.out.println(e.getMessage());
            }
            System.out.println("LockThread1 is stop");
    }
    
    /**
     * @Author 安仔夏天勤奋
     * Create Date is  2019/4/29
     * Des
     */
    public class LockThread2 implements Runnable {
        @Override
        public void run() {
            try {
                System.out.println("LockThread2 is running");
                synchronized (ThreadResource.resource2) {
                    System.out.println("LockThread2 lock resource2");
                    Thread.sleep(2000);//休眠2s等待线程1锁定资源1
                }
                System.out.println("LockThread2 release resource2");
                synchronized (ThreadResource.resource1) {
                    System.out.println("LockThread2 lock resource1");
                }
                System.out.println("LockThread2 release resource1");
            } catch (Exception e) {
                System.out.println(e.getMessage());
            }
            System.out.println("LockThread2 is stop");
        }
    }
    

    运行结果

    LockThread1 is running
    LockThread1 lock resource1
    LockThread2 is running
    LockThread2 lock resource2
    LockThread1 release resource1
    LockThread1 lock resource2
    LockThread1 release resource2
    LockThread1 is stop
    LockThread2 release resource2
    LockThread2 lock resource1
    LockThread2 release resource1
    LockThread2 is stop
    

    如果需要同时去锁定两个对象,可以根据加锁顺序定义一个先后的规则。按照上面的例子,需要同时锁定两个资源,可以根据资源的hashcode值大小来判断先后锁定顺序。代码如下:

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

    死锁更详细可参照

    活锁

    活锁是指线程1可以使用资源,但它很礼貌,让其他线程先使用资源,线程2也可以使用资源,但它很绅士,也让其他线程先使用资源。这样你让我,我让你,最后两个线程都无法使用资源。

    活锁不会被阻塞,而是不停检测一个永远不可能为真的条件。除去进程本身持有的资源外,活锁状态的进程会持续耗费宝贵的CPU时间。

    举个例子,两个人在走廊上碰见,大家都互相很有礼貌,互相礼让,A从左到右,B也从从左转向右,发现又挡住了地方,继续转换方向,但又碰到了,反反复复,一直没有机会运行下去。

    活锁例子

    活锁的解决方法

    • 调整重试机制。

    • 引入一些随机性。

    活锁和死锁的区别

    • 活锁的实体是在不断的改变状态,所谓的“活”, 而处于死锁的实体表现为等待。

    • 活锁有可能自行解开,死锁则不能。

    饥饿

    饥饿是指如果线程T1占用了资源R,线程T2又请求封锁R,于是T2等待。T3也请求资源R,当T1释放了R上的封锁后,系统首先批准了T3的请求,T2仍然等待。然后T4又请求封锁R,当T3释放了R上的封锁之后,系统又批准了T4的请求......,T2可能永远等待。

    线程长时间无法获得共享资源从而继续相继的处理。这种情况经常发生在当共享资源被“贪婪”线程长时间占据时。假设一个对象提供的互斥方法需要很长时间处理才能返回,然而如果某线程老是频繁激活这个方法,那么其他需要访问该对象的线程就会被长时间阻塞,而处于饥饿状态。

    饥饿.png

    Java中的读写锁的实现类ReentranctReadWriteLock,在默认使用非公平模式(不是先来先处理的模式)的情况下,如果某个线程想要读取资源,只要没有线程正在对该资源进行写操作且没有线程请求对该资源的写操作即可。如果读操作发生的比较频繁,我们又没有提升写操作的优先级,那么就会产生“饥饿”现象。请求写操作的线程会一直阻塞,直到所有的读线程都从ReentranctReadWriteLock上解锁了。如果一直保证新线程的读操作权限,那么等待写操作的线程就会一直阻塞下去,结果就发生了“饥饿”。

    java代码会引起这种类型的饥饿

    synchronized(obj) {
     while (true) {
     // .... infinite loop
     }
    }
    

    优先级引起也会引起线程饥饿

    高优先级线程吞噬所有的低优先级线程的CPU时间。例如在java中调用了Thread.setPriority方法设置了线程优先级,优先级低的线程始终得不到执行的机会,虽然线程优先级对于不同操作系统的实现方式不一样,即便设置了优先级也不一定会有效果,但还是有可能会出现这种情况。

    饥饿的解决办法有

    • 提升写请求的优先级或者采用公平策略。

    • 在synchronized方法或者块中避免无限循环。

    • 采用线程默认的优先级。

    相关文章

      网友评论

        本文标题:线程之活跃度失败(死锁、活锁、饥饿)

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