一个简单的转账场景示例带你了解并发安全?

这篇具有很好参考价值的文章主要介绍了一个简单的转账场景示例带你了解并发安全?。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

本文转帐场景主要参考来自于极客时间 王老师的 《Java 并发编程实战》

一个简单的转账场景示例带你了解并发安全?

例如如银行业务里面的转账操作,账户 A 减少 100 元,账户 B 增加 100 元。
我们声明了个账户类:Account,该类有一个成员变量余额:balance,还有一个用于转账的方法:transfer(),然后怎么保证转账操作 transfer() 没有并发问题呢?


示例代码如下:

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

首先直觉告诉我们,有线程安全问题那就用 synchronized 关键字修饰一下 transfer() 方法不就可以了,如下所示。

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

在这段代码中,问题出在哪里呢?问题就出在 this 这把锁上,this 这把锁可以保护自己的余额 this.balance,却保护不了别人的余额 target.balance。
一个简单的转账场景示例带你了解并发安全?,后端工程师,并发编程具体可以分析一下,假设有 A、B、C 三个账户,余额都是 200 元,我们用两个线程分别执行两个转账操作:账户 A 转给账户 B 100 元,账户 B 转给账户 C 100 元,最后我们期望的结果应该是账户 A 的余额是 100 元,账户 B 的余额是 200 元, 账户 C 的余额是 300 元。

实际多线程执行结果可能为最终账户 B 的余额可能是 300,可能是 100,自行分析或验证。

使用锁的正确姿势

this 是对象级别的锁,所以 A 对象和 B 对象都有自己的锁,我们只要让 A 对象和 B 对象共享一把锁,那就能解决并发安全问题。
我们于是可以用 Account.class 作为共享的锁。这个对象是 Java 虚拟机在加载 Account 类的时候创建的,所以我们不用担心它的唯一性。使用 Account.class 作为共享的锁,代码修正示例如下:

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

我们用 Account.class 作为互斥锁,来解决银行业务里面的转账问题,虽然这个方案不存在并发问题,但是所有账户的转账操作都是串行的,例如账户 A 转账户 B、账户 C 转账户 D 这两个转账操作现实世界里是可以并行的,但是在这个方案里却被串行化了,这样的话,性能太差。

向现实世界要答案

现实世界里,账户转账操作是支持并发的,而且绝对是真正的并行,我们试想在古代,没有信息化,账户的存在形式真的就是一个账本,而且每个账户都有一个账本,这些账本都统一存放在文件架上。银行柜员在给我们做转账时,要去文件架上把转出账本和转入账本都拿到手,然后做转账。

上面这个过程在编程的世界里怎么实现呢?其实用两把锁就实现了,转出账本一把,转入账本另一把。在 transfer() 方法内部,我们首先尝试锁定转出账户 this(先把转出账本拿到手),然后尝试锁定转入账户 target(再把转入账本拿到手),只有当两者都成功时,才执行转账操作。

一个简单的转账场景示例带你了解并发安全?,后端工程师,并发编程

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;
        }
      }
    }
  } 
}

我们知道,使用细粒度锁可以提高并行度,但是也可能会导致死锁
一个简单的转账场景示例带你了解并发安全?,后端工程师,并发编程

如何预防死锁?

并发程序一旦死锁,一般没有特别好的方法,很多时候我们只能重启应用。因此,解决死锁问题最好的办法还是规避死锁。
要避免死锁就需要分析死锁发生的条件,有个叫 Coffman 的牛人早就总结过了,只有以下这四个条件都发生时才会出现死锁:

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

所以,我们只要破坏其中一个条件,就能成功避免死锁的发生。
其中,互斥这个条件我们没有办法破坏,因为我们用锁为的就是互斥。不过其他三个条件都是有办法破坏掉的,到底如何做呢?

破坏不可抢占条件

java.util.concurrent 这个包下面提供的 Lock 是可以轻松解决这个问题的。

破坏循环等待条件

破坏这个条件,需要对资源进行排序,然后按序申请资源。这个实现非常简单,我们假设每个账户都有不同的属性 id,这个 id 可以作为排序字段,申请的时候,我们可以按照从小到大的顺序来申请。示例代码如下:

class Account {
  private int id;
  private int balance;
  // 转账
  void transfer(Account target, int amt){
    Account left = thisAccount 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;
        }
      }
    }
  } 
}
破坏占用且等待条件

从理论上讲,要破坏这个条件,可以一次性申请所有资源。示例代码如下:

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)
    }
  } 
}

上面用死循环的方式实现等待有什么问题呢?

如果 apply() 操作耗时非常短,而且并发冲突量也不大时,这个方案还挺不错的,因为这种场景下,循环上几次或者几十次就能一次性获取转出账户和转入账户了。但是如果 apply() 操作耗时长,或者并发冲突量大的时候,循环等待这种方案就不适用了,因为在这种场景下,可能要循环上万次才能获取到锁,太消耗 CPU 了。

