前面几篇文章,咱们一直在关注如何解决并发问题,也就是程序的原子性、可见性、有序性。这些问题一旦出现,程序的结果就无法保证。程序员
好在 Java 是一门强大的语言,锁-synchronized
是一味万能药。你只要用好锁,几乎能解决全部并发问题。编程
不过,并发编程有一个特色:解决完一个问题,总会冒出另外一个新问题。segmentfault
实际开发中,锁虽然是一副万能药,但使用起来要很是当心。由于你不但要考虑锁和资源的关系,还得考虑性能问题。多线程
咱们之因此写并发程序,不就是想提升性能吗?并发
然而,锁的本质是串行化,程序要排队轮流执行。这样一来,多线程的优点就无法发挥了,性能天然会降低。性能
好比,银行的转帐操做,若是想保证结果的正确,就得用到锁。优化
class Account { // 余额 private Integer balance; // 转帐 void transfer(Account target, Integer amt) { synchronized (Account.class) { if (this.balance > amt) { this.balance -= amt; target.balance += amt; } } } }
在这里,咱们对 Account.class
进行加锁,解决了银行转帐的并发问题,代码也特别简单,看似很完美。可是,这里有一个致命缺陷:性能太差,全部帐户的转帐操做都是串行的。this
在现实世界中,帐户 A 转帐户 B,帐户 C 转帐户 D,这些都是能够并行处理的。但在这个方案中,却没考虑这些,转帐只能一笔一笔的处理。spa
试想一下,中国网民即便天天只交易一次,就有 10 亿笔转帐,平均每秒转帐超过 1 万次。若是交易只能一笔笔处理,那结果是对了,性能却根本无法看。线程
所以,在实际工做中,咱们不光要考虑程序的结果对不对,还得考虑程序的性能好很差。
咱们曾提到过,若是想用好锁,那么锁要覆盖全部受保护的资源。否则的话,就无法发挥锁的互斥做用。PS.能够复习这篇文章:用锁的正确姿式
然而,若是锁覆盖的范围太大,程序的性能也会大幅降低。
好比,前面的转帐操做实在是牵连巨大,你再看一下代码:
class Account { // 余额 private Integer balance; // 转帐 void transfer(Account target, Integer amt) { synchronized (Account.class) { if (this.balance > amt) { this.balance -= amt; target.balance += amt; } } } }
每笔转帐只涉及了两个帐号,但咱们却把整个 Account.class
锁住了。这个方案虽然简单,但 Account.class
这个资源覆盖的范围实在太大了。
并且,帐户不止转帐一个功能,还有查余额、提现等等操做,可这些操做也得串行处理的,那性能天然好不了。
那这样行不行?既然问题是锁覆盖的范围太大,那我缩小覆盖范围,问题不就解决了吗?
很是正确,这样的锁叫:细粒度锁。
所谓细粒度锁,就是缩小资源的覆盖范围,而后用不一样的锁对资源作精细化管理,从而提升程序的并行度,以此来提高性能。
那按照这个思路,咱们来分析一下代码,转帐只涉及到两个帐户,分别是:this
、target
。既然如此,咱们就不用锁定整个 Account.class
,只须要锁定两个帐户,作两次加锁操做就行了。
首先,咱们尝试锁定转出帐户 this
;而后,再尝试锁定转入帐户 target
。只有两个帐户都锁定成功时,才能执行转帐操做。你能够看下面这副图:
思路有了,接下来,就得转换成代码了。
class Account { // 余额 private Integer balance; // 转帐 void transfer(Account target, Integer amt) { synchronized (this) { synchronized (target) { if (this.balance > amt) { this.balance -= amt; target.balance += amt; } } } } }
相比原来的代码,如今只是多用了一个 synchronized
,好像没有什么变化,但转帐的并行度却大大提升。
你想一下,如今同时出现两笔交易,帐户 A 转帐户 B,帐户 C 转帐户 D。
本来的转帐操做是锁定了整个 Account.class
,转帐只能一笔一笔地处理。
但通过改造后,第一笔转帐只锁定了 A、B 两个帐户,第二笔转帐只锁定了 C、D 两个帐户,这两笔转帐彻底能够并行处理。
这样一来,程序的性能提高了好几个档次,而这都是细粒度锁的功劳。
在转帐这个例子中,咱们一开始用是 Account.class
来作锁,但为了优化性能,咱们用了细粒度锁,只锁定和转帐相关的两个帐号。这样一来,性能有了很大的提高。
然而,天下没有免费的午饭。细粒度锁看上去这么简单,是否是也有代价呢?
没错,细粒度锁可能形成死锁。所谓死锁,就是两个以上的线程在执行的时候,由于竞争资源形成互相等待,从而进入“永久”阻塞的状态。
听起来有点复杂,咱们仍是继续看转帐的例子吧。
class Account { // 余额 private Integer balance; // 转帐 void transfer(Account target, Integer amt) { synchronized (this) { synchronized (target) { if (this.balance > amt) { this.balance -= amt; target.balance += amt; } } } } }
如今同时有两笔交易,帐户A 转 帐户B,帐户B 转 帐户A。这个就有了两个线程,分别是:线程1、线程二。
其中,线程一先锁定了帐户A,线程二先锁定了帐户B。
那么,问题来了。线程一想继续对帐户B 加锁,但发现帐户B 被锁定,转帐无法执行下去,只能进入等待。
一样的道理,线程二想继续对帐户A 加锁,但发现帐户A 被锁定,转帐也无法执行,也进入了等待。
这样一来,线程1、线程二都在死死地等着,这就是经典的死锁问题了,你能够看下这副图。
在这副图中,两个线程造成一个完美的闭环,根本无法出去。
并且,转帐随时都会发生,但这两个线程却一直占着资源,新的订单无法处理,只能堆在一块儿,愈来愈多。这不只浪费大量的计算机资源,还影响其它功能的运行。
此外,程序一旦发生死锁,那除了重启应用外,没有别的路能走。但银行分分钟进出几十亿,重启应用也是死路一条。
能够说,如何完全解决死锁问题,就是程序员价值所在。
锁的本质是串行化,若是锁覆盖的范围太大,会致使程序的性能低下。
为了提高性能,咱们用了细粒度锁,但这又带来了死锁问题。
既然如此,死锁该怎么解决?咱们下次再聊。