zoukankan      html  css  js  c++  java
  • 并发编程学习笔记之死锁(八)

    死锁

    每个人手里都有其他人需要的资源,自己又不会放下手上的资源,这么一直等待下去,就会发生死锁.

    当一个线程永远占有一个锁,而其他线程尝试去获得这个锁,那么它们将永远被阻塞.

    当线程A占有锁L时,想要获得锁M,同时线程B持有M,并尝试得到L,两个线程将永远等待下去,这种情况是死锁最简单的形式(或称致命的拥抱,deadly embrace)

    数据库不会发生死锁的情况,它会选择一个牺牲者,强行释放锁,让程序可以继续执行下去.

    JVM不行,只能重启程序.

    死锁并不会每次都出现

    死锁很少能立即发现.一个类如果有发生死锁的潜在可能并不意味着每次都将发生,它只发生在该发生的时候.

    当死锁出现的时候,往往是遇到了最不幸的时候--- 在高负载下.

    锁顺序死锁

    public class LeftRightDeadLock {
        private Object leftLock = new Object();
        private Object rightLock = new Object();
    
    
        public void getLeftLock(){
            synchronized (this.rightLock){
                synchronized (this.leftLock){
                    //do something
                }
            }
        }
    
    
        public void getRightLock(){
            synchronized (this.leftLock){
                synchronized (this.rightLock){
                    //do something.
                }
            }
        }
    }
    

    两个线程分别进入getRightLock和getLeftLock方法,同时获得第一个锁,在等待下一个锁的时候,就会发生锁顺序死锁.

    发生死锁的原因: 两个线程试图通过不同的顺序获得多个相同的锁.

    如果请求的顺序相同就不会出现循环的锁依赖现象,就不会产生死锁了.

    如果所有线程以通用的固定秩序获得锁,程序就不会出现锁顺序死锁问题了.
    

    动态的锁顺序死锁

    public class DynamicDeadLock {
    
        public void transferMoney(Account fromAcount,Account toAccount){
            synchronized (fromAcount){
                synchronized (toAccount){
                    //转账操作
                }
            }
        }
    }
    

    当两个线程同时调用transferMoney,一个从X向Y转账,另一个从Y向X转账,那就会发生死锁.

    transferMoney(myAccount,yourAccount)
    
    transferMoney(yourAccount,myAccount)
    

    之前说了,造成死锁的原因就是以不同的顺序获得相同的锁.

    那么要解决这个问题,我们就必须制定锁的顺序.

    System.indentityHashCode(传入对象)方法可以得到对象的哈希码.我们通过哈希码来决定锁的顺序.

    public class DynamicDeadLock {
    
        private Object obj = new Object();
    
    
        public void transferMoney(Account fromAcount,Account toAccount){
        //这个内部类秒啊,可以减少重复代码
            class Helper {
                public void transferMoney(){
                    //真正的转账操作..
                    //假装使用 外部的两个参数 fromAcount和toAccount做一下操作..
                }
            }
            //制定锁的顺序
            int fromHash = System.identityHashCode(fromAcount);
            int toHash = System.identityHashCode(toAccount);
    
            if(fromHash<toHash){
                synchronized (fromAcount){
                    synchronized (toAccount){
                        new Helper().transferMoney();
                    }
                }
            }else if(fromHash>toHash){
                synchronized (toAccount){
                    synchronized (fromAcount){
                        new Helper().transferMoney();
                    }
                }
            }else{
                //使用成员变量的锁
                synchronized (obj){
                    synchronized (fromAcount){
                        synchronized (toAccount){
                            new Helper().transferMoney();
                        }
                    }
                }
            }
        }
    }
    
    

    虽然有点麻烦,但是减少了发生死锁的可能性.

    注意上面代码的最后一种else的情况,使用了一个额外的obj的锁,这是因为极少数的情况下会出现hashcode相同的情况,当hashCode相同的时候,使用之前的两种顺序锁,两个线程同时调用两个方法,参数换位,颠倒顺序计算哈希值,就又有了出现死锁的可能,所以引入第三种锁来保证锁的顺序,从而减少死锁发生的可能性.

    如果经常出现hash值冲突,那么并发性会降低(因为多加了一个锁),但是因为
    System.identityHashCode的哈希冲突出现频率很低,所以这个技术以最小的代价,换来了最大的安全性.

    如果Account具有一个唯一的,不可变的,并且具有可比性的key,比如账号,那么就可以通过账号来排定对象顺序,这样就能省去obj的锁了.

    协作对象间的死锁

    public class A {
    
        private final B b ;
    
        public A(B b) {
            this.b = b;
        }
    
        public  synchronized  void methodA(){
            //do something.
    
            //调用B的同步的方法
            b.methodB();
    
        }
    }
    
    
    public class B {
        private final A a;
    
        public B(A a) {
            this.a = a;
        }
    
        public synchronized void methodB(){
            //do something
    
            //调用A的同步的方法
            a.methodA();
        }
    }
    
    

    在持有锁的时候调用外部方法是在挑战活跃度问题,外部方法可能会获得其他锁(产生死锁的风险),或者遭遇严重超时的阻塞,当你持有锁的时候会延迟其他试图获得该锁的线程

    开放调用

    在持有锁的时候调用一个外部方法很难进行分析,因此是危险的.

    当调用的方法不需要持有锁时,这被称为开放调用(open call). 依赖于开放调用的类更容易与其他的类合作.

    使用开放调用来避免死锁类似于使用封装来提供线程安全:对一个有效封装的类进行线程安全分析,要比分析没有封装的类容易得多.

    类似地,分析一个完全依赖于开放调用的程序的程序活跃度,比分析哪些非开放调用的程序更简单.

    尽量让你自己使用开放调用,这比获得多重锁后识别代码路径更简单,因为可以确保使用一致的顺序获得锁.

    不使用synchronized修饰方法,减少synchronized包住的代码块,来避免协作对象间的死锁.

    public class A {
    
        private final B b;
    
        public A(B b) {
            this.b = b;
        }
    
     
       public void methodA() {
            //关键在这
            synchronized (this) {
                //do something.
            }
    
            //调用B的同步的方法
            b.methodB();
    
        }
    }
    

    除了能避免死锁以外,因为同步的代码块变小,所以使得响应速度得到提高.

    在程序中尽量使用开放调用.依赖于开放调用的程序,相比于那些在持有锁的时候还调用外部方法的程序,更容易进行死锁自由度(deadlock-freedom)的分析.
    

    在同步方法之间互相调用的时候,尽量使用开放调用来避免死锁.

    避免和诊断死锁

    使用定时的锁

    使用显示的Lock类中定时tryLock方法来替代synchronized,可以设置超时时间,超时会失败,这样避免了死锁.

    其他的活跃度失败.

    除了死锁,还有一些其他的活跃度危险:

    • 饥饿
    • 丢失信号
    • 活锁

    饥饿

    当线程访问它所需要的资源时却被永久拒绝,以至于不能再继续进行,这样就发生了饥饿(starvation).

    引发饥饿的情况:

    • 使用线程的优先级不当
    • 在锁中执行无终止的构建(无限循环,或者无尽等待资源).

    归根结底是因为线程不能再执行.

    线程优先级并不是方便的工具,它改变线程优先级的效果往往不明显;提高一个线程的优先级往往什么都不能改变,或者总是会引起一个线程的调度优先高于其他线程,从而导致饥饿.

    抵制使用线程优先级的诱惑,因为这会增加平台依赖性,并且可能引起活跃度问题.大多数并发应用程序可以对所有线程使用相同的优先级.
    

    弱响应性

    当计算密集型后台计算任务影响到响应性时,这种情况下可以使用线程优先级.降低执行后台任务的线程的优先级,从而提高程序的响应性.

    活锁

    活锁(livelock)是线程活跃度失败的另一种形式,尽管没有被阻塞,线程缺仍然不能继续,因为他不断重试相同的操作,却总是失败.

    例如程序处理一段代码出错了,业务逻辑使它回退重复执行,然后又错了,再回退重新执行,如此反复.这就是活锁.

    这种形式的活跃通常来源于过渡的错误恢复代码,误将不可修复的错误当做是可修复的错误.

    还有另一个例子: 多个相互协作的线程间,他们为了彼此响应而修改了状态,使得没有一个线程能够继续前进,那么就发生了活锁.

    就好比两个有礼貌的人在路上相遇,他们给对方让路,于是在另一条路又遇上了,如此反复...

    在并发程序中,通过随机等待和撤回来进行重试能够相当有效地避免活锁的发生.

    总结:

    活跃度失败是非常严重的问题,因为除了中止应用程序,没有任何机制可以恢复这种失败.

    最常见的活跃度失败是死锁.应该在设计时就避免锁顺序死锁:确保多个线程在获得多个锁时,使用一致的顺序.

    最好的解决方法是在程序中使用开放调用,这会大大减少一个线程一次请求多个锁的情况.

    下篇会更新提高响应速度的方式.

  • 相关阅读:
    找的好网站(macdow语法,扫描二维码,)
    c语言中static的作用以及(递归,八大算法原理)
    WKWebView加载Html文件,如何自适应网页内容呢?就是不要让它左右滑动
    iOS 8.0模拟器键盘弹出以及中文输入
    sizeof与strlen的理解
    各种效果原理(抽屉,多个tableView复用)
    激励自己的话
    IT培训出来的人为什么难找工作,各种纠结
    如何在跟新xcode后跟新插件
    Objective-C中的Block
  • 原文地址:https://www.cnblogs.com/xisuo/p/9843450.html
Copyright © 2011-2022 走看看