在 Java 中,锁好像是颗万能药,没什么问题是加锁解决不了的。的确,锁能解决绝大部分的并发问题。编程
然而,最简单的东西也每每最容易出现问题。你只要稍有不慎,不但 Bug 没有解决,还得花费大量的时间作各类排查。segmentfault
既然如此,咱们就来好好看看:为何用了锁,程序仍是出错了。并发
一说到锁,你的大脑中可能立马想起这个模型。性能
中间蓝色的一段代码,叫作:临界区。线程进入临界区前,先尝试加锁,若是成功,就进入临界区。此时,这个线程持有锁,等执行完临界区的代码后,持有锁的线程就会执行解锁。this
一样的道理,若是加锁失败,线程就会一直等待,直到持有锁的线程解锁后,再从新尝试加锁。spa
这是锁的简易模型,看起来简单明了,但这个模型是错的,它忽略了最最重要的一点:锁与资源的关系。线程
你还记得吗?锁是为了实现互斥,即:在同一时刻,一个资源只能由一个线程操做。3d
换句话说,之因此要用锁,就是要保护某些资源。所以,锁与资源之间的关系,是重点中的重点。不少并发 Bug 就是忽略了这一点致使的。好比,你看下面这段代码:code
class Account { private int balance; // 转帐 synchronized void transfer(Account target, int amt){ if (this.balance > amt) { this.balance -= amt; target.balance += amt; } } }
你可能会以为,这段代码没问题呀?transfer()
方法不是加锁了吗?但在并发环境下,转帐后,余额极可能对不上。缘由也很简单,没考虑锁和资源的关系。对象
既然如此,那正确的锁模型是怎样的呢?
首先,咱们要标注出受保护的资源 R;而后,为资源 R 建立一把锁 LR;最后,在进出临界区的时候,再加上加锁、解锁操做。
其中,锁 LR
和受保护资源 R
之间有一条关联线,这很是重要,表明的是:锁与资源之间,是有对应关系的。
打个比方,你家的锁只能保护你家的东西,我家的锁只能保护我家的东西。这个关系要搞清楚,否则,就是锁自家的门来保护他家的东西。
能够说,咱们平时对锁的理解都是错的。虽然这通常不会有什么问题,但只要出现问题,就是公司破产之类的大事。
好比,我前东家的支付系统,就是由于锁的问题,巨亏了几十万,负责人引咎辞职。在濒临倒闭之际,我接手了,这个项目才起死回生。
因此,若是你不想重蹈覆辙,而是想升职加薪。那必需要掌握正确的锁模型,千万不能忽略锁和资源的关系。
你已经知道了,想要用好用锁,关键是搞清楚锁和资源的关系。那么,这二者的关系是怎样的呢?
资源和锁之间的关系是 N:1。
换句话说,你能够用一把锁,来保护多个资源。可是,你不能用多把锁,来保护一个资源。缘由在于,若是你针对一个资源建立了多把锁,那么就达不到互斥的效果了。
打个比方,现实世界中,你能够用好几把锁,来保护你家的东西。但在编程世界中,你不能这样作。你看这个例子:
class SafeCalc { private long value = 0L; void addOne() { synchronized (this) { value += 1; } } void addTwo() { synchronized (SafeCalc.class) { value += 2; } } }
你看下 addOne()
和 addTwo()
两个方法,它们分别用了两把锁 this
和 SafeCalc.class
。虽然都是保护同一个资源,但临界区被拆成了 2 个,而这 2 个临界区是没有互斥关系的,这致使了并发问题。
那问题该怎么解决呢?
很简单,不要用多把锁来保护一个资源,你能够只用 this
,又或者只用 SafeCalc.class
。
固然,这个例子比较简单。在现实工做中,状况确定更加复杂,咱们要保护的资源不止一个,而是多个,这又该怎么处理呢?
不管是单个资源,仍是多个资源,关键都在于:锁要覆盖全部受保护的资源。
单个资源比较好理解,但若是要保护的资源不止一个,那咱们首先要作的是,区分这些资源是否有关联,这分为两种状况。
第一,如何保护没有关联的多个资源,这和处理单个资源差很少。若是不考虑性能,你能够用一把锁来保护;若是想提升性能,你能够用不一样的锁来保护。
你看下面的代码:
class Account { // 余额 private Integer balance; // 密码 private String password; // 锁:保护余额 private final Object balLock = new Object(); // 锁:保护密码 private final Object pwLock = new Object(); // 取款 void withdraw(Integer amt) { synchronized (balLock) { if (this.balance > amt) { this.balance -= amt; } } } // 更改密码 void updatePassword(String pw) { synchronized (pwLock) { this.password = pw; } } }
帐户类-Account
有两个方法:取款-withdraw()
、更改密码-updatePassword()
。从功能上看,这两个方法没什么关联。因此,为了提高性能,我用了两把锁,让它们各管各的。
固然,你也能够用一把锁来管理全部资源,就像下面这样:
class Account { // 余额 private Integer balance; // 密码 private String password; // 取款 synchronized void withdraw(Integer amt) { if (this.balance > amt) { this.balance -= amt; } } // 更改密码 synchronized void updatePassword(String pw) { this.password = pw; } }
在这里,我对性能没有要求,因此只要用 this
这一把锁,直接管理帐户的全部资源。
第二,如何保护有关联的多个资源,这个问题就有点复杂了。
好比,银行的转帐操做,不一样的帐户是有关联的,帐户 A 若是减小 100 元,帐户 B 就得增长 100 元。这时候,该怎么避免转帐的并发问题呢?
你可能想到加锁,用 synchronized
关键词修饰一下,就像下面这样:
class Account { // 余额 private Integer balance; // 转帐 synchronized void transfer(Account target, Integer amt) { if (this.balance > amt) { this.balance -= amt; target.balance += amt; } } }
一把锁能够保护多个资源,这看上去没问题。然而,倒是错误的作法。
你想象一下这样的场景,A、B、C 三个帐户的余额都是 100 元。这时候,有两笔转帐操做:帐户 A 转 100 元到帐户 B,帐户 B 转 100 元到帐户 C。照理说,结果应该是:帐户A-0元,帐户B-100元,帐户C-200元。
然而,若是是并发转帐,最终的结果还有两种可能。一种是:帐户A-0元,帐户B-200元,帐户C-200元;另外一种是:帐户A-0元,帐户B-0元,帐户C-200元。
简单来讲,帐户 B 的余额极可能是错的,问题就出在this
这把锁上。
还记得吗?锁要覆盖全部受保护的资源。
在这个例子中,却没能作到这一点。临界区内有两个资源:this.balance
、target.balance
。但this
这把锁只能保护this.balance
,不能保护target.balance
。
打个比方,你家的锁就算再厉害,也无法保护邻居家的东西吧?
所以,咱们要找到一把锁,同时覆盖全部帐号。
方案仍是挺多的,最简单的一个就是:Account.class
。Account.class
共享给全部 Account 对象。并且,Account.class
由 Java 虚拟机建立,具备惟一性。
这样一来,转帐的并发问题就解决了,代码也特别简单。
class Account { // 余额 private Integer balance; // 转帐 void transfer(Account target, Integer amt) { synchronized (Account.class) { if (this.balance > amt) { this.balance -= amt; target.balance += amt; } } } }
在 Java 中,锁是解决并发的万能药,但咱们却每每用很差,这是由于咱们忽略了锁与资源的关系。
通常状况下,这不会有什么大问题。
然而,一旦出现多个资源,这些资源还相互关联,就极可能出现公司破产之类的大事。所以,你要时刻记住 3 点:
你只要检查好这三点,坏事就轮不到你头上。