使用了并发工具类库,线程就安全了吗

简介: 使用了并发工具类库,线程就安全了吗

使用了并发工具类库,线程就安全了吗

在这里插入图片描述

并发工具类库

  • 有时会听到有关线程安全和并发工具的一些片面的观点和结论,比如“把 HashMap 改为 ConcurrentHashMap ,要不我们试试无锁的 CopyOnWriteArrayList 吧,性能更好,事实上,这些说法都特殊场景下都不太准确
  • 为了方便开发者进行多线程编程,现代编程语言提供了各种并发工具类 并且提供了 JUC 包 java.util.concurrent , 但是没有充分了解他们的使用场景、解决的问题,以及最佳实践的话,盲目使用就可能导致一些坑、小则损失性能,大则无法保证多线程去看下业务逻辑正确性。

在这里插入图片描述


1. 没有意识到线程重用导致用户信息错乱的 Bug


ThreadLocal提高一个线程的局部变量,访问某个线程拥有自己局部变量。我们常常使用使用ThreadLocal 用来存储用户信息,但是发现ThreadLocal 有时获取到的用户信息是别人的,


我们知道,ThreadLocal适用于变量在线程间隔离,而在方法或类间共享的场景。如果用户信息的获取比较昂贵(比如从数据库查询用户信息),那么在 ThreadLocal中缓存数据是比较合适的做法。但,这么做为什么会出现用户信息错乱的 Bug ?

