synchronized与ReentrantLock
为什么锁能解决线程安全问题?
答:因为只有一个线程拿到锁,加锁后的代码中的资源操作时线程安全的。所以加锁前要清楚锁和被保护的对象是不是一个层面的(线程、业务逻辑、锁三者之间的关系)。
锁和被保护的对象层面怎么确认?
答:静态字段属于类,类级别的锁才能保护;而非静态字段属于类级别,实例级别的锁就可以保护。
静态字段用类级别的锁保护例子:
@Data public class Inventory { public static int counter = 10000; public synchronized void wrong() { counter--; } } @Slf4j public class InventoryDemo { public static void main(String[] args) { int count = 10000; log.info("总库存:{}", count); IntStream.rangeClosed(1, count).parallel().forEach(i -> new Inventory().wrong()); log.info("每次减1执行{}次,剩余库存:{}", count, Inventory.counter); } } //执行第一次结果 //总库存:10000 //每次减1执行10000次,剩余库存:456 //执行第二次 //总库存:10000 //每次减1执行10000次,剩余库存:179
问题:可以看出并不是正确的答案值0,分析得出,这里的的非静态方法上加的锁,并不能解决静态的counter在多个实例中共享问题,所以出现了线程安全问题。
方案:把wrong方法定义静态是一种方法,但这个时候锁是类级别的,为了解决线程安全改变代码结构,所以不可行。另一种则是,对counter加锁。
@Data public class Inventory { public static int counter = 10000; public static Object lock = new Object(); public void right() { synchronized (lock){ counter--; } } } @Slf4j public class InventoryDemo { public static void main(String[] args) { int count = 10000; log.info("总库存:{}", count); IntStream.rangeClosed(1, count).parallel().forEach(i -> new Inventory().right()); log.info("每次减1执行{}次,剩余库存:{}", count, Inventory.counter); } } //执行多次结果 //总库存:10000 //每次减1执行10000次,剩余库存:0
加锁的粒度和场景
1. 我们常用的ssm架构,业务代码都是三层架构,数据经过无状态的Controller、Service、Responsitory流转到数据库,没必要使用synchronized保护什么数据。
2. 锁可能极大降性能。Spring框架,默认情况下Controller、Service、Responsitory是单例的,加上synchronized会导致整个程序几乎就只能支持单线程,造成极大性能问题。
3. 降低锁的粒度。仅对必要的代码或者说需要保护的资源本身加锁,一个方法里可能有调用其他耗时的业务方法,所以只需要在共享的资源上加锁。
4. 区分读写场景以及资源的访问冲突,考虑使用悲观锁还是乐观锁。在读写比例差异明显的场景,考虑使用ReentrantReadWriteLock细化区分读写锁,来提高性能。同时,JDK里ReentrantLock和ReentrantReadWriteLock都提供了公平锁的版本在没有明确需求的情况下不要轻易开启公平锁特性,任务轻情况下开启公平锁会让性能下降百倍。
5. 注意死锁。①互斥条件②不可剥夺条件③请求和保持条件④循环等待条件。以下例子是并发情况下多把锁持有相同资源。
死锁问题:
比如:下单操作要锁定订单中多个商品的库存,拿到所有商品的锁后进行下单扣库存,全部操作完后释放所有锁。代码上线后发现,下单失败概率很高,失败后需要用户重新下单,极大影响了用户体验,还影响了销量。