特此声明:文中有关支付宝帐户的说明,只是用来举例,实际支付宝帐户要比文中描述的复杂的多。也与文中描述的彻底不一样。java
不少网友留言说:在编写多线程并发程序时,我明明对共享资源加锁了啊?为何仍是出问题呢?问题到底出在哪里呢?其实,我想说的是:你的加锁姿式正确吗?你真的会使用锁吗?错误的加锁方式不但不能解决并发问题,并且还会带来各类诡异的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(),代码以下所示。this
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对象,都会共享同一把锁,解决了转帐的并发问题。
若是以为文章对你有点帮助,请微信搜索并关注「 冰河技术 」微信公众号,跟冰河学习高并发编程技术。
最后,附上并发编程须要掌握的核心技能知识图,祝你们在学习并发编程时,少走弯路。