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

目录

一、多线程环境使用ArrayList

二、多线程环境使用队列

三、多线程环境使用哈希表

1、HashMap

2、Hashtable

3、ConcurrentHashMap

(1)缩小了锁的粒度

(2)充分使用了CAS原子操作,减少一些加锁

(3)针对扩容操作的一些优化(化整为零)

四、相关面试题


大部分集合类都是线程不安全的,Vector,Stack,Hashtable是线程安全的,但不建议使用,因为无论什么情况都要加锁,甚至单线程也是,这样就很不合理;并且这几个集合类官方已经不推荐使用了,可能在未来的版本中就被删掉了。

下面介绍一些线程不安全的集合类。

一、多线程环境使用ArrayList

1、自己使用同步机制(synchronized或者ReentrantLock)

2、Collections.synchronizedList(new ArrayList);

相当于给ArrayList套了个壳,ArrayList各种操作本身是不带锁的,通过上述操作套壳后,得到了新的对象,新的对象里面的关键方法都是带有锁的。

3、使用CopyOnWriteArrayList

CopyOnWrite容器即写时复制的容器,多线程对这个顺序表进行读操作时,不会有线程安全问题,但是当多线程进行写操作时,就会有线程安全问题,CopyOnWriteArrayList会复制一份原来的顺序表,并且修改新的顺序表内容,再把原来的引用指向新的顺序表(此操作是原子的,不需要加锁)。


二、多线程环境使用队列

1、自己加锁

2、使用BlockingQueue

  1. ArrayBlockingQueue

基于数组实现的阻塞队列

  1. LinkedBlockingQueue

基于链表实现的阻塞队列

  1. PriorityBlockingQueue

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

  1. TransferQueue

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


三、多线程环境使用哈希表

1、HashMap

HashMap本身就是线程不安全的。

2、Hashtable

在一些关键方法上加了锁

这也相当于对this加了锁,也就是针对Hashtable对象本身加锁,如果尝试修改Hash表中两个不同Hash值里的链表,会发生锁冲突。如图:

3、ConcurrentHashMap

相对于Hashtable,进行了些优化。

(1)缩小了锁的粒度

多线程如果修改Hash表里Hash值不同的链表都发生锁冲突,是不合理的,而且锁冲突是很耗时的,所以ConcurrentHashMap是对Hash表里每个链表都进行加锁,这样,不同的链表有不同的锁对象,多线程修改两个不同的链表,就不会发生锁冲突了,如图:

**注意:**更多的锁并不意味着要耗费更多的空间,因为在java中的任何对象都可以作为锁对象,而本身Hash表中就得有数组,数组元素都已经存在,即链表的头结点,每个链表都有一个头结点,可以直接把这个头结点作为链表的锁对象。

(2)充分使用了CAS原子操作,减少一些加锁

比如,针对Hash表元素个数的维护。

(3)针对扩容操作的一些优化(化整为零)

负载因子:描述了每个桶(Hash表)平均有多少个元素;公式:实际个数 / 数组长度(桶的个数)。0.75是默认的扩容阈值(也可以是其他数字值),如果我们算出的负载因子超过规定的扩容阈值,Hash表就会进行扩容。

进行扩容时,如果不是concurrentHashMap,会创建一个更大是数组,把旧的数组元素搬运到新的数组中,一次性的全部搬运完,如果Hash表本身的元素就非常多,这里扩容就会非常耗时,但可能过一会儿就又好了,存在不稳定因素,我们无法控制Hash表何时触发扩容。

concurrentHashMap则不是一次性的全部搬运完,而是把Hash表中的元素分为若干次搬运完,而不是直接一次性梭哈完,假设Hash表有1kw个元素,每次就只搬运5k哥元素,一共花费2k次搬运完成(搬运的时间会更长一些),但能确保每次搬运消耗的时间不会很长,避免出现很卡的情况。

总的来说,扩容是一个低频的操作(前提把扩容阈值设置合理),运行整个程序,可能一天都不会触发扩容,触发了每次可能会花费几分钟的时间进行搬运。

**注意:**在扩容过程中,存在两份Hash表,一份是新的,一份是旧的。

进行插入操作,直接往新的Hash表上插入。

进行删除操作,新的旧的都要删除。

进行查找操作,新的旧的都要查找。


四、相关面试题

1.ConcurrentHashMap的读是否要加锁,为什么?

读操作没有加锁.目的是为了进一步降低锁冲突的概率.为了保证读到刚修改的数据,搭配了volatile关键字.

2.介绍下ConcurrentHashMap的锁分段技术?

这个是Java1.7所采取的技术.Java1.8中已经不再使用了.简单的说就是把若干个哈希桶分成一个"段"(Segment),针对每个段分别加锁.

目的也是为了降低锁冲突的概率.当两个线程访问的数据恰好在同一个段上时,才会触发锁竞争

3.ConcurrentHashMap在jdk1.8做了哪些优化?

取消了分段锁,直接给每个哈希桶(每个链表)分配了一个锁(就是以每个链表的头节点对象作为锁对象).

将原来的数组 + 链表的实现方式改进成 数组 + 链表 /红黑树的方式.当链表较长的时候(大于等于8个元素)就转换成红黑树.

4.HashMap和HashTable,ConcurrentHashMap之间的区别?

HashMap: 线程不安全.key允许为null

HashTable:线程安全.使用synchronized锁HashTable对象,效率较低.key不允许设置为null.

ConcurrentHashMap: 线程安全.使用synchronized锁每个链表的头节点,锁冲突概率较低,充分利用CAS机制,优化了扩容方式.key不允许为null.


都看到这了,点个赞再走吧,谢谢谢谢谢

相关推荐
一颗花生米。10 分钟前
深入理解JavaScript 的原型继承
java·开发语言·javascript·原型模式
问道飞鱼10 分钟前
Java基础-单例模式的实现
java·开发语言·单例模式
学习使我快乐0114 分钟前
JS进阶 3——深入面向对象、原型
开发语言·前端·javascript
通信仿真实验室1 小时前
(10)MATLAB莱斯(Rician)衰落信道仿真1
开发语言·matlab
勿语&1 小时前
Element-UI Plus 暗黑主题切换及自定义主题色
开发语言·javascript·ui
ok!ko4 小时前
设计模式之原型模式(通俗易懂--代码辅助理解【Java版】)
java·设计模式·原型模式
2402_857589364 小时前
“衣依”服装销售平台:Spring Boot框架的设计与实现
java·spring boot·后端
吾爱星辰5 小时前
Kotlin 处理字符串和正则表达式(二十一)
java·开发语言·jvm·正则表达式·kotlin
ChinaDragonDreamer5 小时前
Kotlin:2.0.20 的新特性
android·开发语言·kotlin
IT良5 小时前
c#增删改查 (数据操作的基础)
开发语言·c#