【JaveEE】——多线程中使用顺序表,队列,哈希表

简介: 多线程环境下使用ArrayList(同步机制,写时拷贝),使用队列,哈希表(高频)ConcurrentHashMap(缩小锁粒度,CAS,扩容优化)


image.gif 编辑

阿华代码,不是逆风,就是我疯

你们的点赞收藏是我前进最大的动力!!

希望本文内容能够帮助到你!!

目录

一:多线程环境使用ArrayList

引入:

1:顺序表使用同步机制

2:套壳封装

3:写时拷贝

(1)添加/修改元素操作

(2)优点

(3)缺点

(4)使用场景

二:多线程环境使用队列

三:多线程环境使用哈希表(面试高频)

引入

1:ConcurrentHashMap

(1)缩小锁粒度

(2)使用CAS原子操作

(3)扩容的优化


一:多线程环境使用ArrayList

引入:

原来的集合类,大部分都是线程不安全的,但是有几个例外:Vector,Stack,HashTable(这几个类)但是现在官方已经不太推荐使用了,后续可能会删掉——因为哪怕是在单线程下也要加锁,这种情况不合理(往下看)

在这些类内部中,把一些关键的方法都加锁了,导致它们不仅在多线程场景下要加锁,而且在单线程场景下也要加锁。虽然JVM中有“锁消除”机制,但这也不是万能的,加锁带来的资源消耗依旧是不可忽视的(单线程下就没必要加锁了嘛)

1:顺序表使用同步机制

使用synchronized和ReentrantLock进行加锁,上一篇文章有提及两者的区别,往回翻翻~~

2:套壳封装

使用Collections.synchronizedList(new ArrayList)

因为ArrayList本身各种操作都是不带锁的,我们把它作为参数传入,相当于给ArrayList封装一下,套入Collections.synchronizedList()这个壳中,得到一个新的对象,这个新的对象调用关键的方法操作都是带有锁的

3:写时拷贝

使用CopyOnWriteArrayList

(1)添加/修改元素操作

如果我们往一个容器里面添加元素,我们不往这个容器中添加,而是先copy一份新容器,往新的容容里面添加或者修改元素

添加修改完元素之后,在将引用指向新的容器

(2)优点

①可以进行并发读在“读多写少”的场景下效率非常高(在引用指向新的容器之前,读操作都可以在旧容器上完成)

(3)缺点

①相应的顺序表如果太大,copy的开销也变高了

②“写操作”非常频繁,copy的频率就会非常高,资源的消耗和占用就比较严重

③不能第一时间读到新写的数据

(4)使用场景

服务器加载配置文件的时候,就会把文件内容解析出来放到内存的数据结构中,配置文件体积小,而且修改频率低

二:多线程环境使用队列

这边以前的文章有总结过就不再加以详述

主要以自己加锁和使用BlockingQueue为主

三:多线程环境使用哈希表(面试高频)

引入

在多线程环境下,Hashtbale是线程安全的,因为在Hashtable内部的关键方法中都有进行synchronized加锁操作。

但是HashMap就不行了,在数组的基础上还涉及到链表和树化

1:ConcurrentHashMap

于是我们就加以改进,引入了ConcurrentHashMap(并发HashMap),以下是我们的改进过程

(1)缩小锁粒度

①HashMap中的加锁

当我们尝试对HashMap中的不同链表下的不同元素进行修改操作的时候,就会触发锁竞争

因为这个锁是针对整个HashMap(this)而言的

如下图中我们想修改元素1和元素2,就会触发锁竞争,

重点:

但实际上修改不同链表上的元素操作,并不会触发线程安全问题(加大了加锁的频率,资源浪费)

只有在修改同一个链表下(相邻必触发:因为操作会涉及到同一个引用)元素可能会触发线程安全问题

于是我们在ConcurrentHashMap中进行了优化

image.gif 编辑

②ConcurrentHashMap中的加锁

锁对象为每个数组中的元素(链表的头结点),此时如果修改同一个链表下的元素,就会触发锁竞争。

