【JAVAEE】线程安全的集合类及死锁

news2024/12/24 10:01:41

目录

1.多线程环境使用集合类

2.多线程环境使用队列

3.多线程环境使用哈希表

3.1HashTable

3.2ConcurrentHashMap

4.死锁

4.1死锁是什么

4.2死锁的代码示例

4.3产生死锁的原因

4.4如何避免死锁


这里有一个代码示例:

定义一个普通的集合类,通过多线程同时对这个集合类进行add操作,并打印集合。

    public static void Demo01() throws InterruptedException {
        List<Integer> list=new ArrayList<>();

        for (int i = 0; i < 10; i++) {
            int finalI=i;
            Thread thread=new Thread(()->{
                list.add(finalI);
                System.out.println(list);
            });
            thread.start();
        }
        TimeUnit.SECONDS.sleep(1);
        System.out.println("=================");
        System.out.println(list);
    }

却抛出了异常,这是一个并发修改异常,也就是说在多线程环境下使用了线程不安全的集合类。

那么在多线程环境下如何使用线程安全的集合类?

1.多线程环境使用集合类

在多线程环境下如何使用线程安全的集合类?

1.使用Vector,HashTable等JDK提供的线程安全的类(不建议用)

2.自己使用同步机制(synchronized或者ReentrantLock)(同上,不建议用)

3.使用工具类转换Collections.synchronizedList(new ArrayList)

        //通过工具类来创建一个线程安全的集合
        List<Object>list= Collections.synchronizedList(new ArrayList<>());

实现方式是在普通集合对象外层又包裹了一层synchronized完成的线程安全。(不建议用)

4.CopyOnWriteArrayList

他时JUC包下的一个类,使用的是一种叫写时复制技术来实现的。

        //使用CopyOnWriteArrayList
        CopyOnWriteArrayList<Integer>list=new CopyOnWriteArrayList<>();

写时复制技术

1.当要修改一个集合时,先复制这个集合的复本

2.修改复本的数据,修改完成后,用复本覆盖原始集合

优点:

在读多写少的场景下,性能很高,不需要加锁竞争

缺点:

1.占用内存较多,因为复制了一份新的数据需要修改

2.新写的数据不能被第一时间读取到

在多线程环境中如果需要使用集合类那么优先考虑CopyOnWriteArrayList

2.多线程环境使用队列

多线程环境下使用队列都是基于底层的数据结果,并具备其特性。

1.ArrayBlockingQueue

基于数组实现的阻塞队列

2.LinkedBlockingQueue

基于链表实现的阻塞队列

3.PriorityBlockingQueue

基于堆实现的带优先级的阻塞队列

4.TransferQueue

最多只包含一个元素的阻塞队列

3.多线程环境使用哈希表

HashMap本身不是线程安全的。在多线程环境下使用哈希表可以使用:

  • HashTable
  • ConcurrentHashMap

3.1HashTable

只是简单的把关键方法加上了synchronized关键字。

 这相当于直接针对HashTable对象本身加锁。读写的时候都加锁这样效率比较低,不推荐使用。

  • 如果多线程访问同一个HashTable就会直接造成锁冲突
  • size属性也是通过synchronized来控制同步,也是比较慢的
  • 一旦触发扩容,就由该线程完成整个扩容过程,这个过程会涉及到大量的元素拷贝,效率会降低

一个HashTable只有一把锁,两个线程访问HashTable中的任意数据都会出现锁竞争。

3.2ConcurrentHashMap

相比于HashTable做出了一系列的改进和优化。

多线程环境下强烈推荐使用这种方式保证线程安全,它与HashTable,Collections不同,并不是使用synchronized关键字实现加锁的,而是通过JUC包下的ReentrantLock实现加锁。(ReentrantLock使用的是CAS,用户态来实现加锁)

优化:

1.更小的锁粒度

HashTable加锁的方式,对所有的操作全部加锁,必然对性能有影响

 ConcurrentHashMap对每个Hash桶进行加锁,提高并发能力

2.只给写加锁,不给读加锁

加锁的方式是ReentrantLock,大量运用CAS操作,而且共享变量使用volatile修饰

