当前位置:首页 > > 架构师社区
[导读]来自:冰河技术      声明 特此声明:文中有关支付宝账户的说明,只是用来举例,实际支付宝账户要比文中描述的复杂的多。也与文中描述的完全不同。 前言 很多网友留言说:在编写多线程并发程序时,我明明对共享资源加锁了啊?为什么还是出问题呢?问题到底出

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

来自:冰河技术     

声明

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

前言

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

在之前的《【高并发】如何使用互斥锁解决多线程的原子性问题?这次终于明白了!》一文中,我们知道在并发编程中,不能使用多把锁保护同一个资源,因为这样达不到线程互斥的效果,存在线程安全的问题。相反,却可以使用同一把锁保护多个资源。那么,如何使用同一把锁保护多个资源呢?又如何判断我们对程序加的锁到底是不是安全的呢?我们就一起来深入探讨这些问题!

分析场景

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

没有直接业务关系的场景

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

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

具体的代码如下所示。

public class AlipayAccount{
    //保护balance资源的锁对象
    private final Object balanceLock = new Object();
    //保护password资源的锁对象
    private final Object passwordLock = new Object();
    //账户余额
    private Integer balance;
    //账户的密码
    private String password;

    //支付方法
    public void pay(Integer money){
        synchronized(balanceLock){
            if(this.balance >= money){
                this.balance -= money;
            }
        }
    }
    //查看账户中的余额
    public Integer getBalance(){
        synchronized(balanceLock){
            return this.balance;
        }
    }

    //修改账户的密码
    public void updatePassword(String password){
        synchronized(passwordLock){
            this.password = password;
        }
    }

    //查看账户的密码
    public String getPassword(){
        synchronized(passwordLock){
            return this.password;
        }
    }
}

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

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

存在直接业务关系的场景

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

public class TansferAccount{
    private Integer balance;
    public void transfer(TansferAccount target, Integer transferMoney){
        if(this.balance >= transferMoney){
            this.balance -= transferMoney;
            target.balance += transferMoney;
        }
    }
}

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

public class TansferAccount{
    private Integer balance;
    public synchronized void transfer(TansferAccount target, Integer transferMoney){
        if(this.balance >= transferMoney){
            this.balance -= transferMoney;
            target.balance += transferMoney;
        }
    }
}

我们仔细分析下,上面的代码真的是安全的吗?!其实,在这段代码中,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锁。代码如下所示。

public class TansferAccount{
    private Integer balance;
    private Object balanceLock;
    private TansferAccount(){}
    public TansferAccount(Object balanceLock){
        this.balanceLock = balanceLock;
    }
    public void transfer(TansferAccount target, Integer transferMoney){
        synchronized(this.balanceLock){
             if(this.balance >= transferMoney){
                this.balance -= transferMoney;
                target.balance += transferMoney;
            }   
        }
    }
}

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

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

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

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

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

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

public class TansferAccount{
    private Integer balance;
    public void transfer(TansferAccount target, Integer transferMoney){
        synchronized(TansferAccount.class){
            if(this.balance >= transferMoney){
                this.balance -= transferMoney;
                target.balance += transferMoney;
            }   
        }
    }
}

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

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

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

写在最后

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

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

后记:

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

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

特别推荐一个分享架构+算法的优质内容,还没关注的小伙伴,可以长按关注一下:

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

长按订阅更多精彩▼

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

如有收获,点个在看,诚挚感谢

免责声明:本文内容由21ic获得授权后发布,版权归原作者所有,本平台仅提供信息存储服务。文章仅代表作者个人观点,不代表本平台立场,如有问题,请联系我们,谢谢!

本站声明: 本文章由作者或相关机构授权发布,目的在于传递更多信息,并不代表本站赞同其观点,本站亦不保证或承诺内容真实性等。需要转载请联系该专栏作者,如若文章内容侵犯您的权益,请及时联系本站删除。
换一批
延伸阅读

LED驱动电源的输入包括高压工频交流(即市电)、低压直流、高压直流、低压高频交流(如电子变压器的输出)等。

关键字: 驱动电源

在工业自动化蓬勃发展的当下,工业电机作为核心动力设备,其驱动电源的性能直接关系到整个系统的稳定性和可靠性。其中,反电动势抑制与过流保护是驱动电源设计中至关重要的两个环节,集成化方案的设计成为提升电机驱动性能的关键。

关键字: 工业电机 驱动电源

LED 驱动电源作为 LED 照明系统的 “心脏”,其稳定性直接决定了整个照明设备的使用寿命。然而,在实际应用中,LED 驱动电源易损坏的问题却十分常见,不仅增加了维护成本,还影响了用户体验。要解决这一问题,需从设计、生...

关键字: 驱动电源 照明系统 散热

根据LED驱动电源的公式,电感内电流波动大小和电感值成反比,输出纹波和输出电容值成反比。所以加大电感值和输出电容值可以减小纹波。

关键字: LED 设计 驱动电源

电动汽车(EV)作为新能源汽车的重要代表,正逐渐成为全球汽车产业的重要发展方向。电动汽车的核心技术之一是电机驱动控制系统,而绝缘栅双极型晶体管(IGBT)作为电机驱动系统中的关键元件,其性能直接影响到电动汽车的动力性能和...

关键字: 电动汽车 新能源 驱动电源

在现代城市建设中,街道及停车场照明作为基础设施的重要组成部分,其质量和效率直接关系到城市的公共安全、居民生活质量和能源利用效率。随着科技的进步,高亮度白光发光二极管(LED)因其独特的优势逐渐取代传统光源,成为大功率区域...

关键字: 发光二极管 驱动电源 LED

LED通用照明设计工程师会遇到许多挑战,如功率密度、功率因数校正(PFC)、空间受限和可靠性等。

关键字: LED 驱动电源 功率因数校正

在LED照明技术日益普及的今天,LED驱动电源的电磁干扰(EMI)问题成为了一个不可忽视的挑战。电磁干扰不仅会影响LED灯具的正常工作,还可能对周围电子设备造成不利影响,甚至引发系统故障。因此,采取有效的硬件措施来解决L...

关键字: LED照明技术 电磁干扰 驱动电源

开关电源具有效率高的特性,而且开关电源的变压器体积比串联稳压型电源的要小得多,电源电路比较整洁,整机重量也有所下降,所以,现在的LED驱动电源

关键字: LED 驱动电源 开关电源

LED驱动电源是把电源供应转换为特定的电压电流以驱动LED发光的电压转换器,通常情况下:LED驱动电源的输入包括高压工频交流(即市电)、低压直流、高压直流、低压高频交流(如电子变压器的输出)等。

关键字: LED 隧道灯 驱动电源
关闭