理解:相当于把一个大锁拆分成了好多把小锁(这就是缩小锁粒度)

优点:不仅解决了线程安全问题,还降低了加锁的频率,节约了资源

注:这里的锁的数量虽然很多,但并不会增加太多的资源消耗,因为加锁对象(头结点)是现成的,不需要我们再去创建了

image.gif 编辑

(2)使用CAS原子操作

在ConcurrentHashMap中,比如针对哈希表中的元素个数的维护,我们使用CAS就可以减少一些加锁。

用synchronized加锁,咱们不知道加锁处于那种阶段(程度)的加锁——可能是偏向锁,轻量级化加锁,甚至是最后升级为重量级化加锁,这件事都是不可预估的

(3)扩容的优化

负载因子=元素个数/数组长度  ,0.75是一个扩容阈值指标

①HashMap扩容机制

如果数组元素个数太多会进行扩容,链表下元素个数太多会进行树化

扩容:创建一个更大数组,把旧Hash表上的元素一下子搬过去(一把梭哈),如果元素数量非常多,这里的copy操作就会非常的耗费时间,实际表现就是突然间某个操作非常卡

②ConcurrentHashMap扩容机制

扩容时,每次只搬运一部分元素,随着每次的插入/删除/添加/查找操作,都会搬运一部分元素。

内部机制:扩容时,有两份哈希表

插入操作——往新表上插

删除操作——新表旧表都删

查找操作——新表旧表都查

优点:确保每次操作耗费的时间都不长,避免出现卡顿的情况

缺点:整体扩容的时间变长了


相关文章
|
1月前
|
缓存 安全 C++
C++无锁队列:解锁多线程编程新境界
【10月更文挑战第27天】
68 7
|
1月前
|
消息中间件 存储 安全
|
2月前
|
存储 运维 API
源码解密协程队列和线程队列的实现原理(一)
源码解密协程队列和线程队列的实现原理(一)
44 1
|
2月前
|
存储 安全 API
源码解密协程队列和线程队列的实现原理(二)
源码解密协程队列和线程队列的实现原理(二)
37 1
|
4月前
|
存储 监控 Java
|
4月前
|
数据采集 Java Python
python 递归锁、信号量、事件、线程队列、进程池和线程池、回调函数、定时器
python 递归锁、信号量、事件、线程队列、进程池和线程池、回调函数、定时器
|
5月前
|
设计模式 安全 Java
Java面试题:请解释Java中的线程池以及为什么要使用线程池?请解释Java中的内存模型以及如何避免内存泄漏?请解释Java中的并发工具包以及如何实现一个简单的线程安全队列?
Java面试题:请解释Java中的线程池以及为什么要使用线程池?请解释Java中的内存模型以及如何避免内存泄漏?请解释Java中的并发工具包以及如何实现一个简单的线程安全队列?
49 1
|
5月前
|
设计模式 安全 NoSQL
Java面试题:设计一个线程安全的单例模式,并解释其内存占用和垃圾回收机制;使用生产者消费者模式实现一个并发安全的队列;设计一个支持高并发的分布式锁
Java面试题:设计一个线程安全的单例模式,并解释其内存占用和垃圾回收机制;使用生产者消费者模式实现一个并发安全的队列;设计一个支持高并发的分布式锁
74 0
|
7月前
|
设计模式 安全 Java
Java 多线程系列Ⅳ(单例模式+阻塞式队列+定时器+线程池)
Java 多线程系列Ⅳ(单例模式+阻塞式队列+定时器+线程池)
|
5天前
|
NoSQL Redis
单线程传奇Redis,为何引入多线程?
Redis 4.0 引入多线程支持,主要用于后台对象删除、处理阻塞命令和网络 I/O 等操作,以提高并发性和性能。尽管如此,Redis 仍保留单线程执行模型处理客户端请求,确保高效性和简单性。多线程仅用于优化后台任务,如异步删除过期对象和分担读写操作,从而提升整体性能。
17 1