打开APP
userphoto
未登录

开通VIP,畅享免费电子书等14项超值服

开通VIP
java并发工具类库,线程安全可能造成的隐患!

代码审核的时候,比如“HashMap”改为“ConcurrentHashMap”,可以解决并发问题,或者尝试无锁“CopyOnWriteArrayList”性能好。这些说法都不准确。

案例1

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;
    }

测试运行正常。

案例2

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

本站仅提供存储服务,所有内容均由用户发布,如发现有害或侵权内容,请点击举报
打开APP,阅读全文并永久保存 查看更多类似文章
猜你喜欢
类似文章
多线程场景下使用 ArrayList,这几点一定要注意!
CopyOnWriteArrayList与Collections.synchronizedList的...
JUC(15)线程安全1:如何保证线程安全
5天玩转C#并行和多线程编程
你所不知道的五件事情--多线程编程(译)
简单分析Java线程编程中ThreadLocal类的使用
更多类似文章 >>
生活服务
热点新闻
分享 收藏 导长图 关注 下载文章
绑定账号成功
后续可登录账号畅享VIP特权!
如果VIP功能使用有故障,
可点击这里联系客服!

联系客服