【JUC基础】11. 并发下的集合类

news2024/12/30 2:17:17

目录

 1、前言

2、并发下的ArrayList

2.1、传统方式

2.1.1、程序正常运行

2.1.2、程序异常

2.1.3、运行期望值不符

2.2、加锁

2.3、synchronizedList

2.4、CopyOnWriteArrayList

3、并发下的HashSet

3.1、CopyOnWriteArraySet

3.2、HashSet底层是什么?

4、并发下的HashMap

4.1、传统方式

4.2、ConcurrentHashMap

4.3、ConcurrentHashMap底层结构

5、小结


 1、前言

我们直到ArrayList,HashMap等是线程不安全的容器。但是我们通常会频繁的在JUC中使用集合类,那么应该如何确保线程安全?

2、并发下的ArrayList

2.1、传统方式

如果在JUC中直接使用ArrayList,可能会引发一系列问题。先来看一段代码:


public class ArrayListTest {

    // 创建一个集合类
    static List<Integer> list = new ArrayList<>(10);

    public static void main(String[] args) throws InterruptedException {
        // 这里执行10次,对比10次结果
        for (int i = 0; i < 10; i++) {

            // 每次执行前将list清空
            list.clear();

            // 创建两个线程,分别往list里面存放数据,每个线程存放10000个
            Thread thread1 = new Thread(new MyThread());
            Thread thread2 = new Thread(new MyThread());
            thread1.start();
            thread2.start();
            thread1.join();
            thread2.join();

            // 当两个线程执行完毕后,期望值应该是list会扩容到20000个,并且打印list.sizes()=20000
            System.out.println("最终集合数量:" + list.size());
        }

    }

    // 操作集合list线程
    static class MyThread implements Runnable {
        @Override
        public void run() {
            for (int i = 0; i < 10000; i++) {
                list.add(i);
            }
        }
    }
}

执行结果:

我们看到执行了10次,居然会出现3种不同的结果。

2.1.1、程序正常运行

从上述的运行结果可以看出,运行10次,有概率出现程序正常运行,也得到了期望的20000这个数值。这说明在JUC中使用ArrayList集合,有概率成功,并不一定每次都会出现问题。

2.1.2、程序异常

可以看到上面其中一次运行结果出现了报错,抛出了ArrayIndexOutOfBoundsException异常。这是因为ArrayList我们设置初始容量为10,在多线程操作中要进行扩容。而在扩容过程中,内部的一致性被破坏,由于没有锁机制,另外一个线程访问到了不一致的内部状态,导致数组越界。

2.1.3、运行期望值不符

相比上面程序异常,程序异常会显式抛出异常信息,还相对容易排查。而这个问题较为隐蔽,从执行结果来看,大部分都是这个问题。也就是运行结果并不是我们所期望的结果。JUC学到这里,应该多少都直到这个就是典型的线程不安全导致的结果。由于多线程访问冲突,使得list容器大小的变量被多线程不正常访问,两个线程对list中的同一个位置进行赋值导致的。

2.2、加锁

上面说到list没有锁机制,出现了多线程问题。那么要解决此类问题,肯定是直接加锁, 我们顺便把集合数量改大点。改造后代码:


public class ArrayListTest {

    // 创建一个集合类
    static List<Integer> list = new ArrayList<>(10);

    public static void main(String[] args) throws InterruptedException {
        // 这里执行10次,对比10次结果
        for (int i = 0; i < 10; i++) {
            // 每次执行前将list清空
            list.clear();

            // 创建两个线程,分别往list里面存放数据,每个线程存放1000000个
            Thread thread1 = new Thread(new MyThread());
            Thread thread2 = new Thread(new MyThread());
            thread1.start();
            thread2.start();
            thread1.join();
            thread2.join();

            // 当两个线程执行完毕后,期望值应该是list会扩容到2000000个,并且打印list.sizes()=2000000
            System.out.println("最终集合数量:" + list.size());
        }
    }

