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