代码审核的时候,比如“HashMap”改为“ConcurrentHashMap”,可以解决并发问题,或者尝试无锁“CopyOnWriteArrayList”性能好。这些说法都不准确。 案例1SpringBoot创建Web应用,使用ThreadLocal存放Integer值,默契并发情况下遇到的bug。 private ThreadLocal<Integer> currentUser = ThreadLocal.withInitial(() -> null) @GetMapping("test") public Map test(@RequestParam("userId") Integer userId) { //设置用户信息之前先查询一次ThreadLocal中的用户信息 String before = Thread.currentThread().getName() + ":" + currentUser.get() //设置用户信息到ThreadLocal currentUser.set(userId); //设置用户信息之后再查询一次ThreadLocal中的用户信息 String after = Thread.currentThread().getName() + ":" + currentUser.get() //汇总输出两次查询结果 Map result = new HashMap(); result.put("before", before); result.put("after", after); return result; } 按照正常逻辑,第一次请求值为null。由于tomcat工作线程基于线程池的,如果线程重用,那么有可能第二次获取的值,就是上次请求的值。
修改tomcat最大线程数量配置:server.tomcat.max-threads=1 测试运行效果,启动项目浏览器输入接口:localhost:8019/test?userId=1 {"before":"http-nio-8019-exec-6:null","after":"http-nio-8019-exec-6:1"} 第一次请求,符合预想结果。 如果再请求接口,userId改为2,返回结果: {"before":"http-nio-8019-exec-6:1","after":"http-nio-8019-exec-6:2"} 因为线程的创建比较昂贵,所以web服务器使用了线程池,这时,使用类似 ThreadLocal 工具来存放一些数据时,需要特别注意在代码运行完后,显式地去清空设置的数据。如果在代码中使用了自定义的线程池,也同样会遇到这个问题。 修改后代码: public Map test(@RequestParam("userId") Integer userId) { //汇总输出两次查询结果 Map result = new HashMap(); try { //设置用户信息之前先查询一次ThreadLocal中的用户信息 String before = Thread.currentThread().getName() + ":" + currentUser.get(); //设置用户信息到ThreadLocal currentUser.set(userId); //设置用户信息之后再查询一次ThreadLocal中的用户信息 String after = Thread.currentThread().getName() + ":" + currentUser.get(); result.put("before", before); result.put("after", after); }finally { currentUser.remove(); } return result; } 测试运行正常。 案例2CopyOnWriteArrayList虽然是线程安全的ArrayList,但是每次修改数据都会复制一份数据,建议在读多写少的场景使用。 测试代码: @GetMapping("test1") public Map test1() { List<Integer> copyOnWriteArrayList = new CopyOnWriteArrayList<>(); List<Integer> synchronizedList = Collections.synchronizedList(new ArrayList()); StopWatch stopWatch = new StopWatch(); int loopCount = 100000; stopWatch.start("Write:copyOnWriteArrayList"); //循环100000次并发往CopyOnWriteArrayList写入随机元素 IntStream.rangeClosed(1, loopCount).parallel().forEach(__ -> copyOnWriteArrayList.add(ThreadLocalRandom. current().nextInt(100000))); stopWatch.stop(); stopWatch.start("Write:synchronizedList"); //循环100000次并发往加锁的ArrayList写入随机元素 IntStream.rangeClosed(1, loopCount).parallel().forEach(__ -> synchronizedList.add(ThreadLocalRandom. current().nextInt(100000))); stopWatch.stop(); log.info(stopWatch.prettyPrint()); Map result = new HashMap(); result.put("copyOnWriteArrayList", copyOnWriteArrayList.size()); result.put("synchronizedList", synchronizedList.size()); return result; } 运行结果如下: --------------------------------------------- ns % Task name --------------------------------------------- 7884828100 093% Write:copyOnWriteArrayList 552068200 007% Write:synchronizedList CopyOnWriteArrayList之所以运行慢,因为add方法会创建一个数组,频繁add释放消耗很大。 源码如下: public boolean add(E e) { synchronized (lock) { Object[] elements = getArray(); int len = elements.length; Object[] newElements = Arrays.copyOf(elements, len + 1); newElements[len] = e; setArray(newElements); return true; } } 总结
|
|