其实在这种场景下,最好的方案应该是:如果线程要求的条件不满足,则线程阻塞自己,进入等待状态;当线程要求的条件满足后,通知等待的线程重新执行。其中,使用线程阻塞的方式就能避免循环等待消耗 CPU 的问题。所以我们可以用等待通知机制来优化此流程,示例代码如下:

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();
  }
}
  • notify() 是会随机地通知等待队列中的一个线程;
  • notifyAll() 会通知等待队列中的所有线程,推荐尽量使用 notifyAll()

并发编程全景图

个人总结及归纳的思维导图,供大家参考。
一个简单的转账场景示例带你了解并发安全?,后端工程师,并发编程文章来源地址https://www.toymoban.com/news/detail-813743.html

到了这里,关于一个简单的转账场景示例带你了解并发安全?的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 【网络安全】带你了解【黑客】

         Yan-英杰的主页 悟已往之不谏 知来者之可追    C++程序员,2024届电子信息研究生 目录 引言 1. 定义 2. 分类         a. 白帽黑客(White Hat Hacker)         b. 黑帽黑客(Black Hat Hacker)         c. 灰帽黑客(Gray Hat Hacker) 3. 黑客文化 4. 伦理问题 5.黑客常用的攻

    2024年02月13日
    浏览(29)
  • 【Spark】一个例子带你了解Spark运算流程

    写在前面:博主是一只经过实战开发历练后投身培训事业的“小山猪”,昵称取自动画片《狮子王》中的“彭彭”,总是以乐观、积极的心态对待周边的事物。本人的技术路线从Java全栈工程师一路奔向大数据开发、数据挖掘领域,如今终有小成,愿将昔日所获与大家交流一二

    2024年02月11日
    浏览(33)
  • 一个案例带你了解独立式键盘设计原理

    单片机与4个独立按键S1~S4以及8只LED指示灯构成一个独立式键盘系统。4个按键接在P1.0~P1.3引脚,P3口接8只LED指示灯,控制8只LED指示灯的亮和灭,原理图如下。当按下S1按键时,P3口的8只LED指示灯正向流水点亮;当按下S2按键时,P3口的8只LED指示灯反向流水点亮;当按下S3按键时

    2023年04月13日
    浏览(23)
  • 【Java递归】一篇文章带你了解,什么是递归 ,递归的特点,递归应用场景,递归练习题

    博主: 東方幻想郷 专栏分类: Java | 从入门到入坟 🌟递归是一种在方法通过 调用自身 来解决某些问题的技术,它可以将一些问题,分为更小,更细类似的子问题,逐步解决, 直到问题被简化到某个基本情况 ,最后可以直接拿到答案。 递归是一种函数调用自身的方法 递归

    2024年02月06日
    浏览(39)
  • 【网络安全】带你了解什么是【黑客】

    随着科技的迅猛发展和互联网的普及,黑客一词也逐渐进入了我们的日常生活。然而,黑客并不仅仅是一个贬义词,它有着复杂而多样的内涵。本文将从多个角度对黑客进行探讨,以帮助读者更好地了解黑客文化的本质。 黑客(Hacker)是指那些具备计算机技术专业知识,并能

    2024年02月16日
    浏览(33)
  • 做为一个产品经理带你了解--Axure交互和情境

                           📚📚 🏅我是bing人,一个在CSDN分享笔记的博主。📚📚                                                                  🌟在这里,我要推荐给大家我的专栏《Axure》。🎯🎯 🚀无论你是编程小白,还是有一定基础的程序员,这个专栏都

    2024年02月04日
    浏览(41)
  • 带你了解LVGL:一个开源的嵌入式图形库

    嵌入式系统是一种将计算机硬件和软件集成在一个特定的应用中的系统,例如智能手机、智能手表、汽车仪表盘等。嵌入式系统通常需要与用户进行交互,因此需要一个友好和易用的图形用户界面(GUI)。然而,开发一个高质量的GUI并不容易,因为嵌入式系统通常有限的资源

    2024年02月09日
    浏览(50)
  • 不知道该学那一个语言?一文带你了解三门语言

    名字:阿玥的小东东 学习:Python。正在学习c++ 主页:阿玥的小东东 目录 粉丝留言,回答问题 1.首先,初步了解 

    2024年02月21日
    浏览(35)
  • 一文带你了解注册信息安全专业人员CISP

    CISP即\\\"注册信息安全专业人员\\\",系国家对信息安全人员资质的最高认可。英文为Certified Information Security Professional (简称CISP),CISP系经中国信息安全测评中心实施国家认证。 CISP证书涵盖方向: “注册信息安全工程师”,英文为Certified Information Security Engineer,简称 CISE。证书持

    2024年02月02日
    浏览(32)
  • 【示例】MySQL-事务控制示例:账户转账-savepoint关键字

    本文讲述MySQL中的事务,以账户转账为例,体会事务的概念,并讲解事务相关的一个用法:savepoint 所有SQL正常执行,没有出错。结果就是:张三账户余额-1000;李四账户余额+1000 只有前两个SQL执行了,第三个SQL没有执行,出现数据不一致了:张三的钱减少了,但是李四

    2024年04月13日
    浏览(24)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包