3. 充分利用CAS特性。比如size属性通过CAS来更新,避免出现重量级锁的情况

4.对扩容进行了特殊优化

  • 发现需要扩容的线程, 只需要创建一个新的数组, 同时只搬几个元素过去.
  • 扩容期间, 新老数组同时存在.
  • 后续每个来操作 ConcurrentHashMap 的线程, 都会参与搬家的过程. 每个操作负责搬运一小 部分元素.
  • 搬完最后一个元素再把老数组删掉.
  • 这个期间, 插入只往新数组加.
  • 这个期间, 查找需要同时查新数组和老数组

是一个典型的以空间换时间的用例。

4.死锁

4.1死锁是什么

死锁就是一个线程加上锁之后不运行也不释放僵持住了。死锁会导致程序无法运行,是一个最严重的bug之一。

举个栗子理解死锁
滑稽老哥和女神一起去饺子馆吃饺子 . 吃饺子需要酱油和醋 .
滑稽老哥抄起了酱油瓶 , 女神抄起了醋瓶 .
滑稽 : 你先把醋瓶给我 , 我用完了就把酱油瓶给你 .
女神 : 你先把酱油瓶给我 , 我用完了就把醋瓶给你 .
如果这俩人彼此之间互不相让 , 就构成了死锁 .
酱油和醋相当于是两把锁 , 这两个人就是两个线程

4.2死锁的代码示例

定义两个锁对象

        //定义两个锁对象
        Object locker1=new Object();
        Object locker2=new Object();

线程1,先获取locker1,在获取locker2

        //线程1,先获取locker1,再获取locker2
        Thread t1=new Thread(()->{
            System.out.println(Thread.currentThread().getName()+"t1申请locker1");
            synchronized (locker1){
                System.out.println(Thread.currentThread().getName()+"t1申请到了locker1");
                //模拟业务处理过程
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                //获取locker2
                System.out.println(Thread.currentThread().getName()+"t1申请locker2");
                synchronized (locker2) {
                    System.out.println(Thread.currentThread().getName() + "t1申请到了两把锁");
                }
            }
        });
        //启动t1
        t1.start();

线程2,先获取locker2,在获取locker1

        //线程2,先获取locker2,再获取locker1
        Thread t2=new Thread(()->{
            System.out.println(Thread.currentThread().getName()+"t2申请locker2");
            synchronized (locker2){
                System.out.println(Thread.currentThread().getName()+"t2申请到了locker2");
                //模拟业务处理过程
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                //获取locker2
                System.out.println(Thread.currentThread().getName()+"t2申请locker2");
                synchronized (locker1) {
                    System.out.println(Thread.currentThread().getName() + "t2申请到了两把锁");
                }
            }
        });
        //启动t2
        t2.start();

运行结果

 这样就造成了死锁,程序无法退出。两个线程对于加锁的顺序没有约定,就容易产生环路等待。

4.3产生死锁的原因

1.互斥使用:A被线程1占用了,线程2就不能用了

2.不可抢占:A被线程1占用了,线程2不能主动把锁A抢过来,除非线程1主动释放

3.请求保持:有多把锁,线程1拿到了锁A之后,不释放还要继续再拿锁B

4.循环等待:线程1等待线程2释放锁,线程2要释放锁得等待线程3先释放锁...形成了循环关系

4.4如何避免死锁

以上四条是形成死锁的必要条件,只要打破其中任何一条就可以避免死锁。

1.互斥使用不可抢占是锁的基本特性,无法打破。

2.请求保持是有可能打破的,取决于代码怎么写

3.循环等待,约定好加锁顺序就可以打破循环等待。在4.2的代码示例中t1.locker1->locker2,t2.locker2->locker1这个顺序造成了循环等待,如果调整加锁顺序,就可以避免循环等待。

4.2示例代码改正:

Object lock1 = new Object();
Object lock2 = new Object();
Thread t1 = new Thread() {
    @Override
    public void run() {
        synchronized (lock1) {
            synchronized (lock2) {
                // do something...
           }
       }
   }
};
t1.start();
Thread t2 = new Thread() {
    @Override
    public void run() {
        synchronized (lock1) {
            synchronized (lock2) {
                // do something...
           }
       }
   }
};
t2.start();

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

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

