2 锁和被保护的对象是不是同一层面
梳理锁和要保护的对象是否是同一层面的。
案例
- 累加counter
- 测试
- 因为传参运行100万次,所以执行后应该输出100万,但输出:
- why?
在非静态的wrong方法上加锁,只能确保多线程无法执行同一实例的wrong,无法保证不执行不同实例的wrong。静态counter在多实例是共享的,所以会出现线程安全问题。
解决方案
在类中定义一个Object类型的静态字段,在操作counter之前对该字段加锁。
评论里肯定又有人会说:就这?直接把wrong定义为静态不就行?锁不就是类级别的了?
是可以,但不可能为解决线程安全改变代码结构,随便把实例方法改为静态方法。
3 加锁前考虑锁粒度和业务场景
方法上加synchronized
加锁是简单,但也不能在业务代码中滥用:
- 没必要
绝大多数业务代码是MVC三层架构,数据经过无状态的Controller=>Service=>Repository=>DB
没必要使用synchronized
保护什么数据。所以这也是为何很多同学笑评面试造火箭,工作拧螺丝~ - 大概率降低性能
使用Spring时,默认Controller、Service、Repository都是单例,加synchronized会导致整个程序几乎只能支持单线程,造成极大性能问题。
即使我们确实有一些共享资源需要保护,也要尽可能减小锁粒度。就像 concurrentHashMap 的一生发展。
案例
业务代码有个ArrayList会被多线程操作而需保护,但又有段比较耗时的不涉及线程安全的操作,应该如何加锁?
推荐只在操作ArrayList时给这ArrayList加锁。
正确加锁的版本几乎是对错误加锁的十倍性能。
细化锁后,性能还无法满足,就要考虑另一个维度的粒度问题:区分读写场景以及资源的访问冲突,考虑
4 悲观锁 V.S 乐观锁
一般业务代码很少需要进一步考虑这两种更细粒度的锁,自己结合业务的性能需求考虑是否要继续优化:
- 读写差异明显场景,考虑使用
ReentrantReadWriteLock
读写锁 - 若JDK版本>8、共享资源的冲突概率也没那么大,考虑使用
StampedLock
乐观读 - JDK的
ReentrantLock
、ReentrantReadWriteLock
都提供了公平锁版本,在没有明确需求情况下不要轻易开启公平锁,在任务很轻情况下开启公平锁可能会让性能下降百倍
5 死锁
锁的粒度够用就好,这意味着程序逻辑中有时会存在一些细粒度锁。但一个业务逻辑如果涉及多锁,就很容易产生死锁。
案例
在电商场景的下单流程中,需要锁定订单中多个商品的库存,拿到所有商品的锁后再进行下单扣减库存,全部操作完成后释放所有锁。
上线后发现,下单失败概率高,失败后用户需重新下单,极大影响用户体验。
案发原因
因为扣减库存的顺序不同,导致并发下多个线程可能相互持有部分商品的锁,又等待其他线程释放另一部分商品的锁,于是出现死锁。
接下来,我们剖析一下核心的业务代码。
首先,定义一个商品类型,包含商品名、库存剩余和商品的库存锁三个属性,每一种商品默认库存1000个;然后,初始化10个这样的商品对象来模拟商品清单:
模拟在购物车进行商品选购,每次从商品清单(items字段)中随机选购三个商品(不考虑每次选购多个同类商品的逻辑,购物车中不体现商品数量):