先来看一段简单的同步锁代码
@Controller public class DemoController {
private final Logger logger = LoggerFactory.getLogger(DemoController.class);
@RequestMapping("/process/{orderId}") @ResponseBody public Map<String, Object> process(@PathVariable("orderId") String orderId) throws Exception { synchronized (this) { logger.debug("[{}] 开始", orderId); Thread.sleep(1500); logger.debug("[{}] 结束", orderId); }
Map<String, Object> map = new HashMap(); map.put("result", "success"); return map; } }
|
这段代码使用了 synchronized 同步锁(锁对象是this),但是出现了一个问题,对于同一个订单的id请求时,在多线程环境下就不应该再进入这段处理代码中,
比如orderId=001时已经进入了 synchronized 中开始执行了,此时又进来一个 orderId=001的请求,因为orderId一致,就不应该再进来。
但实际情况是:因为同步锁对象是 this,而两次请求的对象不同,自然不属于同一个对象,导致 synchronized 并不会起作用。
修改上面的代码,使其同一个 orderId 不能再进入 同步锁中
@Controller public class DemoController {
private final Logger logger = LoggerFactory.getLogger(DemoController.class);
@RequestMapping("/process/{orderId}") @ResponseBody public Map<String, Object> process(@PathVariable("orderId") String orderId) throws Exception { synchronized (orderId.intern()) { logger.debug("[{}] 开始", orderId); Thread.sleep(1500); logger.debug("[{}] 结束", orderId); }
Map<String, Object> map = new HashMap(); map.put("result", "success"); return map; } }
|
上述代码,将同一个 orderId 的请求变成了字符串常量池,这样,多个请求时,虽然每次都是一个新的String对象,但是这里锁的却是字符串值,因此是相等的,所以可以起到作用。
那么除了上面的字符串常量池方式,我们还可以自己维护一个缓存池对象,每次来一个orderId时从缓存池中获取对象。
@Controller public class DemoController {
private final Logger logger = LoggerFactory.getLogger(DemoController.class);
Map<String, Object> mutexCache = new HashMap<>();
@RequestMapping("/process/{orderId}") @ResponseBody public Map<String, Object> process(@PathVariable("orderId") String orderId) throws Exception { Object mutex4Key = mutexCache.get(orderId); if(mutex4Key == null){ mutex4Key = new Object(); mutexCache.put(orderId, mutex4Key); } synchronized (mutex4Key) { logger.debug("[{}] 开始", orderId); Thread.sleep(1500); logger.debug("[{}] 结束", orderId); mutexCache.remove(orderId); }
Map<String, Object> map = new HashMap(); map.put("result", "success"); return map; } }
|
上面写法会带来一个问题:当有大量的请求到来时,会出现前一个线程在判断 mutex4Key==null 后,然后开始new了,但还没有push进去,但此时后一个线程进来了,
它得到的判断是 mutex4Key==null,导致又开始new了。所以会有这种错误情况出现。那怎么解决呢?可以给if判断加一个同步锁:
@Controller public class DemoController {
private final Logger logger = LoggerFactory.getLogger(DemoController.class);
Map<String, Object> mutexCache = new HashMap<>();
@RequestMapping("/process/{orderId}") @ResponseBody public Map<String, Object> process(@PathVariable("orderId") String orderId) throws Exception { Object mute4Key = null; synchronized (this){ mute4Key = mutexCache.get(orderId); if (mute4Key == null){ mute4Key = new Object(); mutexCache.put(orderId, mute4Key); } }
synchronized (mute4Key) { logger.debug("[{}] 开始", orderId); Thread.sleep(1500); logger.debug("[{}] 结束", orderId); mutexCache.remove(orderId); } Map<String, Object> map = new HashMap(); map.put("result", "success"); return map; } }
|
上面代码写的有点繁琐了,其实我们在构建缓存时可以使用 ConcurrentHashMap,ConcurrentHashMap本身就是线程安全的map的实现
Map<String, Object> mutexCache = new ConcurrentHashMap<>(); @RequestMapping("/process/{orderId}") @ResponseBody public Map<String, Object> process(@PathVariable("orderId") String orderId) throws Exception { Object mute4Key = mutexCache.computeIfAbsent(orderId, k -> new Object());
synchronized (mute4Key) { logger.debug("[{}] 开始", orderId); Thread.sleep(1500); logger.debug("[{}] 结束", orderId); mutexCache.remove(orderId); } Map<String, Object> map = new HashMap(); map.put("result", "success"); return map; }
|
相关代码参考: https://github.com/hello-github-ui/java_base/blob/master/lock/src/main/java/com/example/lock/LockApplication.java