相关文章

动态规划之背包模型

文章目录 采药&#xff08;01背包&#xff09;装箱问题&#xff08;01背包&#xff09;宠物小精灵之收服(二维费用01背包&#x1f44d;&#x1f618;)数字组合(01背包)买书&#xff08;完全背包&#xff09;货币系统&#xff08;完全背包&#xff09; 采药&#xff08;01背包&a…

ROS:yaml文件解析:base_local_planner、global_costmap、local_costmap、base_local_planner

一.costmap_common_params.yaml # 设置了代价地图中障碍物信息的阀值 # obstacle_range&#xff1a;确定了最大范围传感器读数&#xff0c;这将导致障碍物被放入代价地图中。 # 此处设置为2.5m&#xff0c;意为着机器人只会更新其地图包含距离移动基座2.5m以内的障碍物信息 obs…

Python学习之用QTimer计时器实现摄像头视频的播放和暂停

在上一篇文章《Python学习之简易视频播放器》中&#xff0c;通过python-opencv-pyqt5&#xff0c;实现了有界面的视频播放。但是&#xff0c;上文代码只有播放&#xff0c;却无法让播放的视频暂停。这是因为&#xff0c;我们在播放中使用的是while(self.cap.isOpened())循环。若…

上海亚商投顾:沪指震荡调整跌0.21% 两市成交金额不足8000亿

上海亚商投顾前言&#xff1a;无惧大盘涨跌&#xff0c;解密龙虎榜资金&#xff0c;跟踪一线游资和机构资金动向&#xff0c;识别短期热点和强势个股。 市场情绪 三大指数今日震荡调整&#xff0c;上证50午后一度跌超1%&#xff0c;以保险为首的权重板块走低。军工股逆市大涨&a…

玩机搞机----电脑端几种反编译apk工具操作步骤解析

经常玩机的友友避免不了有时候需要反编译有些app或者JAR文件等等。目前各种反编译工具很多。各有所长吧。很多都是就过工具结合使用。而且很多app涉及到加密加壳。由于有些工具没有及时更新。老版本的底层还是apktool_2.4这些。对于新款的app反编译有点吃力且兼容性不太好。当然…

yolov2

yolov2相对于yolov1的改进&#xff1a; 1、加入Batch Normalization 2、yolov2使用更大的分辨率图片 V1训练使用图片分辨率为224*224&#xff0c;测试图片分辨率为448*448。 V2在V1上的改进为&#xff1a;V2训练时额外又进行了10次448*448的微调。 3、yolov2的网络结构 相…

linux0.12-9-3-hd.c

1、 没找到。 就是个变量。 #define DEVICE_INTR do_hd 2、 哪里用到 setup((void *) &drive_info); sys_setup [403页] 9-3 hd.c程序 9-3-1 功能描述 hd.c程序是硬盘控制器驱动程序&#xff0c;提供对硬盘控制器块设备的读写驱动和硬盘初始化处理。 程序中所有函数按照…

C++ ---- 类和对象(下)

目录 初始化列表 初始化列表的语法 初始化列表的特性 explicit关键字 构造函数的隐式转换 explicit的作用 static修饰成员变量和成员函数 static修饰成员变量 static修饰成员函数 友元 友元函数 友元类 内部类 匿名对象 拷贝对象时的一些编译器优化 初始化列表 …

盘点 | 10大类企业管理系统有哪些

人类的发展史也是一部工具的进化史&#xff0c;企业管理手段同样不例外。移动互联网时代给了传统低下的手工操作方式致命一击&#xff0c;应运而生的各类企业管理系统工具为企业管理插上腾飞的翅膀&#xff0c;彻底颠覆了手动低效率的历史&#xff0c;变得更加移动化、智能化。…

C语言——史上最全通讯录讲解(附源码)

C语言——史上最全通讯录讲解&#xff08;附源码&#xff09; 一、开始界面的打印二、对六大板块进行定义操作三、对联系人进行初始化四、对通讯录进行初始化4.1动态版本4.2静态版本 五、通讯录六大功能的具体实现5.1判断是否需要扩容Checkcapcity5.2添加联系人ADDcontact5.3删…