案例 :
private ThreadLocal<Integer> currentUser = ThreadLocal.withInitial(() -> null);



 @ApiOperation(value = "test2")
    @GetMapping("/test2")
    public ResponseMessage test2(@ApiParam(value = "id")@RequestParam(required = false) Integer id) {
        //设置用户信息之前先查询一次ThreadLocal中的用户信息
        String before = Thread.currentThread().getName() + ":" + currentUser.get();
        currentUser.set(id);
        String after = Thread.currentThread().getName() + ":" + currentUser.get();
        //汇总输出两次查询结果
        Map result = new HashMap();
        result.put("before", before);
        result.put("after", after);
        return ResultBody.success(result);

在这里插入图片描述

在设置用户信息之前第一次获取的值始终应该是 null,但我们要意识到,程序运行在 Tomcat 中,执行程序的线程是 Tomcat 的工作线程,而 Tomcat 的工作线程是基于线程池的。


  • 顾名思义,线程池会重用固定的几个线程,一旦线程重用,那么很可能首次从 ThreadLocal 获取的值是之前其他用户的请求遗留的值。这时,ThreadLocal 中的用户信息就是其他用户的信息。所以上图中我新用户 获取到了 旧用户遗留的 信息,

因为线程的创建比较昂贵,所以web服务器往往会使用线程池来处理请求,就意味着线程会被重用。这是,使用类似ThreadLocal工具来存放一些数据时,需要特别注意在代码运行完后,显式的去清空设置的睡觉。如果在代码中使用来自定义线程池,也同样会遇到这样的问题

优化
 @ApiOperation(value = "test2")
    @GetMapping("/test2")
    public ResponseMessage test2(@ApiParam(value = "id")@RequestParam(required = false) Integer id) {
        //设置用户信息之前先查询一次ThreadLocal中的用户信息
        String before = Thread.currentThread().getName() + ":" + currentUser.get();
        currentUser.set(id);
        Map result = new HashMap();
        try {
            String after = Thread.currentThread().getName() + ":" + currentUser.get();
            //汇总输出两次查询结果
            
            result.put("before", before);
            result.put("after", after);
        }finally {
        //在finally代码块中删除ThreadLocal中的数据,确保数据不串
            currentUser.remove();
        }
        return ResultBody.success(result);
    }

1. 使用了线程安全的并发工具,并不代表解决了所有的线程安全问题

JDK 1.5 后推出的 ConcurrentHashMap,是一个高性能的线程安全的哈希表容器。“线程安全”这四个字特别容易让人误解,因为 ConcurrentHashMap 只能保证提供的原子性读写操作是线程安全的。

案例
public class Test {

    private ConcurrentHashMap<String, Integer> map=new ConcurrentHashMap<String, Integer>();

        public static void main(String[] args) {
            final Test t=new Test();
            for (int i = 0; i < 10000; i++) {
                new Thread(new Runnable() {
                    @Override
                    public void run() {
                        t.add("key");
                    }
                }).start();
            }
        }

    public void add(String key){
        Integer value=map.get(key);
        if(value==null)
            map.put(key, 1);
        else
            map.put(key, value+1);

        System.out.println(map.get(key));
    }



}

在这里插入图片描述

解决:

public class Test {

    private ConcurrentHashMap<String, Integer> map=new ConcurrentHashMap<String, Integer>();

        public static void main(String[] args) {
            final Test t=new Test();
            for (int i = 0; i < 10000; i++) {
                new Thread(new Runnable() {
                    @Override
                    public void run() {
                        t.add("key");
                    }
                }).start();
            }
        }

    public synchronized void add(String key){
        Integer value=map.get(key);
        if(value==null)
            map.put(key, 1);
        else
            map.put(key, value+1);

        System.out.println(map.get(key));
    }



}

如果只是调用put或者get方法,ConcurrentHashMap是线程安全的,但是如果调用了get后在调用map.put(key, value+1)之前有另外的线程去调用了put,然后你再去执行put,就有可能将结果覆盖掉,但这个其实也不能算ConcurrentHashMap线程不安全,ConcurrentHashMap内部操作是线程安全的,但是外部操作还是要靠自己来保证同步,即使在线程安全的情况下,也是可能违反原子操作规则。。。


3. 没有认清并发工具的使用场景,因而导致性能问题

除了 ConcurrentHashMap 这样通用的并发工具类之外,我们的工具包中还有些针对特殊场景实现的生面孔。一般来说,针对通用场景的通用解决方案,在所有场景下性能都还可以,属于“万金油”;而针对特殊场景的特殊实现,会有比通用解决方案更高的性能,但一定要在它针对的场景下使用,否则可能会产生性能问题甚至是 Bug。


CopyOnWrite 是一个时髦的技术,不管是 Linux 还是 Redis 都会用到。在 Java 中,

CopyOnWriteArrayList 虽然是一个线程安全的 ArrayList,但因为其实现方式是,每次
修改数据时都会复制一份数据出来,所以有明显的适用场景,即读多写少或者说希望无锁读的场景。

案例:

测试写的性能

public static void main(String[] args) {
        CopyOnWriteArrayList<Integer> cowal = new CopyOnWriteArrayList<Integer>();
        ArrayList<Integer> list = new ArrayList<Integer>();

        int count = 500;
        long time1 = System.currentTimeMillis();
        while (System.currentTimeMillis() - time1 < count) {
            cowal.add(1);
        }
        time1 = System.currentTimeMillis();
        while (System.currentTimeMillis() - time1 < count) {
            list.add(1);
        }
        System.out.println("CopyOnWriteArrayList在" + count + "毫秒时间内添加元素个数为:  "
                + cowal.size());
        System.out.println("ArrayList在" + count + "毫秒时间内添加元素个数为:  "
                + list.size());

    }

在这里插入图片描述

  • 以 add 方法为例,每次 add 时,都会用 Arrays.copyOf 创建一个新数组,频繁 add 时内存的申请释放消耗会很大

读性能比较

public static void main(String[] args) throws InterruptedException {

        // create object of CopyOnWriteArrayList
        List<Integer> ArrLis = new ArrayList<>();


        List<Integer> copyOnWriteArrayList = new CopyOnWriteArrayList<>();

        System.gc();

        for (int i = 0; i < 100000; i++) {
            ArrLis.add(i);
        }

        for (int i = 0; i < 100000; i++) {
            copyOnWriteArrayList.add(i);
        }

        Thread.sleep(500);
        long startTime = System.currentTimeMillis();    //获取开始时间
        // print CopyOnWriteArrayList
        System.out.println("ArrayList: "
                + ArrLis);
        // 2nd index in the arraylist
        System.out.println(" index: "
                + ArrLis.get(5000));
        long endTime = System.currentTimeMillis();    //获取结束时间
        System.out.println(" ArrayList  : 程序运行时间:" + (endTime - startTime) + "ms");    //输出程序运行时间

        Thread.sleep(500);

        long startTime2 = System.currentTimeMillis();    //获取开始时间
        // print CopyOnWriteArrayList
        System.out.println("copyOnWriteArrayList: "
                + copyOnWriteArrayList);
        // 2nd index in the arraylist
        System.out.println(" index: "
                + copyOnWriteArrayList.get(5000));
        long endTime2 = System.currentTimeMillis();    //获取结束时间
        System.out.println(" copyOnWriteArrayList  : 程序运行时间:" + (endTime2 - startTime2) + "ms");    //输出程序运行时间

        System.gc();
    }

在这里插入图片描述

  • 总结:虽然JDK 给我们提供了一些并发工具类,我们要想充分体现他的性能 还需要更加的去了解他的机制 ,不然可能就会成为项目中的累赘

个人博客地址:http://blog.yanxiaolong.cn/

相关文章
|
19天前
|
缓存 安全 Java
【JavaEE】——单例模式引起的多线程安全问题:“饿汉/懒汉”模式,及解决思路和方法(面试高频)
单例模式下,“饿汉模式”,“懒汉模式”,单例模式下引起的线程安全问题,解锁思路和解决方法
|
19天前
|
Java 调度
【JavaEE】——线程的安全问题和解决方式
【JavaEE】——线程的安全问题和解决方式。为什么多线程运行会有安全问题,解决线程安全问题的思路,synchronized关键字的运用,加锁机制,“锁竞争”,几个变式
|
2月前
|
安全 Java
线程安全的艺术:确保并发程序的正确性
在多线程环境中,确保线程安全是编程中的一个核心挑战。线程安全问题可能导致数据不一致、程序崩溃甚至安全漏洞。本文将分享如何确保线程安全,探讨不同的技术策略和最佳实践。
54 6
|
2月前
|
安全 Java 开发者
Java 多线程并发控制:深入理解与实战应用
《Java多线程并发控制:深入理解与实战应用》一书详细解析了Java多线程编程的核心概念、并发控制技术及其实战技巧,适合Java开发者深入学习和实践参考。
77 6
|
2月前
|
存储 安全 Java
Java多线程编程中的并发容器:深入解析与实战应用####
在本文中,我们将探讨Java多线程编程中的一个核心话题——并发容器。不同于传统单一线程环境下的数据结构,并发容器专为多线程场景设计,确保数据访问的线程安全性和高效性。我们将从基础概念出发,逐步深入到`java.util.concurrent`包下的核心并发容器实现,如`ConcurrentHashMap`、`CopyOnWriteArrayList`以及`BlockingQueue`等,通过实例代码演示其使用方法,并分析它们背后的设计原理与适用场景。无论你是Java并发编程的初学者还是希望深化理解的开发者,本文都将为你提供有价值的见解与实践指导。 --- ####
|
2月前
|
存储 设计模式 分布式计算
Java中的多线程编程:并发与并行的深度解析####
在当今软件开发领域,多线程编程已成为提升应用性能、响应速度及资源利用率的关键手段之一。本文将深入探讨Java平台上的多线程机制,从基础概念到高级应用,全面解析并发与并行编程的核心理念、实现方式及其在实际项目中的应用策略。不同于常规摘要的简洁概述,本文旨在通过详尽的技术剖析,为读者构建一个系统化的多线程知识框架,辅以生动实例,让抽象概念具体化,复杂问题简单化。 ####
|
21天前
|
NoSQL Redis
单线程传奇Redis,为何引入多线程?
Redis 4.0 引入多线程支持,主要用于后台对象删除、处理阻塞命令和网络 I/O 等操作,以提高并发性和性能。尽管如此,Redis 仍保留单线程执行模型处理客户端请求,确保高效性和简单性。多线程仅用于优化后台任务,如异步删除过期对象和分担读写操作,从而提升整体性能。
51 1
|
3月前
|
存储 消息中间件 资源调度
C++ 多线程之初识多线程
这篇文章介绍了C++多线程的基本概念,包括进程和线程的定义、并发的实现方式,以及如何在C++中创建和管理线程,包括使用`std::thread`库、线程的join和detach方法,并通过示例代码展示了如何创建和使用多线程。
68 1
|
3月前
|
Java 开发者
在Java多线程编程中,创建线程的方法有两种:继承Thread类和实现Runnable接口
【10月更文挑战第20天】在Java多线程编程中,创建线程的方法有两种:继承Thread类和实现Runnable接口。本文揭示了这两种方式的微妙差异和潜在陷阱,帮助你更好地理解和选择适合项目需求的线程创建方式。
47 3
|
3月前
|
Java 开发者
在Java多线程编程中,选择合适的线程创建方法至关重要
【10月更文挑战第20天】在Java多线程编程中,选择合适的线程创建方法至关重要。本文通过案例分析,探讨了继承Thread类和实现Runnable接口两种方法的优缺点及适用场景,帮助开发者做出明智的选择。
29 2