[Java] concurrent basis deadlock

Foreword

We use the locking mechanism to ensure thread-safe, but if used excessively locked, it may lead to a deadlock. The following describes the relevant knowledge about the deadlock and how we can prevent deadlocks in the preparation of the program.

What is a deadlock

Learning operating system, the definition given for the deadlock two or more threads in the implementation process, a blocking phenomenon caused due to compete for resources, the absence of external force, they will not be able to promote it. Simplified point is this: a group of threads compete for resources with each other because the wait for each other, leading to "permanent" blocking phenomenon .

Let's take a deep understanding of the deadlock through a transfer case in point.

class Account {
    private int balance;
    // 转账
    void transfer(Account target, int amt){
        if (this.balance > amt) {
            this.balance -= amt;
            target.balance += amt;
        }
    } 
}

In order to make the above transfer method transfer () does not exist concurrency problems, and soon we may want to use the Java synchronized modification transfer method, so the code is as follows:

class Account {
    private int balance;
    // 转账
    synchronized void transfer(Account target, int amt){
        if (this.balance > amt) {
            this.balance -= amt;
            target.balance += amt;
        }
    } 
}

Note that here we are using the built-in locks this, although the lock can protect our own balance, not to protect the balance target. The model we use a lock on a presentation This code is depicted in FIG :( follows from the reference [1])

More specifically, assume that A, B, C three account balance is $ 200, we perform two threads are two transfer operations: Account Account B 100 A transferred dollars, transferred to account Account B C 100 yuan, and finally we expected result should be a balance of the account is 100 yuan a, B account balance is $ 200, the balance of the account C is 300 yuan.
If there are two operating threads 1 and 2, the thread 1 performs the operation of the account A-to-B account, perform account thread 2 thread C B translocation account. These two threads are running on two of the CPU, due thisto this lock can only protect their balance and can not protect others, locking thread 1 is an example of account A (A.this), and the thread 2 is locked accounts examples of B (B.this), so the two threads enter the critical region transfer () simultaneously, two threads without mutual exclusion.
The result is likely to occur, two threads simultaneously read the B account balance of $ 200, resulting in the balance of the final accounts may be the B 300 (thread after thread 1 2 B.balance write, write B.balance value Thread 2 thread 1 is covered), may be 100 (thread 1 to thread the first to write B.balance 2, the thread writing B.balance value 1 is covered by a thread 2), it is unlikely to be 200.
Concurrent transfer schematic (FIG from Reference [1])

So we should use a can cover all the locks to protect the resource , if the default still remember when we talk about a modified static method synchronized lock object, then here it is easy to solve. The default lock is the class object class. So we can use Account.class as a lock to protect the transfer process.

class Account {
    private int balance;
    // 转账
    void transfer(Account target, int amt){
        synchronized(Account.class) {
            if (this.balance > amt) {
                this.balance -= amt;
                target.balance += amt;
            }
        }
    } 
}

Although this program concurrency problems do not exist, but the transfer operation of all accounts are serial. The real world, accounts A transfer account B, C account transfer account transfer operation D Both the real world is parallel. Compared to the actual situation, this program becomes poor performance.

So, we try to mimic real-world transfer operation:
each account has a books, these books are stored in a unified file rack. When the transfer A to B transfers account, the teller will go for the A and B books books do register, this time at the teller to take the books will meet three conditions:

  1. A file rack happen to have books and books B, then while away;
  2. If the file rack Only one of the books A and B books, and that the teller would put some file shelves books get our hands on, while waiting for the other teller sent back to the other books;
  3. A and B books are not books, that the teller waiting for two books are sent back

In programming, we can use two locks to achieve this process. In () internal method transfer, we first attempt to lock the roll-out account this (A first books get our hands on), and then try to lock into account target (B books and then get our hands on), only when both are successful before execution transfer operation.
This logic may be patterned in this way the FIG., (Refer to FIG from [1]):

code show as below:

class Account {
    private int balance;
    // 转账
    void transfer(Account target, int amt){
        // 锁定转出账户A
        synchronized(this) {              
            // 锁定转入账户B
            synchronized(target) {           
                if (this.balance > amt) {
                    this.balance -= amt;
                    target.balance += amt;
                }
            }
        }
    } 
}

After this optimization, account transfer account A and the account B C D transfer account both the parallel transfer operation can be.

But this has lead to a deadlock. For example: The transfers do Zhang teller operating the account A B account transfer, account transfer account do teller John Doe B translocation of the C account. They two simultaneously operated, so that the following situation occurs from FIG :( Reference [1])

It will always be waiting for the other books into the file rack, resulting in a situation of stalemate has been.

About this phenomenon, we can also help resource allocation map to visualize occupancy locks (resource allocation graph is a directed graph, it can describe the resources and the state of the thread). Wherein the resource is represented by a square node, represented by a circular thread node; resources side points to the thread indicates that the thread that the resource has been obtained, the thread edges to the resource request indicates resources thread, but has not yet been. (FIG from the reference [1])

Once Java concurrent programs deadlocks, generally do not have particularly good way, the only way to recover is to suspend the application and restart. Therefore, we must try to avoid a deadlock, it is best not to have a deadlock. Do you want to know how to do a deadlock, we must first know what will happen deadlock condition.

The four necessary conditions for deadlock

