JavaEE 第16节 线程安全的集合类

ops/2024/10/9 17:28:25/

目录

前言

顺序表

队列

哈希表

1、Hashtable

2、ConcurrentHashMap(重点)


前言

本文章主要介绍在多线程环境下如何线程安全的使用一些常用的集合类(顺序表和哈希表)。


顺序表

1、自己使用同步锁机制(synchornized和ReentrantLock)保证线程安全

2、Collections.synchronizedList()

它是 Java 中 java.util.Collections 类提供的一个基于synchronized关键字,给List加锁的静态方法,用于创建安全的List。

3、写时拷贝

Java中提供了CopyOnWrit这个容器来解决线程安全问题。

它的解决思路是:

在多个线程读取容器中的数据的时候不会做任何操作,但是当一个线程要修改容器中的数据的时候,不能立刻修改当前引用所指向的数据,而是重新拷贝一份相同的容器,在这个新的容器中去修改数据。在写入数据的时候,其他线程只能读取旧容器中的数据。等修改完毕,把引用现在已经修改好的新的容器(引用的赋值是一个原子操作)。

优点:

 在读多写少的环境下,因为没有加锁,所以没有锁开销,也没有锁竞争,性能高。

缺点:

  • 不能实现多个线程同时修改数据,因此不适用于写多读少的场景。
  • 因为拷贝的参与,所以当数据量本身很大的情况下会消耗内存,并且时间开销也会增大。

队列

关于队列的线程安全,自然使用到的是阻塞队列:

1. ArrayBlockingQueue:   基于数组实现的阻塞队列

2. LinkedBlockingQueue: 基于链表实现的阻塞队列

3. PriorityBlockingQueue: 基于堆实现的带优先级的阻塞队列

4. TransferQueue:             最多只包含⼀个元素的阻塞队列

关于阻塞队列的详细介绍这里就不展开了,在同专栏第9节专门讲解了。


哈希表

HashMap本身是线程不安全的,在多线程环境下可以使用Hashtable/ConcurrentHashMap

1、Hashtable

Hashtable这个类只是对HashMap进行简单的加锁,也就是在关键方法上增加了synchronized关键字:

这就导致只要多个线程同时调用put或者get方法,即使他们想要访问的不是同一组数据,照样会进入阻塞状态,也就是所冲突,极大的影响了运行效率。

另外,如何在某一个线程使用put操作时,如果需要扩容,那么就会由当前线程进行扩容,如果涉及大量的元素拷贝,那么多线程的运行效率又会大幅降低。

总的来讲,HashTable相当于对整个哈希桶上了锁:

2、ConcurrentHashMap[推荐使用]

为了降低锁冲突概率,ConcurrentHashMap做出了以下优化:

  • 读操作:没有加锁,但是加了volatile保证内存可见。
  • 写操作:对每一个链表都进行了加锁(synchronized实现),如果写入数组中不同的下标,那么就不会发生所冲突,如图:
  • CAS的使用:在锁竞争并不激烈的情况下,采用CAS来更新size(键值对 增加/减少),这种方式不用加锁,更轻量,提高运行效率(jdk8引入)。
     
  • 分散计数器槽:在锁竞争激烈时,使用CAS就可能出现忙等的情况,使用CAS修改size就不太好了。为了避免多个线程同时修改同一个数据(size),ConcurrentHashMap引入了分散计数器槽(Counter Cells),它的原理如下:

  1、分散计数器槽的初始化:

.     ◦ 当多个线程进行插入或删除操作时,如果检测到对单个计数器的竞争变得激烈,ConcurrentHashMap会创建一个计数器槽数组。

      ◦ 这个数组中的每个槽(cell)都可以独立地更新,从而减少竞争。

  2、计数器槽的更新:

     ◦ 当一个线程需要更新size(例如插入或删除元素)时,它会尝试使用CAS操作更新某个计数器槽。

     ◦ 如果更新失败(因为另一个线程同时在更新这个槽),该线程会尝试更新另一个槽,直到成功为止。

  3、计数器槽的汇总:

     ◦ 当需要获取ConcurrentHashMap的大小时,size()方法会遍历所有计数器槽,将它们的值相加得到总的元素数量。


