最新公告
  • 欢迎您光临悠哉网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入我们
  • 高并发环境下诡异的加锁问题(你加的锁未必安全)

    作者个人研发的在高并发场景下,提供的简单、稳定、可扩展的延迟消息队列框架,具有精准的定时任务和延迟队列处理功能。自开源半年多以来,已成功为十几家中小型企业提供了精准定时调度方案,经受住了生产环境的考验。为使更多童鞋受益,现给出开源框架地址:https://github.com/sunshinelyz/mykit-delay。

    高并发环境下诡异的加锁问题(你加的锁未必安全)

    声明

    特此声明:文中有关支付宝账户的说明,只是用来举例,实际支付宝账户要比文中描述的复杂的多。也与文中描述的完全不同。

    前言

    很多网友留言说:在编写多线程并发程序时,我明明对共享资源加锁了啊?为什么还是出问题呢?问题到底出在哪里呢?其实,我想说的是:你的加锁姿势正确吗?你真的会使用锁吗?错误的加锁方式不但不能解决并发问题,而且还会带来各种诡异的Bug问题,有时难以复现!

    我们知道在并发编程中,不能使用多把锁保护同一个资源,因为这样达不到线程互斥的效果,存在线程安全的问题。相反,却可以使用同一把锁保护多个资源。那么,如何使用同一把锁保护多个资源呢?又如何判断我们对程序加的锁到底是不是安全的呢?我们就一起来深入探讨这些问题!

    分析场景

    我们在分析多线程中如何使用同一把锁保护多个资源时,可以将其结合具体的业务场景来看,比如:需要保护的多个资源之间有没有直接的业务关系。如果需要保护的资源之间没有直接的业务关系,那么如何对其加锁;如果有直接的业务关系,那么如何对其加锁?接下来,我们就顺着这两个方向进行深入说明。

    没有直接业务关系的场景

    例如,我们的支付宝账户,有针对余额的付款操作,也有针对账户密码的修改操作。本质上,这两种操作之间没有直接的业务关系,此时,我们可以为账户的余额和账户密码分配不同的锁来解决并发问题。

    例如,在支付宝账户AlipayAccount类中,有两个成员变量,分别是账户的余额balance和账户的密码password。付款操作的pay()方法和查看余额操作的getBalance()方法会访问账户中的成员变量balance,对此,我们可以创建一个balanceLock锁对象来保护balance资源;另外,更改密码操作的updatePassword()方法和查看密码的getPassowrd()方法会访问账户中的成员变量password,对此,我们可以创建一个passwordLock锁对象来保护password资源

    具体的代码如下所示。

    1. public class AlipayAccount{ 
    2.     //保护balance资源的锁对象 
    3.     private final Object balanceLock = new Object(); 
    4.     //保护password资源的锁对象 
    5.     private final Object passwordLock = new Object(); 
    6.     //账户余额 
    7.     private Integer balance; 
    8.     //账户的密码 
    9.     private String password
    10.  
    11.     //支付方法 
    12.     public void pay(Integer money){ 
    13.         synchronized(balanceLock){ 
    14.             if(this.balance >= money){ 
    15.                 this.balance -= money; 
    16.             } 
    17.         } 
    18.     } 
    19.     //查看账户中的余额 
    20.     public Integer getBalance(){ 
    21.         synchronized(balanceLock){ 
    22.             return this.balance; 
    23.         } 
    24.     } 
    25.  
    26.     //修改账户的密码 
    27.     public void updatePassword(String password){ 
    28.         synchronized(passwordLock){ 
    29.             this.password = password
    30.         } 
    31.     } 
    32.  
    33.     //查看账户的密码 
    34.     public String getPassword(){ 
    35.         synchronized(passwordLock){ 
    36.             return this.password
    37.         } 
    38.     } 

    这里,我们也可以使用一把互斥锁来保护balance资源和password资源,例如都使用balanceLock锁对象,也可以都使用passwordLock锁对象,甚至也都可以使用this对象或者干脆每个方法前加一个synchronized关键字。

    但是,如果都使用同一个锁对象的话,那么,程序的性能就太差了。会导致没有直接业务关系的各种操作都串行执行,这就违背了我们并发编程的初衷。实际上,我们使用两个锁对象分别保护balance资源和password资源,付款和修改账户密码是可以并行的。

    存在直接业务关系的场景

    例如,我们使用支付宝进行转账操作。假设账户A给账户B转账100,A账户减少100元,B账户增加100元。两个账户在业务中有直接的业务关系。例如,下面的TansferAccount类,有一个成员变量balance和一个转账的方法transfer(),代码如下所示。

    1. public class TansferAccount{ 
    2.     private Integer balance; 
    3.     public void transfer(TansferAccount target, Integer transferMoney){ 
    4.         if(this.balance >= transferMoney){ 
    5.             this.balance -= transferMoney; 
    6.             target.balance += transferMoney; 
    7.         } 
    8.     } 

    在上面的代码中,如何保证转账操作不会出现并发问题呢?很多时候我们的第一反应就是给transfer()方法加锁,如下代码所示。

    1. public class TansferAccount{ 
    2.     private Integer balance; 
    3.     public synchronized void transfer(TansferAccount target, Integer transferMoney){ 
    4.         if(this.balance >= transferMoney){ 
    5.             this.balance -= transferMoney; 
    6.             target.balance += transferMoney; 
    7.         } 
    8.     } 

    我们仔细分析下,上面的代码真的是安全的吗?!其实,在这段代码中,synchronized临界区中存在两个不同的资源,分别是转出账户的余额this.balance和转入账户的余额target.balance,这里只用到了一把锁synchronized(this)。说到这里,大家有没有一种豁然开朗的感觉。没错,问题就出现在synchronized(this)这把锁上,这把锁只能保护this.balance资源,而无法保护target.balance资源

    我们可以使用下图来表示这个逻辑。

    高并发环境下诡异的加锁问题(你加的锁未必安全)

    从上图我们也可以发现,this锁对象只能保护this.balance资源,而不能保护target.balance资源

    接下来,我们再看一个场景:假设存在A、B、C三个账户,余额都是200,此时我们使用两个线程分别执行两个转账操作:账户A给账户B转账100,账户B给账户C转账100。理论上,账户A的余额为100,账户B的余额为200,账户C的余额为300。

    真的是这样吗?我们假设线程A和线程B同时在两个不同的CPU上执行,线程A执行账户A给账户B转账100的操作,线程B执行账户B给账户C转账100的操作。两个线程之间是互斥的吗?显然不是,按照TansferAccount的代码来看,线程A锁定的是账户A的实例,线程B锁定的是账户B的实例。所以,线程A和线程B能够同时进入transfer()方法。此时,线程A和线程B都能够读取到账户B的余额为200。两个线程都完成转账操作后,B的账户余额可能为300,也可能为100,但是不可能为200。

    这是为什么呢?线程A和线程B同时读取到账户B的余额为200,如果线程A的转账操作晚于线程B的转账操作对balance的写入,则账户B的余额为300;如果线程A的转账操作早于线程B的转账操作对balance的写入,则账户B的余额为100。无论如何账户B的余额都不会是200。

    综上所示,TansferAccount的代码根本无法解决并发问题!

    正确的加锁

    如果我们希望对转账操作中涉及的多个资源加锁,那我们的锁就必须要覆盖所有需要保护的资源。

    在前面的TansferAccount类中,this是对象级别的锁,这就导致了线程A和线程B执行过程中所获取到的锁是不同的,那么如何让两个线程共享同一把锁呢?!

    其中,方案有很多,一种简单的方式,就是在TansferAccount类的构造方法中传入一个balanceLock锁对象,以后在创建TansferAccount类对象的时候,每次传入相同的balanceLock锁对象,并在transfer方法中使用balanceLock锁对象加锁即可。这样,所有创建的TansferAccount类对象就会共享balanceLock锁。代码如下所示。

    1. public class TansferAccount{ 
    2.     private Integer balance; 
    3.     private Object balanceLock; 
    4.     private TansferAccount(){} 
    5.     public TansferAccount(Object balanceLock){ 
    6.         this.balanceLock = balanceLock; 
    7.     } 
    8.     public void transfer(TansferAccount target, Integer transferMoney){ 
    9.         synchronized(this.balanceLock){ 
    10.              if(this.balance >= transferMoney){ 
    11.                 this.balance -= transferMoney; 
    12.                 target.balance += transferMoney; 
    13.             }    
    14.         } 
    15.     } 

    那么,问题又来了:这样解决问题真的完美吗?!

    上述代码虽然解决了转账操作的并发问题,但是它真的就完美了吗?!仔细分析后,我们发现,并不是想象中的那么完美。因为它要求创建TansferAccount对象的时候,必须传入同一个balanceLock对象,如果传入的不是同一个balanceLock对象,就不能保证并发带来的线程安全问题了!在实际的项目中,创建TansferAccount对象的操作可能被分散在多个不同的项目工程中,这样很难保证传入的balanceLock对象是同一个对象。

    所以,在创建TansferAccount对象时传入同一个balanceLock锁对象的方案,虽然能够解决转账的并发问题,但是却无法在实际项目中被有效的采用!

    还有没有其他的方案呢?答案是有!别忘了JVM在加锁类的时候,会为类创建一个Class对象,而这个Class对象对于类的实例对象来说是共享的,也就是说,无论创建多少个类的实例对象,这个Class对象都是同一个,这是由JVM来保证的。

    高并发环境下诡异的加锁问题(你加的锁未必安全)

    说到这里,我们就能够想到使用如下方式对转账操作加锁。

    1. public class TansferAccount{ 
    2.     private Integer balance; 
    3.     public void transfer(TansferAccount target, Integer transferMoney){ 
    4.         synchronized(TansferAccount.class){ 
    5.             if(this.balance >= transferMoney){ 
    6.                 this.balance -= transferMoney; 
    7.                 target.balance += transferMoney; 
    8.             }    
    9.         } 
    10.     } 

    我们可以使用下图表示这个逻辑。

    高并发环境下诡异的加锁问题(你加的锁未必安全)

    这样,无论创建多少个TansferAccount对象,都会共享同一把锁,解决了转账的并发问题。

    写在最后

    如果觉得文章对你有点帮助,请微信搜索并关注「 冰河技术 」微信公众号,跟冰河学习高并发编程技术。

    最后,附上并发编程需要掌握的核心技能知识图,祝大家在学习并发编程时,少走弯路。

    高并发环境下诡异的加锁问题(你加的锁未必安全)

    后记

    记住:你比别人强的地方,不是你做过多少年的CRUD工作,而是你比别人掌握了更多深入的技能。不要总停留在CRUD的表面工作,理解并掌握底层原理并熟悉源码实现,并形成自己的抽象思维能力,做到灵活运用,才是你突破瓶颈,脱颖而出的重要方向!

    你在刷抖音,玩游戏的时候,别人都在这里学习,成长,提升,人与人最大的差距其实就是思维。你可能不信,优秀的人,总是在一起。。

    本文转载自微信公众号「冰河技术」,可以通过以下二维码关注。转载本文请联系冰河技术公众号。

    高并发环境下诡异的加锁问题(你加的锁未必安全)

    1. 本站所有资源来源于用户上传和网络,如有侵权请邮件联系站长!
    2. 分享目的仅供大家学习和交流,您必须在下载后24小时内删除!
    3. 不得使用于非法商业用途,不得违反国家法律。否则后果自负!
    4. 本站提供的源码、模板、插件等等其他资源,都不包含技术服务请大家谅解!
    5. 如有链接无法下载、失效或广告,请联系管理员处理!
    6. 本站资源售价只是赞助,收取费用仅维持本站的日常运营所需!
    7. 如遇到加密压缩包,默认解压密码为"www.yoozai.net",如遇到无法解压的请联系管理员!
    悠哉网 » 高并发环境下诡异的加锁问题(你加的锁未必安全)

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    悠哉网 WWW.YOOZAI.NET
    悠哉网,用户消费首选的网站,喜欢你就悠哉一下。

    发表评论

    • 1072会员总数(位)
    • 40643资源总数(个)
    • 0本周发布(个)
    • 0 今日发布(个)
    • 489稳定运行(天)

    提供最优质的资源集合

    立即查看 了解详情