    // 操作集合list线程
    static class MyThread implements Runnable {
        @Override
        public void run() {
            for (int i = 0; i < 1000000; i++) {
                synchronized (list) {
                    list.add(i);
                }
            }
        }
    }
}

运行结果:

说明线程安全问题被解决。

2.3、synchronizedList

相比上面直接加synchronized方法的解决方式,JDK提供了一种自带synchronized的集合,来保证线程安全。如vector也是如此。

改造代码:


public class ArrayListTest {

    // 创建一个集合类,Collections.synchronizedList来保证线程安全
    static List<Integer> list = Collections.synchronizedList(new ArrayList<>(10));

    public static void main(String[] args) throws InterruptedException {
        // 这里执行10次,对比10次结果
        for (int i = 0; i < 10; i++) {
            // 每次执行前将list清空
            list.clear();

            // 创建两个线程,分别往list里面存放数据,每个线程存放10000个
            Thread thread1 = new Thread(new MyThread());
            Thread thread2 = new Thread(new MyThread());
            thread1.start();
            thread2.start();
            thread1.join();
            thread2.join();

            // 当两个线程执行完毕后,期望值应该是list会扩容到20000个,并且打印list.sizes()=20000
            System.out.println("最终集合数量:" + list.size());
        }
    }

    // 操作集合list线程
    static class MyThread implements Runnable {
        @Override
        public void run() {
            for (int i = 0; i < 1000000; i++) {
                list.add(i);
            }
        }
    }
}

同样执行结果:

2.4、CopyOnWriteArrayList

JUC也给我们提供了一种线程安全的变体ArrayList。根据名字就可以直到他是采用复制“快照”的方式,性能上是会有一定开销的。这里在实验过程中,明显感觉得到结果的速度变慢了。

改造后代码:


public class ArrayListTest {

    // 创建一个集合类,CopyOnWriteArrayList,写入时复制。
    // 当多个线程调用的时候,对list进行写入操作时,将数据拷贝避免由于多线程同时操作而被覆盖。可以简单理解成读写分离操作。
    // 这个类的操作使用的是lock锁,相比上述的两种synchronized来实现同步,性能更高
    static List<Integer> list = new CopyOnWriteArrayList<>();

    public static void main(String[] args) throws InterruptedException {
        // 这里执行10次,对比10次结果
        for (int i = 0; i < 10; i++) {
            // 每次执行前将list清空
            list.clear();

            // 创建两个线程,分别往list里面存放数据,每个线程存放10000个
            Thread thread1 = new Thread(new MyThread());
            Thread thread2 = new Thread(new MyThread());
            thread1.start();
            thread2.start();
            thread1.join();
            thread2.join();

            // 当两个线程执行完毕后,期望值应该是list会扩容到20000个,并且打印list.sizes()=20000
            System.out.println("最终集合数量:" + list.size());
        }
    }

    // 操作集合list线程
    static class MyThread implements Runnable {
        @Override
        public void run() {
            for (int i = 0; i < 10000; i++) {
                list.add(i);
            }
        }
    }
}

运行结果:

那么他是如何保证线程安全的呢?我们查看他的源码发现:

在他的setArra方法中,对array加了transient和volatile修饰,从而保证了线程安全。

transient:被transient修饰的属性,是不会被序列化的。后面有机会单独详细讲

volatile:防止指令重排,以及保证可见性。他是java中一种轻量的同步机制,相比synchronized来说,volatile更轻量级。后面单独会讲

3、并发下的HashSet

HashSet和ArrayList存在同样的问题。

public class HashSetTest {

    static Set<Integer> hashSet = new HashSet<>();

