美文网首页
并发场景下死锁

并发场景下死锁

作者: Easy的幸福 | 来源:发表于2019-10-15 16:09 被阅读0次
案例场景

例如账户A 转账户B、账户C 转账户D这两个转账操作。

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

这种方式采用了细粒度锁。使用细粒度锁可以提高并行度,是性能优化的一个重要手段。

但是会出现一个新的问题就是死锁。

上面转账的代码是怎么发生死锁的呢?我们假设线程T1执行账户A转账户B的操作,账户A.transfer(账户B);同时线程T2执行账户B转账户A的操作,账户B.transfer(账户A)。当T1和T2同时执行完①处的代码时,T1获得了账户A的锁(对于T1,this是账户A),而T2获得了账户B的锁(对于T2,this是账户B)。之后T1和T2在执行②处的代码时,T1试图获取账户B的锁时,发现账户B已经被锁定(被T2锁定),所以T1开始等待;T2则试图获取账户A的锁时,发现账户A已经被锁定(被T1锁定),所以T2也开始等待。于是T1和T2会无期限地等待下去,也就是我们所说的死锁了。

如何预防死锁

产生死锁的四个必要条件:

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

其中,互斥这个条件我们没有办法破坏,因为我们用锁为的就是互斥。不过其他三个条件都是有办法破坏掉的,到底如何做呢?

  • 破坏占用且等待条件

可以一次性申请所有资源,新增一个账本管理员的概念,两个资源只能同时给一个线程。这个管理员只能是一个单例,因为只有一个管理员。

class Allocator {
  private List<Object> als =
    new ArrayList<>();
  // 一次性申请所有资源
  synchronized boolean apply(
    Object from, Object to){
    if(als.contains(from) ||
         als.contains(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))
      ;
    try{
      // 锁定转出账户
      synchronized(this){              
        // 锁定转入账户
        synchronized(target){           
          if (this.balance > amt){
            this.balance -= amt;
            target.balance += amt;
          }
        }
      }
    } finally {
      actr.free(this, target)
    }
  } 
}
  • 破坏不可抢占条件

核心是要能够主动释放它占有的资源,这一点synchronized是做不到的。原因是synchronized申请资源的时候,如果申请不到,线程直接进入阻塞状态了,而线程进入阻塞状态,啥都干不了,也释放不了线程已经占有的资源。

lock可以后续补充。

  • 破坏循环等待条件
用“等待-通知”机制优化循环等待 image

为什么说是曾经满足过呢?因为notify()只能保证在通知时间点,条件是满足的。而被通知线程的执行时间点和通知的时间点基本上不会重合,所以当线程执行的时候,很可能条件已经不满足了(保不齐有其他线程插队)。被通知的线程要想重新执行,仍然需要获取到互斥锁(因为曾经获取的锁在调用wait()时已经释放了)

notify()是会随机地通知等待队列中的一个线程,而notifyAll()会通知等待队列中的所有线程。

class Allocator {
  private List<Object> als;
  // 一次性申请所有资源
  synchronized void apply(
    Object from, Object to){
    // 经典写法
    while(als.contains(from) ||
         als.contains(to)){
      try{
        wait();
      }catch(Exception e){
      }   
    } 
    als.add(from);
    als.add(to);  
  }
  // 归还资源
  synchronized void free(
    Object from, Object to){
    als.remove(from);
    als.remove(to);
    notifyAll();
  }
}

相关文章

  • 并发场景下死锁

    案例场景 例如账户A 转账户B、账户C 转账户D这两个转账操作。 这种方式采用了细粒度锁。使用细粒度锁可以提高并行...

  • Java Concurrent 死锁

    前言 死锁是一个比较大的概念,在并发场景下的加锁行为都有可能产生死锁问题。在Java 并发编程中会有死锁,操作系统...

  • 数据库主键更新死锁问题

    记一次压测数据死锁问题:并发场景为秒杀减库存的场景,使用Jmeter并发调用秒杀接口,秒杀使用数据库乐观锁,主键更...

  • MySQL InnoDB锁机制

    在我们的日常工作中,经常会遇到各种死锁的场景,有的死锁分析起来是比较容易的,比如同类型的事务并发引起的死锁;而不同...

  • MySQL死锁分析

    死锁场景1:并发插入重复key 场景重现 表结构如下: 三个session按顺序执行下面的操作 Session 1...

  • Java 并发编程中的死锁 ( Kotlin 语言讲解)

    什么是死锁? 在操作系统中的并发处理场景中, 进程对资源的持有与请求过程中,会产生死锁.Say, Process ...

  • 操作系统(二)进程管理 2.4 死锁

    2.4 死锁 2.4.1 死锁的概念 2.4.1.1 死锁的定义 在并发环境下,各进程因竞争资源而造成的一种互相等...

  • Java高并发 -- 并发扩展

    Java高并发 -- 并发扩展 主要是学习慕课网实战视频《Java并发编程入门与高并发面试》的笔记 死锁 死锁是指...

  • GCD 死锁

    GCD死锁 同步 异步 串行 并发

  • 2020-05-19-数据库死锁问题2

    很烦,又一次遇到数据库死锁,不过有了前车之鉴,这次很容易就发现了死锁的原因。 业务场景:批量并发操作(约1000w...

网友评论

      本文标题:并发场景下死锁

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