关于内存安全,线程安全,死锁(中)

接上,死锁问题

1.原因

定义之前已经阐述,这里先上一个死锁最简单的例子:

//线程1:
    public void leftRight() {
        // 得到left锁
        synchronized (left) {
            // 得到right锁
            synchronized (right) {
                doSomething();
            }
        }
    }


//线程2:
    public void rightLeft() {
        // 得到right锁
        synchronized (right) {
            // 得到left锁
            synchronized (left) {
                doSomethingElse();
            }
        }
    }


可以看到,1线程锁住left,现在要right,不给就等,等的时候left也不解锁;2线程同时开始运行,于是锁住right,要left,不给也等,等的时候right也不解锁。于是谁都进行不下去。

结合例子看死锁条件:

1.互斥:即独占,synchronized锁是表面原因,一个人占用了就不给其他人用,否则1线程就不需要等待left了,直接拿来用就行了。根本原因是被锁的东西的属性,属性要求不能被同时共享,如打印机,如对账户的修改。

2.不剥夺:即线程1占用了left之后,不会因为你2来了就会给你,只有我运行完了才释放,2也是这么想的。

3.保持和请求:如果单单不剥夺,比如线程1只能占用一个线程,现在占着left,就够用了,不会要求right也就没事了,但还是必须要吃着碗里的看着锅里的才会导致死锁。

4.循环等待:对资源的需求链要能构成闭环,死锁不是卡住,错误原因必须要是来自逻辑的谬误而非打印机坏了:2欠了1的钱,3欠了2的钱,3欠了1的钱,大家在内部越借越多,全是次级信贷,借了一圈到了该还钱的都傻眼了,金融危机爆发;如果123都是找我要钱,我就是不给,虽然后果是一样的,但不能称之为死锁。

2.如何避免

其实标题应该是编程中如何避免死锁,有哪些避免的措施,像银行家算法这类操作系统层面的东西暂不考虑

1.保证上锁顺序

举个例子,设线程1,2用到的资源并集是ACD(即两个都用的资源),当线程1依次对ACD上锁时,我们发现,如果2对资源中A,C,D,无论何时,无论和其他资源什么顺序,只要保证这三个资源的上锁顺序是依次的,就可以避免死锁。其实相当于ACD三个资源合并了。

举一个常用的转账例子:

从账户from转到账户to,显然这两个账户都要上锁,如果这么写:

//先锁转出账户
synchronized (fromAccount) { 
//再锁转入账户
        synchronized (toAccount) {
            //查询是否有余额
            if (fromAccount.getBalance().compareTo(amount) < 0)
                throw new InsufficientFundsException();
            else {
                //转出账户减钱,转入的加钱
                fromAccount.debit(amount);
                toAccount.credit(amount);
            }
        }
    }

看起来是很有道理的,所有人都先锁转出账户,再锁转入账户,完美。

其实这帮助我们认识到一个常见的顺序上锁误区:当我们在谈论A,C,D时,我们在谈论什么?

这时刚刚那个转账的方法头:

public static void transferMoney(Account fromAccount,Account toAccount,DollarAmount amount)

现在有这样一个场景:我在淘宝上买东西,给马云10块,马云分红,给我1亿:

//taobao买东西
public static void transferMoney(Account myAccount,Account mayunAccount,DollarAmount 10)
//分红
public static void transferMoney(Account mayunAccount,Account myAccount,DollarAmount 100000000)

线程1会先锁我的账户并要马云账户锁,线程2会先锁马云账户并要我账户锁,仍然死锁了。所以from和to只是形参而已,不是资源的肉身。引入两个方法的概念:

S1.hashCode:根据S1的字符序列计算其哈希值;

S1.identityHashCode:根据S1的内存地址来计算哈希值。

内存地址可以在一些情况下确定资源的肉身:

public class InduceLockOrder {
    private static final Object tieLock = new Object();

    public void transferMoney(final Account fromAcct,
                              final Account toAcct,
                              final DollarAmount amount)
            throws InsufficientFundsException {
        class Helper {
            public void transfer() throws InsufficientFundsException {
                if (fromAcct.getBalance().compareTo(amount) < 0)
                    throw new InsufficientFundsException();
                else {
                    fromAcct.debit(amount);
                    toAcct.credit(amount);
                }
            }
        }
        /*
        获取资源的内存地址的哈希值
        根据内存地址来规范资源被锁的顺序        
        能保证所有线程对资源上锁的顺序一致
        */
        int fromHash = System.identityHashCode(fromAcct);
        int toHash = System.identityHashCode(toAcct);

        if (fromHash < toHash) {
            synchronized (fromAcct) {
                synchronized (toAcct) {
                    new Helper().transfer();
                }
            }
        } else if (fromHash > toHash) {
            synchronized (toAcct) {
                synchronized (fromAcct) {
                    new Helper().transfer();
                }
            }
        } else {
            synchronized (tieLock) {
                synchronized (fromAcct) {
                    synchronized (toAcct) {
                        new Helper().transfer();
                    }
                }
            }
        }
    }}

总结来说,当有100个资源要共享时,要为他们每个人安排在这个独一无二的标号,不管每个线程申请哪些,都按照从小到大(一个顺序,都是大到小也可以)的顺序加锁。

2.时限锁

打破了保持和请求的条件,拿不到不头铁,不硬等。例如使用可中断锁trylock():没获取返回false,

boolean tryLock(long time, TimeUnit unit):在设置等待时间内没有获取则返回false。

3.检测

可以用工具对代码进行审查,参考资料:https://blog.csdn.net/abc86319253/article/details/49534225

原理大致是在一个线程获取锁时,会有记录在一个数据结构中,通过遍历这个结构形成关系图,查看对资源的占有是否满足死锁。

我不知道多少人会由此想到图灵的停机问题:

图灵在1936年证明这样一个算法是不存在的:该过程以一个计算机程序以及该程序的一个输入作为输入,并判断该过程在给定输入运行时是否最终能停止。

???   检测工具不就是能判断输入程序是否会终止咩?

这段博客中的话着实不严谨,真正的问题描述不是“一个程序作为输入”而是“任意一个程序作为输入”,它的证明方法也是将自身作为程序带入自身。(话说,把代码审查的代码带入代码审查会发生什么?----之后开一个坑,分享看《论可计算数》学到的知识)

猜你喜欢

转载自blog.csdn.net/cn_leeyiru_static/article/details/82771267