Salesforce许可证和版本有什么区别,购买帐号时应该如何选择?

Salesforce许可证分配给特定用户&#xff0c;授予他们访问Salesforce产品和功能的权限。Salesforce版本和许可证是不同的概念&#xff0c;但极易混淆。 Salesforce版本&#xff1a;这是对组织购买的Salesforce产品和功能的访问权限。大致可分为Essentials、Professional、Ente…

E-office Server_v9.0 漏洞分析

漏洞简介 泛微e-office是一款标准化的协同OA办公软件&#xff0c;实行通用化产品设计&#xff0c;充分贴合企业管理需求&#xff0c;本着简洁易用、高效智能的原则&#xff0c;为企业快速打造移动化、无纸化、数字化的办公平台。由于泛微 E-Office 未能正确处理上传模块中输入…

解读赛力斯年报:华为智选车的B面

作者 | Amy 编辑 | 德新 赛力斯&#xff0c;华为智选车的B面。 2021年&#xff0c;赛力斯SF5进入华为渠道销售&#xff0c;华为自此开启了智选车模式。到年末&#xff0c;双方更是推出AITO品牌。AITO凭借M5/M7等车型在2022年拿下了超过7.5万台的销量&#xff0c;成为增长最快的…

无线模块|如何选择天线和设计天线电路

无线模块的通信距离是一项重要指标&#xff0c;如何把有效通信距离最大化一直是大家疑惑的问题。本文根据调试经验及对天线的选择与使用方法做了一些说明&#xff0c;希望对工程师快速调试通信距离有所帮助。 一、天线的种类 随着技术的进步&#xff0c;为了节省研发周期&…

Blender基础技巧小结(二)

本文续前一篇&#xff1a;Blender基础技巧小结_皮尔斯巴巴罗的博客-CSDN博客 由于2.83开始使用的是新版ui&#xff0c;但是2.83文档内并没有更新&#xff0c;所以最好参考3.3版文档 https://docs.blender.org/manual/zh-hans/3.3/interface/controls/buttons/menus.html 缩…

单片机:实战练习

目录 【1】GPIO 1.定义 2.应用 I - Input - 输入采集 O - Output - 输出控制 ​编辑​编辑 3.GPIO结构框图 4.功能描述 输入功能 输出功能 5.相关寄存器 【2】点亮一盏LED灯 1.实验步骤 2.编程实现 3.编译下载 4.复位上电 练习&#xff1a;实现LED灯闪烁…

“视频AI+职业教育”会碰撞出什么样的火花?

玩过腾讯智影、用过D-ID、体验过Vega Al&#xff0c;终于等到要出“视频AI教育”类的应用型产品了&#xff0c;很振奋。 展望&#xff1a; 已经实现AI情绪分析、AI智能审核、AI视频内容检索、AI多语言实时字幕、AI会议速记、AI窄带高清编码多种功能。 “视频AI职业教育”&#…

ubantu22里面配置apache2的cgi

在 Ubuntu 22.04 中配置 Apache2 的 CGI 1.安装 Apache2 在终端中使用以下命令安装 Apache2&#xff1a; sudo apt-get update sudo apt-get install apache22.启用 CGI 模块 在 Ubuntu 中默认情况下&#xff0c;CGI 模块是禁用的。您需要手动启用它。在终端中使用以下命令来启…

为什么要使用Thrift与Protocol Buffers?

编码数据的格式 程序通常&#xff08;至少&#xff09;使用两种形式的数据&#xff1a; 在内存中&#xff0c;数据保存在对象、结构体、列表、数组、散列表、树等中。 这些数据结构针对 CPU 的高效访问和操作进行了优化&#xff08;通常使用指针&#xff09;。如果要将数据写…

centos7中安装mattermost

centos7中安装mattermost 步骤如下: 第一步安装依赖项&#xff1a;在终端中执行以下命令以安装所需的依赖项 sudo yum install epel-release sudo yum install yum-utils sudo yum install wget第二步&#xff0c;下载Mattermost安装包&#xff1a;执行以下命令以下载Mattermo…