    public static void main(String[] args) throws InterruptedException {
        // 这里执行10次,对比10次结果
        for (int i = 0; i < 10; i++) {
            // 每次执行前将list清空
            hashSet.clear();

            // 创建两个线程,分别往list里面存放数据,每个线程存放10000个
            Thread thread1 = new Thread(new MyThread());
            Thread thread2 = new Thread(new MyThread());
            thread1.start();
            thread2.start();
            thread1.join();
            thread2.join();

            // 当两个线程执行完毕后,期望值应该是list会扩容到20000个,并且打印list.sizes()=20000
            System.out.println("最终集合数量:" + hashSet.size());
        }
    }

    // 操作集合list线程
    static class MyThread implements Runnable {
        @Override
        public void run() {
            for (int i = 0; i < 10000; i++) {
                hashSet.add(i);
            }
        }
    }
}

执行结果:

与ArrayList类似,当然也存在加锁的方式。同样采用JDK提供的方式:

Collections.synchronizedSet(new HashSet<>());

3.1、CopyOnWriteArraySet

同样JUC也提供了类似CopyOnWriteArrayList的方式。

改造后代码:

public class HashSetTest {

    static Set<Integer> hashSet = new CopyOnWriteArraySet<>();

    public static void main(String[] args) throws InterruptedException {
        // 这里执行10次,对比10次结果
        for (int i = 0; i < 10; i++) {
            // 每次执行前将list清空
            hashSet.clear();

            // 创建两个线程,分别往list里面存放数据,每个线程存放10000个
            Thread thread1 = new Thread(new MyThread());
            Thread thread2 = new Thread(new MyThread());
            thread1.start();
            thread2.start();
            thread1.join();
            thread2.join();

            // 当两个线程执行完毕后,期望值应该是list会扩容到20000个,并且打印list.sizes()=20000
            System.out.println("最终集合数量:" + hashSet.size());
        }
    }

    // 操作集合list线程
    static class MyThread implements Runnable {
        @Override
        public void run() {
            for (int i = 0; i < 10000; i++) {
                hashSet.add(i);
            }
        }
    }

}

运行结果:

3.2、HashSet底层是什么?

细心的网友有没有发现,这里的运行结果也不是我们期望的20000。而是10000。那么是不是说明这里其实并不能保证线程安全?JDK出bug了?

这里就涉及到HashSet的底层存储结构了。我们跟进去看下HashSet源码:

我们可以看到HashSet的底层结构其实是个HashMap,而HashSet存储的是使用了HashMap的key。这就保证了HashSet的存储是不能重复的。

hashSet的add方法使用的就是HashMap的put方法:

而我们上面两个线程都同时从0开始存储,因而被去重导致期望结果是10000。而CopyOnWriteArraySet虽然实现存储结构是CopyOnWriteArrayList,但他保留了Hashset的去重结构,在add的时候使用了AddIfAbsent,因而输出的结果值为10000。

要验证这个结果其实也很简单,我们把hashSet.add()中的值,改为不重复的,比如使用雪花id来填充:

public class HashSetTest {

    static Set<String> hashSet = new CopyOnWriteArraySet<>();
    public static void main(String[] args) throws InterruptedException {
        // 这里执行10次,对比10次结果
        for (int i = 0; i < 10; i++) {
            // 每次执行前将list清空
            hashSet.clear();

            // 创建两个线程,分别往list里面存放数据,每个线程存放10000个
            Thread thread1 = new Thread(new MyThread());
            Thread thread2 = new Thread(new MyThread());
            thread1.start();
            thread2.start();
            thread1.join();
            thread2.join();

            // 当两个线程执行完毕后,期望值应该是list会扩容到20000个,并且打印list.sizes()=20000
            System.out.println("最终集合数量:" + hashSet.size());
        }
    }

    // 操作集合list线程
    static class MyThread implements Runnable {
        @Override
        public void run() {
            for (int i = 0; i < 10000; i++) {
                hashSet.add(IdUtil.getSnowflakeNextIdStr());
            }
        }
    }
}

那么结果就是我们想要的20000了:

4、并发下的HashMap

4.1、传统方式

