Java封装同步锁

先来看一段简单的同步锁代码

@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()) {   // 将 orderId 变成一个 字符串常量池
            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) {   // 将 orderId 变成一个 字符串常量池
            logger.debug("[{}] 开始", orderId);
            Thread.sleep(1500);
            logger.debug("[{}] 结束", orderId);
            // 业务执行完,删除缓存
            mutexCache.remove(orderId);
        }

        Map<String, Object> map = new HashMap();
        map.put("result", "success");
        return map;
    }
}

上面写法会带来一个问题:当有大量的请求到来时,会出现前一个线程在判断 mutex4Keynull 后,然后开始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) {   // 锁对象是 缓存中对应的 value
            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<>(); // 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) {   // 锁对象是 缓存中对应的 value
            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

posted @ 2022-06-22 00:27  LoremMoon  阅读(175)  评论(0)    收藏  举报