18.透彻理解死锁

Posted 纵横千里,捭阖四方

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了18.透彻理解死锁相关的知识,希望对你有一定的参考价值。

synchronized同步锁,虽然能解决线程安全的问题,但是如果使用不当,就可能导致死锁,也即请求被阻塞而一直无法返回。

除了死锁,还有个活锁的情况,我们看一下概念的区别:

  • 死锁: 一组互相竞争资源的线程因互相等待,导致“永久”阻塞的现象。

  • 活锁: 活锁指的是任务或者执行者没有被阻塞,由于某些条件没有满足,导致一直重复尝试—失败—尝试—失败的过程。也就是“生不如死”的状态,不过处于活锁的实体是在不断的改变状态,活锁有可能自行解开。

这四个条件同时满足,就会产生死锁。

  • 互斥,共享资源 X 和 Y 只能被一个线程占用;

  • 占有且等待,线程 T1 已经取得共享资源 X,在等待共享资源 Y 的时候,不释放共享资源 X;

  • 不可抢占,其他线程不能强行抢占线程 T1 占有的资源;

  • 循环等待,线程 T1 等待线程 T2 占有的资源,线程 T2 等待线程 T1 占有的资源,就是循环等待。

1 构造一个死锁

接下来,我们构造一个简单的死锁程序来分析一下这个场景。

public class DeadLockDemo 
    private static String A = "A";
    private static String B = "B";

    public static void main(String[] args) 
        new DeadLockDemo().deadLock();
    

    private void deadLock() 
        Thread t1 = new Thread(new Runnable() 
            @Override
            public void run() 
                synchronized (A) 
                    try 
                        Thread.currentThread().sleep(200);
                     catch (InterruptedException e) 
                        e.printStackTrace();
                    
                    synchronized (B) 
                        System.out.println("1");
                    
                
            
        );

        Thread t2 = new Thread(new Runnable() 
            @Override
            public void run() 
                synchronized (B) 
                    try 
                        Thread.currentThread().sleep(200);
                     catch (InterruptedException e) 
                        e.printStackTrace();
                    
                    synchronized (A) 
                        System.out.println("2");
                    
                
            
        );
        t1.start();
        t2.start();
    

这个例子中,线程1 先给A加锁,然后就睡觉了,之后再给B加锁才能执行完。线程2,则给B加锁后就睡觉,之后再给A加锁才能执行完。所以这里两个线程就死锁了。 执行之后检验一下情况:使用jps 查看进程号,然后使用jstack 查看,结果如下:

Found one Java-level deadlock:
=============================
"Thread-1":
  waiting to lock monitor 0x04ddd074 (object 0x0f39ae88, a java.lang.String),
  which is held by "Thread-0"
"Thread-0":
  waiting to lock monitor 0x04dddd94 (object 0x0f39aea8, a java.lang.String),
  which is held by "Thread-1"

Java stack information for the threads listed above:
===================================================
"Thread-1":
        at com.liuqingchao.threadTest.DeadLockDemo$2.run(DeadLockDemo.java:38)
        - waiting to lock <0x0f39ae88> (a java.lang.String)
        - locked <0x0f39aea8> (a java.lang.String)
        at java.lang.Thread.run(Thread.java:748)
"Thread-0":
        at com.liuqingchao.threadTest.DeadLockDemo$1.run(DeadLockDemo.java:22)
        - waiting to lock <0x0f39aea8> (a java.lang.String)
        - locked <0x0f39ae88> (a java.lang.String)
        at java.lang.Thread.run(Thread.java:748)

Found 1 deadlock.

上面的内容说人话就是: 找到一个java级别的死锁: Thread1 锁了0x0f39aea8 ,等待去锁 object 0x0f39ae88,但是这个被Thread 0 持有 Thread0 锁了0x0f39ae88,等待去锁 object 0x0f39aea8,这个被 Thread1 持有 所以就死锁了。

2 死锁预防措施

要预防死锁,只要破坏掉其4条规则的任何一个都可以。因此我们有多种策略来解除死锁。 1.针对互斥,重写代码逻辑。调整资源分配策略,先尝试都能拿到资源再去占有,例如:

public class Allocator 
    private List<Object> list=new ArrayList<>();
    synchronized  boolean apply(Object from,Object to)
        if(list.contains(from)||list.contains(to))
            return false;
        
        list.add(from);
        list.add(to);
        return true;
    

    synchronized void free(Object from,Object to)
        list.remove(from);
        list.remove(to);
    

2.不用synchronized,而使用ReentrantLock,加锁的时候使用fromLock.tryLock(),解决条件2 和3。

public class TransferAccount implements Runnable 
    private Account fromAccount; //转出账户
    private Account toAccount; //转入账户
    private int amount;
    Lock fromLock = new ReentrantLock();
    Lock toLock = new ReentrantLock();

    public TransferAccount(Account fromAccount, Account toAccount, int amount) 
        this.fromAccount = fromAccount;
        this.toAccount = toAccount;
        this.amount = amount;
    

    @Override
    public void run() 
        while (true) 
            if (fromLock.tryLock())  //返回true和false
                if (toLock.tryLock()) //返回true和false
                    if (fromAccount.getBalance() >= amount) 
                        fromAccount.debit(amount);
                        toAccount.credit(amount);
                    
                
            
            //转出账户的余额
            System.out.println(fromAccount.getName() + "->" + fromAccount.getBalance());
            //转入账户的余额
            System.out.println(toAccount.getName() + "->" + toAccount.getBalance());
        
    

    public static void main(String[] args) 
        Account fromAccount = new Account("tt", 100000);
        Account toAccount = new Account("ff", 300000);
        Thread a = new Thread(new TransferAccount(fromAccount, toAccount, 10));
        Thread b = new Thread(new TransferAccount(toAccount, fromAccount, 30));

        a.start();
        b.start();
    

3.除此之外,我们还可以破坏循环等待条件,每个都是按照顺序进行加锁等等。

以上是关于18.透彻理解死锁的主要内容,如果未能解决你的问题,请参考以下文章

高并发,你真的理解透彻了吗?

进程死锁及解决办法

java 死锁产生原因及解锁(转)

高并发,你真的理解透彻了吗?

高并发,你真的理解透彻了吗?

死锁处理策略和死锁预防