虽然进程在运行过程中,可能发生死锁,但死锁的发生也必须具备一定的条件,死锁的发生必须具备以下四个必要条件:

  • 互斥,共享资源 X 和 Y 只能被一个线程占用;
  • 占有且等待,线程 T1 已经取得共享资源 X,在等待共享资源 Y 的时候,不释放共享资源 X;
  • 不可抢占,其他线程不能强行抢占线程 T1 占有的资源;
  • 循环等待,线程 T1 等待线程 T2 占有的资源,线程 T2 等待线程 T1 占有的资源,就是循环等待。

破坏死锁发生的条件预防死锁

只有这四个条件都发生时才会出现死锁,那么反过来,也就是说只要我们破坏其中一个,就可以成功预防死锁的发生

四个条件中我们不能破坏互斥,因为我们使用锁目的就是保证资源被互斥访问,于是我们就对其他三个条件进行破坏:

  • 占用且等待:一次性申请所有的资源,这样就不存在等待了。
  • 不可抢占,占用部分资源的线程进一步申请其他资源时,如果申请不到,可以主动释放它占有的资源。
  • 循环等待,靠按序申请资源来预防。所谓按序申请,是指资源是有线性顺序的,申请的时候可以先申请资源序号小的,再申请资源序号大的,这样线性化申请后就不存在循环了。

下面我们使用这些方法去解决如上的死锁问题。

破坏占用且等待条件

一次性申请完所有资源。我们设置一个管理员来管理账本,柜员同时申请需要的账本,而管理员同时出他们需要的账本。如果不能同时出借,则柜员就需要等待。

“同时申请”:这个操作是一个临界区,含有两个操作,同时申请资源apply()和同时释放资源free()。

class Allocator {
    private List<Object> als = new ArrayList<>();
    // 一次性申请所有资源
    synchronized boolean apply( Object from, Object to){
        if(als.contains(from) || als.contains(to)){    //from 或者 to账户被其他线程拥有
            return false;  
        } else {
            als.add(from);
            als.add(to);  
        }
        return true;
    }
    // 归还资源
    synchronized void free(Object from, Object to){
        als.remove(from);
        als.remove(to);
    }
}

class Account {
    // actr 应该为单例,只能由一个人来分配资源
    private Allocator actr;
    private int balance;
    // 转账
    void transfer(Account target, int amt){
        // 一次性申请转出账户和转入账户,直到成功
        while(!actr.apply(this, target))  //最好可以加个timeout避免一直循环
            ;
            try{
                // 锁定转出账户
                synchronized(this){ //存在客户对自己账户的操作
                    // 锁定转入账户
                    synchronized(target){           
                        if (this.balance > amt){
                            this.balance -= amt;
                            target.balance += amt;
                        }
                    }
                }
            } finally {
                actr.free(this, target)    //释放资源
            }
    }
}

破坏不可抢占条件

破坏不抢占要能够主动释放它占有的资源,但synchronized是做不到的。原因为synchronized申请不到资源时,线程直接进入了阻塞状态,而线程进入了阻塞状态也就没有办法释放它占有的资源了。不过SDK中的java.util.concurrent提供了Lock解决这个问题。

支持定时的锁

显示使用Lock类中的定时tryLock功能来代替内置锁机制,可以检测死锁和从死锁中恢复过来。使用内置锁的线程获取不到锁会被阻塞,而显示锁可以指定一个超时时限(Timeout),在等待超过该时间后tryLock就会返回一个失败信息,也会释放其拥有的资源。

破坏循环等待条件

破坏这个条件,需要对资源进行排序,然后按序申请资源。我们假设每个账户都有不同的属性 id,这个 id 可以作为排序字段,申请的时候,我们可以按照从小到大的顺序来申请。
比如下面代码中,①~⑤处的代码对转出账户(this)和转入账户(target)排序,然后按照序号从小到大的顺序锁定账户。这样就不存在“循环”等待了。

class Account {
    private int id;
    private int balance;
    // 转账
    void transfer(Account target, int amt){
        Account left = this            // ①
            Account right = target;    // ②
        if (this.id > target.id) {     // ③
            left = target;             // ④
            right = this;              // ⑤
        }                          
        // 锁定序号小的账户
        synchronized(left){
            // 锁定序号大的账户
            synchronized(right){ 
                if (this.balance > amt){
                    this.balance -= amt;
                    target.balance += amt;
                }
            }
        }
    } 
}

小结

记得学习操作系统时还有避免死锁,其和预防死锁的区别在于:预防死锁是设法至少破坏产生死锁的四个必要条件之一,严格地防止死锁的出现,但是这也会使系统性能降低;而避免死锁则不那么严格的限制产生死锁的必要条件的存在,因为即使死锁的必要条件存在,也不一定发生死锁,死锁避免是在系统运行过程中注意避免死锁的最终发生。避免死锁的经典算法就是银行家算法,这里就不扩开介绍了。

还有一个避免出现死锁的结论:如果所有线程以固定顺序来获得锁,那么在程序中就不会出现锁顺序死锁问题。查看参考[4]理解。

我们使用细粒度锁锁住多个资源时,要注意死锁的产生。只有先嗅到死锁的味道,才有我们的施展之地。

参考:
[1]极客时间专栏王宝令《Java并发编程实战》
[2]Brian Goetz.Tim Peierls. et al.Java并发编程实战[M].北京:机械工业出版社,2016
[3]iywwuyifan.避免死锁和预防思索的区别.https://blog.csdn.net/masterchiefcc/article/details/83303813
[4]AddoilDan.死锁面试题(什么是死锁,产生死锁的原因及必要条件).https://blog.csdn.net/hd12370/article/details/82814348

Guess you like

Origin www.cnblogs.com/myworld7/p/12230010.html