public class HashMapTest {

    static Map<String, Object> hashMap = new HashMap<>();

    public static void main(String[] args) throws InterruptedException {
        // 这里执行10次,对比10次结果
        for (int i = 0; i < 10; i++) {
            // 每次执行前将list清空
            hashMap.clear();

            // 创建两个线程,分别往list里面存放数据,每个线程存放10000个
            Thread thread1 = new Thread(new MyThread());
            Thread thread2 = new Thread(new MyThread());
            thread1.start();
            thread2.start();
            thread1.join();
            thread2.join();

            // 当两个线程执行完毕后,期望值应该是list会扩容到20000个,并且打印list.sizes()=20000
            System.out.println("最终集合数量:" + hashMap.size());
        }
    }

    // 操作集合list线程
    static class MyThread implements Runnable {
        @Override
        public void run() {
            for (int i = 0; i < 10000; i++) {
                hashMap.put(IdUtil.getSnowflakeNextIdStr(), i);
            }
        }
    }

}

运行结果:

同样也存在线程安全问题。与ArrayList类似,当然也存在加锁的方式。同样采用JDK提供的方式:

Collections.synchronizedMap(new HashMap<>());

4.2、ConcurrentHashMap

与CopyOnWriteArrayList或者set类似,JUC也提供了线程安全的Map集合。只是换个了名字:ConcurrentHashMap。

改造后代码:

public class HashMapTest {

    static Map<String, Object> hashMap = new ConcurrentHashMap<>();

    public static void main(String[] args) throws InterruptedException {
        // 这里执行10次,对比10次结果
        for (int i = 0; i < 10; i++) {
            // 每次执行前将list清空
            hashMap.clear();

            // 创建两个线程,分别往list里面存放数据,每个线程存放10000个
            Thread thread1 = new Thread(new MyThread());
            Thread thread2 = new Thread(new MyThread());
            thread1.start();
            thread2.start();
            thread1.join();
            thread2.join();

            // 当两个线程执行完毕后,期望值应该是list会扩容到20000个,并且打印list.sizes()=20000
            System.out.println("最终集合数量:" + hashMap.size());
        }
    }

    // 操作集合list线程
    static class MyThread implements Runnable {
        @Override
        public void run() {
            for (int i = 0; i < 10000; i++) {
                hashMap.put(IdUtil.getSnowflakeNextIdStr(), i);
            }
        }
    }

}

运行结果:

4.3、ConcurrentHashMap底层结构

那么JUC为什么不叫CopyOnWriteHashMap,而改名叫ConcurrentHashMap呢?因为他们两者的实现方式完全不一样。 前面讲到CopyOnWriteArrayList是采用复制快照的方式,实现类似读写分离的方式来确保数值不会被覆盖。

而ConcurrentHashMap却采用了分段锁的机制来确保线程安全。具体的后面专门来讲。这里只需要记住ConcurrentHashMap是可以保证线程安全即可。

可以初步看到源码中采用了分段,并添加了synchronized同步块代码,来确保高性能下的线程安全。

5、小结

学到这里,我们发现java下的集合类大部分都不是线程安全的。而为了确保线程安全,我们可以采取多种措施,包括JDK也提供了多种方式来确保集合在多线程中的线程安全问题。而很多时候,因为集合线程不安全导致的问题是很隐蔽的,如上述示例代码所示,并不会每次都显式的抛出异常信息,只是会让你每次的结果不一致,而每次运行结果未必都会复现。所以针对此类问题,需要谨慎对待。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/579281.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

python基础----环境搭建-----01

一 python介绍 1.1 Python 特点 Python 是完全面向对象的语言。函数、模块、数宁、宁符串都是对象&#xff0c;在 Python 中一切皆对象。完全支持继承、重载、多重继承。支持重载运算符&#xff0c;也支持泛型设计。Python 拥有一个强大的标准库&#xff0c;Python 语言的核心…

