Java常见编程错误:锁
2021-02-07 14:15
标签:共享资源 adl unlock 总结 void val readwrite art object 分析解决线程安全问题的锁在使用中的问题。 在?个类?有两个int类型的字段a和b,有?个add?法循环1万次对a和b进 ?++操作,有另?个compare?法,同样循环1万次判断a是否?于b,条件成?就打印a和b的值,并判断 a>b是否成?。 代码如下: 按道理,a和b同样进?累加操作,应该始终相等,compare中的第?次判断应该始终不会成?,不会输出任何?志。但,执?代码后发现不但输出了?志,?且更诡异的是,compare?法在判断ab也成?: 操作两个字段a和b,有线程安全问题,为add?法加上锁,确保a和b的++是原?性的,就不会错乱 了。 加锁后问题并没有解决。 来仔细想?下,为什么锁可以解决线程安全问题呢。因为只有?个线程可以拿到锁,所以加锁后的代码 中的资源操作是线程安全的。 但是,这个案例中的add?法始终只有?个线程在操作,显然只为add?法加锁是没?的。 之所以出现这种错乱,是因为两个线程是交错执?add和compare?法中的业务逻辑,?且这些业务逻辑不 正确的做法应该是,为add和compare都加上?法锁,确保add?法执?时,compare?法读取a和 b: 所以,使?锁解决问题之前?定要理清楚,我们要保护的是什么逻辑,多线程执?的情况?是怎样的。 除了没有分析清线程、业务逻辑和锁三者之间的关系随意添加?效的?法锁外,还有?种?较常?的错误是,没有理清楚锁和要保护的对象是否是?个层?的。 静态字段属于类,类级别的锁才能保护;??静态字段属于类实例,实例级别的锁就可以保护。 在类Data中定义了?个静态的int字段counter和?个?静态的wrong?法,实 现counter字段的累加操作。 代码如下: 因为默认运?100万次,所以执?后应该输出100万,但实际输出的是673767: 问题分析: 在?静态的wrong?法上加锁,只能确保多个线程?法执?同?个实例的wrong?法,却不能保证不会执?不同实例的wrong?法。 ?静态的counter在多个实例中共享,所以必然会出现线程安全问题。 同样在类中定义?个Object类型的静态字段,在操作counter之前对这个字段加锁。 在?法上加synchronized关键字实现加锁确实简单,也因此曾看到?些业务代码中?乎所有?法都加了synchronized,但这种滥?synchronized的做法: 即使我们确实有?些共享资源需要保护,也要尽可能降低锁的粒度,仅对必要的代码块甚?是需要保护的资源本?加锁。 在业务代码中,有?个ArrayList因为会被多个线程操作?需要保护,?有?段?较耗时的操作(代码中的slow?法)不涉及线程安全问题,应该如何加锁呢? 错误的做法是,给整段业务逻辑加锁,把slow?法和操作ArrayList的代码同时纳?synchronized代码块; 更合适的做法是,把加锁的粒度降到最低,只在操作ArrayList的时候给这个ArrayList加锁。 ?般业务代码中,很少需要进?步考虑这两种更细粒度的锁,?概的结论: 锁的粒度够?就好,这就意味着我们的程序逻辑中有时会存在?些细粒度的锁。但?个业务逻 辑如果涉及多把锁,容易产?死锁问题。 案例: 下单操作需要锁定订单中多个商品的库存,拿到所有商品的锁之后进?下单扣 减库存操作,全部操作完成之后释放所有的锁。代码上线后发现,下单失败概率很?,失败后需要??重新 下单,极?影响了??体验,还影响到了销量。 经排查发现是死锁引起的问题,背后原因是扣减库存的顺序不同,导致并发的情况下多个线程可能相互持有 部分商品的锁,?等待其他线程释放另?部分商品的锁,于是出现了死锁问题。 代码示例: 定义?个商品类型,包含商品名、库存剩余和商品的库存锁三个属性,每?种商品默认库存1000 个;初始化10个这样的商品对象来模拟商品清单: 写?个?法模拟在购物?进?商品选购,每次从商品清单(items字段)中随机选购三个商品(为了逻辑简单,不考虑每次选购多个同类商品的逻辑,购物?中不体现商品数量) 下单代码如下:先声明?个List来保存所有获得的锁,然后遍历购物?中的商品依次尝试获得商品的锁,最 ?等待10秒,获得全部锁之后再扣减库存;如果有?法获得锁的情况则解锁之前获得的所有锁,返回false 下单失败。 写?段代码测试这个下单操作。模拟在多线程情况下进?100次创建购物?和下单操作,最后通过?志 输出成功的下单次数、总剩余的商品个数、100次下单耗时,以及下单完成后的商品库存明细: 使?JDK?带的VisualVM?具来跟踪?下,重新执??法后不久就可以看到,线程Tab中提?了死锁问题 分析: 购物?添加商品的逻辑,随机添加了三种商品,假设?个购物?中的商品是item1和 item2,另?个购物?中的商品是item2和item1, ?个线程先获取到了item1的锁,同时另?个线程获取到 了item2的锁,然后两个线程接下来要分别获取item2和item1的锁,这个时候锁已经被对?获取了,只能相互等待?直到10秒超时。 解决方案: 为购物?中的商品排?下序,让所有的线程?定是先获取item1的锁然后获 取item2的锁,就不会有问题了。所以,我只需要修改??代码,对createCart获得的购物?按照商品名进?排序即可: 如果业务逻辑中锁的实现?较复杂的话,要仔细看看加锁和释放是否配对,是否有遗漏释放或重复释 放的可能性;并且要考虑锁?动超时释放了,?业务逻辑却还在进?的情况下,如果别的线线程或进程拿到 了相同的锁,可能会导致重复执?。 如果业务代码涉及复杂的锁操作,应该Mock相关外部接?或数 据库操作后对应?代码进?压测,通过压测排除锁误?带来的性能问题和死锁问题。 Java常见编程错误:锁 标签:共享资源 adl unlock 总结 void val readwrite art object 原文地址:https://www.cnblogs.com/liekkas01/p/12775880.html场景:
volatile int a = 1;
volatile int b = 1;
int loop=10000000;
public void add() {
System.out.println("add start");
for (int i = 0; i b));
//最后的a>b应该始终是false吗?
}
}
System.out.println("compare done");
}
public static void main(String[] args) {
LockTest test = new LockTest();
new Thread(() -> test.add()).start();
new Thread(() -> test.compare()).start();
}
9899491,9899492,false
9899949,9899950,true
9900959,9900959,false
9901787,9901786,true
解决方案1:
public synchronized void add()
是原?性的:a++和b++操作中可以穿插在compare?法的?较代码中;更需要注意的是,a作在字节码层?是加载a、加载b和?较三步,代码虽然是??但也不是原?性的。解决方案2:
public synchronized void add()
public synchronized void compare()
加锁前要清楚锁和被保护的对象是不是?个层?的
场景:
static int count = 1000000;
@Getter
private static int counter = 0;
public static int reset() {
counter = 0;
return counter;
}
public synchronized void wrong() {
counter++;
}
public static void main(String[] args) {
Data.reset();
//多线程循环?定次数调?Data类不同实例的wrong?法
IntStream.rangeClosed(1, count)
.parallel()
.forEach(i -> new Data().wrong());
System.out.println(Data.getCounter());
}
解决方案:
static Object locker = new Object();
public void right() {
synchronized (locker) {
counter++;
}
}
加锁要考虑锁的粒度和场景问题
场景:
private List
如果精细化考虑了锁应?范围后,性能还?法满?需求的话,就要考虑另?个维度的粒度问题了,
即: 区分读写场景以及资源的访问冲突,考虑使?悲观?式的锁还是乐观?式的锁。
多把锁要??死锁问题
@Data
@RequiredArgsConstructor
public class Item {
final String name; //商品名
int remaining = 1000; //库存剩余
//ToString不包含这个字段
@ToString.Exclude
ReentrantLock lock = new ReentrantLock();
}
private List
private boolean createOrder(List
public long wrong() {
long begin = System.currentTimeMillis();
//并发进?100次下单操作,统计成功次数
long success = IntStream.rangeClosed(1, 100).parallel()
.mapToObj(i -> {
List
long success = IntStream.rangeClosed(1, 100).parallel()
.mapToObj(i -> {
List
总结: