一个订单要给多个供应商派单,为了防止重复提交,在给业务代码添加了一个 synchronized块进行串行化执行,重点是把订单id作为该同步块的锁,只有同一个订单的执行业务时才会串行执行,而不同的订单会并行执行。
代码是这样的:
public boolean addNewPurchaseDistribute(List<String> supplierIds, String purchaseId) {
//同一个采购单派单需要串行化执行,防止重复派单
synchronized (purchaseId){
//判断采购单是否处于派单中
PurchaseProcess purchaseProcess = purchaseProcessService.getProcessInfoById(purchaseId);
if (purchaseProcess == null) {
return false;
}
if (!StringUtils.equals(purchaseProcess.getSuppDeliveStatus(), SupplierOrderStatusEnum.WAITING_CONFIRM.getValue())) {
return false;
}
//过滤掉重复派单的供应商
supplierIds = supplierIds.stream().filter(supplierId -> purchaseProcess.getDistributeSupps().indexOf(supplierId) <0).collect(Collectors.toList());
if(CollectionUtils.isEmpty(supplierIds)){
return false;
}
//录入派单列表
PurchaseDistribute distribute = new PurchaseDistribute();
distribute.setPdAcceptStatus(PurchaseDistributeStatus.WAITING.getValue());
distribute.setPdProcId(purchaseId);
distribute.setPdDeleteFlag(YesNo.NO.name());
distribute.setPdDealPerson(SessionUtils.getUserId());
purchaseDistributeBusiness.addBatchToSuppliers(distribute, supplierIds);
//采购单的分派供应商更新
updateDistributeSupps(purchaseProcess.getDistributeSupps(), supplierIds, purchaseProcess);
//向供应商发送短信
purchaseSendInfoService.sendPurchaseMsgToSupplier(supplierIds);
//新增派单日志
String names = getSupplierNames(supplierIds);
String content = "采购单 " + purchaseProcess.getInnerName() + " 已派单给" + names;
distributeLogService.addOne(content, purchaseId);
return true;
}
这样表面上看起来是可以的,但是执行起来却不尽人意!,因为当你页面重复点击提交请求时,在controller层会有打印出两个线程id,可在方法内打断点的时候只会执行一次,看来并没有串行化。
那到底这样做是有什么问题吗?
我们分析到,这里面的string虽然值是同一个,但是在controller层传递过来的参数其实是从两个线程内存中取到的,地址不是同一个。所以,现在的问题就是如何保证锁住的string是同一个。
那我在String类中的方法中找到一个intern()方法,它的方法解释是“从String类中维护的常量池中取该值对应的字符串对象,如果没有,会创建一个新的字符串对象”,就是把锁换成这个
synchronized (purchaseId.intern()){
//相关业务代码
}
这样确实可以保证是同一把锁,还有一点是需要关心的,就是在String类中的这个属性是维护常量池的
/** The value is used for character storage. */
private final char value[];
它在jdk1.6及之前是存放在永久代内存区的,在jdk1.7之后是存放在堆内存中,是为了既能保证字符串重复利用,也能在非引用状态有可能被gc回收。