element-ui菜单el-menu的使用

效果演示 先给大家看一下效果吧 el-menu详解 Menu Attributes# 属性名说明类型可选值默认值mode菜单展示模式stringhorizontal / verticalverticalcollapse是否水平折叠收起菜单&#xff08;仅在 mode 为 vertical 时可用&#xff09;boolean—falseellipsis是否省略多余的子项…

四、 JSP04 Servlet 技术

四、 Servlet 技术 4.1 认识 Servlet Web 容器在处理 JSP 文件时&#xff0c;会将 JSP 文件通过 JSP 容器转换成可识别的 .java 文件 这个 .java 文就是一个 Servlet 类&#xff0c;JSP 技术就是基于 Servlet 实现的 4.1.1 什么是 Servlet Servlet 是一个符合特定规范的 Java…

Linux系统编程学习 NO.5 ——shell命令行的概念以及原理、权限的概念

1.shell命令行的概念以及原理 首先&#xff0c;用户下达指令需求。此时Linux操作系统的内核kernel&#xff0c;并不会直接接收用户下达的指令&#xff0c;因为操作系统不擅长跟用户打交道。那么指令要如何下达呢?这就命令行解释器来对用户的指令进行处理。 1.1.shell命令行的…

每日学术速递5.26

CV - 计算机视觉 | ML - 机器学习 | RL - 强化学习 | NLP 自然语言处理 Subjects: cs.CV 1.Text2NeRF: Text-Driven 3D Scene Generation with Neural Radiance Fields 标题&#xff1a;Text2NeRF&#xff1a;具有神经辐射场的文本驱动 3D 场景生成 作者&#xff1a;Jingb…

从组件化角度聊聊设计工程化

目录 设计系统 设计系统的定义 设计系统的优势 设计系统存在的问题 设计工程化 设计系统探索 设计系统落地实践 Design Token Design Token 实践 设计工程化理想方案构想 展望 参考文献 近几年围绕业务中台化的场景&#xff0c;涌现出了许多低代码平台。面对多组件…

RAW、RGB 、YUV三种图像格式理解

文章目录 1. 背景2. 相关概念2.1 颜色与色彩空间2.2 RAW图像2.3 RGB图像2.4 YUV图像 3. 分类简图 RAW、RGB 、YUV三种图像格式理解 1. 背景 在工作中&#xff0c;经常听到用来描述图像格式的RAW&#xff0c;RGB与YUV&#xff0c;但一直没有系统的进行了解&#xff0c;处于局部认…

Redis实战之实现共同关注

Redis实战之实现共同关注 一 需求 二 实现 package com.hmdp.service.impl;import cn.hutool.core.bean.BeanUtil; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.…

用ChatGPT一分钟自动产出一份高质量PPT

如何用ChatGPT一分钟自动产出一份高质量PPT&#xff0c;节约时间摸鱼呢&#xff1f;废话少说&#xff0c;直接上案例。 一.用ChatGPT做一下提问&#xff0c;这里我用的小程序万事知天下&#xff0c;根据自己PPT的需求&#xff0c;制作chatgpt的prompt就行了。 请帮我创建一个以…

Spring Security 核心解读(一)整体架构

Spring Security 整体架构 前提整体架构Servlet 整体的过滤器模型Security 过滤器链自定义过滤器 实际开发解决方案一个替代cookie认证的filter其他组件&#xff0c;后续抽时间再整理整理 前提 开源项目一手文档基本都在github&#xff0c;标准文档基本都在官网。 最好的文档就…

在Centos Stream 9上Docker的实操教程 - Docker的常用命令

在Centos Stream 9上Docker的实操教程 - Docker的常用命令 Docker启动类命令Docker镜像命令镜像列表 docker images镜像查找 docker search拉取镜像 docker pull删除镜像 docker rmi查看占用信息 docker system df容器创建新镜像 docker commit 容器命令启动容器 docker run查看…

