当前位置: 首页 > article >正文

JavaEE:多线程进阶(线程安全的集合类)

文章目录

  • 线程安全的集合类
    • 多线程环境使用ArrayList
    • 多线程环境使用队列
    • 多线程环境使用哈希表
      • Hashtable
      • ConcurrentHashMap


线程安全的集合类

之前学习的集合类大部分都不是线程安全的.
比如ArrayList,Queue,HashMap等等,这都是线程不安全的.

Vector,Stack,Hashtable,这些集合类虽然是线程安全的(内置了synchronized),但是实际上这几个东西并不推荐使用,因为它们都是无脑加锁的.

多线程环境使用ArrayList

  1. 自己加锁

  2. 使用标准库中提供的带锁的List,Collections.synchronizedList(new ArrayList);
    synchronizedList就相当于构造出了一个核心方法,自带synchronized的List.
    在这里插入图片描述

  3. 使用CopyOnWriteArrayList
    这个集合类,没有加锁,但是通过"写实拷贝"来实现的线程安全.通过写时拷贝,避免两个线程同时修改一个变量.
    在这里插入图片描述

    写实拷贝介绍:当我们往一个容器添加元素的时候,不直接往当前容器内添加,而是先将当前容器Copy,复制出一个新的容器,然后在新的容器里添加元素.添加完元素之后,再将原容器的引用指向新的容器.

    在这里插入图片描述

    • 在拷贝过程中,读操作,都仍然读取旧版本的内容.写操作,则是在新版本的内容上修改.
    • 这个修改引用指向的操作,本身是原子的,即使在这个过程中,存在大量的读操作,读取内容.此时,仍然能够确保读到的数据是有效的(读到的数据要么是旧版本的数据,要么是新版本数据,不会是一个"修改了一半"的数据)

    写实拷贝的缺点:

    1. 无法应对多个线程同时修改的情况
    2. 如果涉及到的数据量很大,拷贝起来就非常慢.(占用内存较多)
    3. 新写入的数据不能被第一时间读取到.

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

多线程环境使用队列

  1. ArrayBlockingQueue: 基于数组实现的阻塞队列
    在这里插入图片描述

  2. LinkedBlockingQueue: 基于链表实现的阻塞队列
    在这里插入图片描述

  3. PriorityBlockingQueue: 基于堆实现的带优先级的阻塞队列
    在这里插入图片描述

  4. TransferQueue: 最多只包含一个元素的阻塞队列
    在这里插入图片描述

多线程环境使用哈希表

Hashtable

Hashtable只是简单的把关键方法加上了synchronized关键字.
在这里插入图片描述
在这里插入图片描述
这相当于直接针对Hashtable对象本身加锁.

这样做有几个缺点:

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

ConcurrentHashMap

Hashtable虽然是可选项,但是我们更推荐使用ConcurrentHashMap.
ConcurrentHashMap相比于HashMap和Hashtable来说,它的改进力度非常大.

这个部分的内容非常重要,可以认为是整个Java方向,高频面试题,能排到top5的问题.

  1. ConcurrentHashMap优化了锁的粒度[最核心]
    Hashtable的加锁,就是直接给put/get等方法加上synchronized,也就是给this加锁.
    给this加锁,这就意味着整个哈希表对象,就是一把锁.任何一个针对这个哈希表的操作,都会触发锁竞争.
    ConcurrentHashMap是给每个hash表中的"链表"进行加锁(多把锁).“锁桶”
    在这里插入图片描述
    上述的设定方式,既可以保证线程安全,又可以大大降低锁冲突的概率.只有同时进行两次的两次修改,恰好在修改同一个链表上的元素的时候,才会触发锁竞争.

  2. ConcurrentHashMap引入了CAS原子操作
    针对像size这样的操作,直接借助CAS完成,并不会加锁.

  3. ConcurrentHashMap针对读操作,做了特殊处理.
    上述的加锁,只是针对写操作加锁.
    对于读操作,它是通过volatile以及一些精巧的代码来实现的.确保读操作,不会读到"修改一半的数据".

  4. ConcurrentHashMap针对hash表的扩容,进行了特殊的优化.
    普通hash表扩容,需要创建新的hash表,把元素都搬过去.这一系列操作,很有肯能就在一次put中完成了,这样就会使这次put的开销特别大,耗时非常长.
    ConcurrentHashMap针对hash扩容操作,使用了"化整为零"的思想.
    它不会在一次操作中,把所有的数据搬走,而是一次只搬运一部分.此时后续的每次操作,都会触发一部分key的搬运,最终把所有的key都搬运完成.等到全部元素都搬运完成后,再把老数组删掉.
    在这里插入图片描述
    当新旧数据同时存在的时候

    1. 插入操作,直接插入到新的空间中.
    2. 查询/修改/删除操作,都是需要同时查询旧的空间和新的空间的.

这里的扩容机制,跟B+树很像,B+树有一个优点,查询的开销非常稳定.

小小的补充,在往上查一些关于ConcurrentHashMap的资料的时候,可能会见到"分段锁"这样的说法.它属于ConcurrentHashMap早期的实现方式,它与现在的锁桶,思想上是一样的,但是实现上有差别.
"分段锁"把所有的桶,分段,每一段有一个锁(一个锁,管好几个链表)

它存在几个明显的问题:

  1. 它这里降低锁冲突,做的还不够彻底.
  2. 分段锁的实现方式,更复杂

咱们很多时候学习一些"原理"类的知识,一方面是能够有更好的理解,能够更深度的使用和排查问题.
另一方面,也是学习大佬们的设计理念,等到我们后续遇到了类似的场景,这些理念都可以给我们解决问题提供一些参考.

本文到这里就结束啦~


http://www.kler.cn/a/301515.html

相关文章:

  • Unity3D 完整直升机控制器(虚拟仿真级别)
  • 从零到一:利用 AI 开发 iOS App 《震感》的编程之旅
  • ElasticSearch学习笔记二:使用Java客户端
  • 【PYTORCH】使用MTCNN和InceptionResnetV1简单进行人脸检测和相似度匹配
  • go环境搭建
  • eBPF on Go
  • Python实现Paillier同态加密算法
  • JavaWeb开发中为什么Controller里面的方法是@RequestMapping?
  • 张力控制器的原理
  • 【2023年】云计算金砖牛刀小试3
  • 《C++枚举类型的进化:从传统到现代的飞跃》
  • SAP B1 学习笔记 - 易混淆字段名(持续更新中)
  • 【c/c++】类型转换:隐式类型转换、强制类型转换
  • Vue - 关于Router路由跳转时显示的animate.css动画
  • ffmpeg使用安装使用教程(命令行-Python)
  • 后端Web之SpringBoot原理
  • Ubuntu 22.04 系统中 ROS2安装
  • 复仇时刻 华为的狙击还没结束
  • JAVA 使用POI实现单元格行合并生成
  • Spring Boot 四大请求编码模板(GET、POST、PUT、DELETE)
  • eureka服务开启之后的默认登录账号密码是什么?
  • SpringBoot项目部署+属性配置方式+多环境开发
  • 爬虫2:web请求与http
  • vue2 条形码 打印3*5的标签,打印标签
  • 【Linux】传输层协议——UDP
  • libyuv之linux编译