
目录
业务场景
第一版代码实现
性能分析
第二版代码实现
按门店分类存储订单信息,维护在一个ConcurrentHashMap
功能实现可以忽略从Map取出ConcurrentlinkedQueue队里和往队列放订单这两步操作的原子性,业务中允许订单顺序存在一定误差。
之前有文章写过ConcurrentHashMap并发场景下的使用方式,可以基于computeIfAbsent方法实现Map的取值非空判断和初始化操作的原子性从而实现并发安全。
computeIfAbsent方法通过加锁的方式保证从ConcurrentHashMap取出ConcurrentlinkedQueue队列的操作是原子操作。
public class OrderService{
public static Map> shopOrderMap = new ConcurrentHashMap();
public void setWaitMakeQueue(MakeOrder makeOrder){
try {
//原子操作取出门店的ConcurrentlinkedQueue队列
ConcurrentlinkedQueue list = this.shopOrderMap.computeIfAbsent(key, key1 -> new ConcurrentlinkedQueue<>());
//此操作允许与取出ConcurrentlinkedQueue队列非原子操作带来的顺序误差
list.add(makeOrder);
} catch (Exception e) {
log.error("订单进入待制作队列异常", e);
}
}
}
上面代码中锁的目的是保证非空判断和初始化操作的原子性,也就是同一个key的value只会被初始化一次。但是在实际业务场景中由于ConcurrentHashMap中每个门店的队列只会初始化一次,所以非空判断的逻辑其实执行的比例其实很少当门店订单队列已经初始化过后,由于ConcurrentHashMap本身的get操作是线程安全的,那就没必要再加锁了,所以从性能考虑的话整体加锁的实现并不是很好。
类似于双重校验单例模式的实现,通过双重校验+volatile实现同步块代码量的最小化提升性能。当门店订单队列已经初始化过后,执行无锁部分的代码借助CAS原子操作就完成放入队列的操作了,只有当门店队列未初始化时才会加锁。
但是由于订单队列是Map结构中的Value无法通过volatile关键字修饰,所以在双重校验时需要重新获取一次用于第二次校验。
public class ShopLock{
private static Map map = new ConcurrentHashMap();
public static ReentrantLock getShopLock(String key){
return map.computeIfAbsent(key,k -> new ReentrantLock());
}
}
public class OrderService{
public static Map> shopOrderMap = new ConcurrentHashMap();
public void setWaitMakeQueue(MakeOrder makeOrder){
try {
//首先不加锁获取一次门店的订单存储队列
ConcurrentlinkedQueue list = this.shopOrderMap.get(key);
if(list != null){
list.add(makeOrder);
return;
}
//加本地门店ReentrantLock锁,目的是保证取出list和list.add(makeOrder)操作的原子性
ShopLock.getShopLock(key).lock();
//因为订单存储队列是Map的Value,无法通过volatile实现可见性,所以加锁后需要重新重新获取门店的订单存储队列,用于队列的双重校验
list = this.shopOrderMap.get(key);
if(list != null){
list.add(makeOrder);
return;
}
//双重校验都为空后初始化存储队列,放入订单后存到ConcurrentHashMap结构中
list = new ConcurrentlinkedQueue<>();
list.add(makeOrder);
this.shopOrderMap.put(key, list)
} catch (Exception e) {
log.error("订单进入待制作队列异常", e);
} finally {
//释放门店锁
ShopLock.getLock(key).unlock();
}
}
}