Counter Cells的核心思想就是使用数组来降低并发负载。

  • 扩容方式:与HashTable让发现需要扩容的线程去完成整个扩容任务不同,Concurrent把扩容任务分散给了多个线程去完成,即“化整为零”。
    大致步骤如下:​​​​

1、创建一个比原来还要大的数组。

2、每一个线程都尝试去获取一个没有完成旧数组到新数组迁移任务的桶(链表),然后对桶进行数据迁移、重新哈希(使用CAS保证线程安全,同时提高整体效率)。

3、所有桶都完成了数据迁移后,数组引用从旧数组指向新数组,扩容才真正完成。


注意,在扩容期间:

1)如果有put(插入操作),先对旧桶进行哈希,如果旧桶没有数据迁移,那么就插入到旧桶中,如果已经数据迁移,就插入到新桶中。

2)如果是get(查找操作),先找旧桶,旧桶没有找到,找新桶。

这种扩容方式大大提高了并发性能(逐步迁移,降低阻塞概率)以及系统稳定性(扩容时,单个线程锁占用时间不会激增)。



http://www.ppmy.cn/ops/100438.html

相关文章

STL中的内存分配器

一、operator new 和 new operator 的区别 1.1、new operator new 运算符是 C 提供的语法糖,用于在堆上动态分配内存并同时调用构造函数初始化对象。 功能: 分配足够的内存来存储对象。调用对象的构造函数,执行初始化。返回指向分配内存的指针。 语法…

【HuggingFace Transformers】BertSelfOutput 和 BertOutput源码解析

BertSelfOutput 和 BertOutput源码解析 1. 介绍1.1 共同点(1) 残差连接 (Residual Connection)(2) 层归一化 (Layer Normalization)(3) Dropout(4) 线性变换 (Linear Transformation) 1.2 不同点(1) 处理的输入类型(2) 线性变换的作用(3) 输入的特征大小 2. 源码解析2.1 BertSe…

kali 设置 时区

kali 设置 时区 在Kali Linux中设置时区可以通过以下步骤进行: 打开终端。 输入以下命令来查看可用的时区列表: timedatectl list-timezones 找到您所在的城市或地区对应的时区,然后使用以下命令进行设置(将Europe/Berlin替换…

redisj集群之哨兵模式

1.redis的高并发可用? 首先可以搭建主从集群,再加上使用redis的哨兵模式。 2. 什么是哨兵模式? 哨兵(sentinel)模式:可以实现主从集群的自动故障恢复。包含了主从服务的监控、自动故障恢复、通知。 监控…

案例分享—优秀ui设计作品赏析

多浏览国外优秀UI设计作品,深入分析其设计元素、色彩搭配、布局结构和交互方式,以理解其背后的设计理念和趋势。 在理解的基础上,尝试将国外设计风格中的精髓融入自己的设计中,同时结合国内用户的审美和使用习惯,进行创…

IDEA插件支持API调试、接口用例支持一键同步API变更,MeterSphere开源持续测试工具v3.2.0版本发布

2024年8月26日,MeterSphere开源持续测试工具正式发布v3.2.0版本。 在这一版本中,接口测试方面,MeterSphere API Debugger插件支持API调试,接口用例支持一键同步API变更;测试管理方面,在“测试用例”模块中…

02-Python 基础语法规则

Python 基础语法规则 规则描述1. 编码默认情况下,Python3 源码文件以 UTF-8 编码,所有字符串都是 Unicode 字符串。可以通过 # -*- coding: utf-8 -*- 指定编码。2. 标识符标识符必须以字母或下划线 _ 开头,其余部分可以是字母、数字或下划线…

制造业中的MES知识与ERP\PLM\WMS架构关系(附智能制造MES解决方案PPT下载)

PPT下载地址见文末~ 一、MES系统定义与功能 制造执行系统(Manufacturing Execution System,简称MES)是一套面向制造企业车间执行层的生产信息化管理系统,用于跟踪和记录从原材料到成品的生产转化过程。MES系统通过收集、分析和处…