【历史上的今天】4 月 27 日:Tumblr 上线;施乐推出了 Star 工作站;第一台安德伍德打字机诞生

整理 | 王启隆 透过「历史上的今天」&#xff0c;从过去看未来&#xff0c;从现在亦可以改变未来。 今天是 2023 年 4 月 27 日&#xff0c;在 1791 年的今天&#xff0c;摩斯电码的共同发明者、电报发明者塞缪尔摩斯&#xff08;Samuel Morse&#xff09;诞生。摩斯最开始是一…

基于springboot + vue 的学生成绩管理系统

基于springboot vue实现的学生成绩管理系统 主要模块&#xff1a; 1&#xff09;学生模块&#xff1a;我的成绩、成绩统计、申述管理、修改密码 2&#xff09;教师模块&#xff1a;任务管理、对学生班级任务安排、班级学生的成绩查看、申述管理 3&#xff09;管理员模块&…

Vue自定义插件的使用

通过 Vue 实例绑定方法&#xff1a; 在 plugins.js 文件中创建 filter 过滤器&#xff0c;定义一个只返回前四个字符的方法。 export default {install(Vue){// 定义过滤器Vue.filter(mySlice,function(value){return value.slice(0,4);})} } 由于我们之前在 main.js 文件中引入…

六级备考20天|CET-6|翻译练习|真题·红楼梦|8:50~9:08+11:33~12:00

目录 1 中文 2 英文​ 3 解析 4 订正 ​ 1 中文 漏翻译&#xff1a;具有很强的艺术感染力&#xff01; 2 英文 3 解析 tell 讲述 tragic love story 悲剧性爱情故事 own painful personal experience 自己痛苦的个人经历 major/minor characters 主要/次要人物 be viv…

SSH爆破攻击及应急响应/事件处置

提示&#xff1a;本文是我做的笔记&#xff0c;有问题可以留言 目录 前言一、什么是SSH&#xff1f;二、开始前的准备1.扫描2.准备爆破3.准备ssh登录登陆后的准备nc反弹 应急响应/事件处置1.查看网络连接情况2.查看守护进程3.删除&#xff0c;结束异常后门4.修改密码 总结 前言…

day40_servlet

今日内容 零、 复习昨日 一、注解 二、改造项目 三、请求转发 四、重定向 零、 复习昨日 一、注解(Annotation) 注解,又称为注释.它是给程序看的注释. JDK1.5后才出现的,作用是为了提高开发效率的,如何做到?(一个注解可以简化很多很多代码…) 常见注解: Override 1.1 自定义注…

Linux基础开发工具之软件包管理器

目录 前言 1.什么是软件包 2.软件下载的三种方式 3. Linux软件生态 4. 使用yum安装软件 5.yum源的相关介绍 总结&#xff1a; 前言 Linux作为一款操作系统&#xff0c;其自然也和我们其他的操作系统一样需要安装对应得软件去满足我们的需求&#xff0c;因此为了更好的下载…

ICV报告: ADAS SoC市场规模将在2024年迎来较大突破

随着先进驾驶辅助系统&#xff08;ADAS&#xff09;的出现和对于自动驾驶的追求&#xff0c;汽车行业正在经历快速转型。这些技术进步的核心是ADAS SoC&#xff0c;它是实现多个功能集成于单一平台的关键组件。ADAS SoC已经成为智能汽车的重要驱动因素&#xff0c;彻底改变了安…

ArduPilot开源代码之H743+BMI270x2+ChibiOS配置适配

ArduPilot开源代码之H743BMI270x2ChibiOS配置适配 1. 源由2. 配置适配2.1 bootloader配置2.2 flight controller配置 3. 4.3.6固件编译Step 1: 获取源代码Step 2: 准备编译环境Step 3: 复制配置文件Step 4: 编译bootloaderStep 5: 编译飞控 4. 基础配置4.1 机型配置4.2 IMU校准…