代码审核的时候,比如“HashMap”改为“ConcurrentHashMap”,可以解决并发问题,或者尝试无锁“CopyOnWriteArrayList”性能好。这些说法都不准确。
SpringBoot创建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工作线程基于线程池的,如果线程重用,那么有可能第二次获取的值,就是上次请求的值。
模拟bug场景
修改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; }
测试运行正常。
CopyOnWriteArrayList虽然是线程安全的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; } }
使用 ThreadLocal 来缓存数据时,记得在业务逻辑结束之前清理。
CopyOnWriteArrayList 的适用场景,在大量写操作的场景下,导致性能问题。建议,你可以考虑是用普通的 List
联系客服