【JavaEE】线程安全的集合类

目录

前言

多线程环境使用ArrayList

多线程环境使用队列

多线程环境下使用哈希表

1.HashTable

2.ConcurrentHashMap

面试题

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

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

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

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


前言

在前面我们学习了一些在中java集合类,例如ArrayList、Queue、HashMap、StringBuilder等一些常见的集合类,但这些都是线程不安全的类,不能在多线程中使用。考虑多线程的情况下,我需要使用一些线程安全的类,Vector、Stack、HashTable是线程安全的类,但并不建议使用。

在多线程的情况下,建议自己加锁,或者使用一些带锁的数据结构

多线程环境使用ArrayList

ArrayList本身是一个线程不安全的集合类,在多线程的情况下,对于其读和写操作,存在着线程安全问题,因此,提出了下面几种解决方法:

  1. 自己使用同步机制(synchronized或者ReentrantLock)
  2. Collection.synchronizedList(new ArrayList);其实就是在相关方法面前进行synchronized的加锁。

synchronizedList 是标准库提供的⼀个基于synchronized进⾏线程同步的List.synchronizedList 的关键操作上都带有synchronized

  3.CopyOnWriteArrayList:即写时复刻的容器

在进行读操作时,容器不用做任何改变。当我们往容器里添加元素的时候,不会直接往当前容器里添加,而是会先对当前容器Copy复制出一个新的容器,往后往新的容器里添加元素。当添加完元素之后,再将原容器的引用指向新的容器。

我们可以查看CopyOnWriteArrayList中的add方法,我们可以看到和我们上面说的一致。

当我们想要进行写操作,会先进行拷贝,再在拷贝的数组里存放数据。

CopyOnWriteArrayList容器的优缺点

优点

  1. 读操作无需加锁:由于使用了写时拷贝策略,在读取操作的时候,可以在没有锁的情况下进行,提高了读取操作的性能。在读多写少的场景下,不需要加锁竞争。
  2. 线程安全CopyOnWriteArrayList是一个线程安全的类,在多线程环境下使用无需加锁。

缺点

  1. 内存占用大CopyOnWriteArrayList是写时复制,在进行写操作的时候,每次都是复制整个底层数组,如果数组比较大,就会导致有明显的性能开销。所以CopyOnWriteArrayList并不适合频繁写的场景。
  2. 数据一致性CopyOnWriteArrayList只能保证数据的最终一致性,不能保证数据的实时一致性。如果想要马上获取到写入的数据,是不能的。(写操作没有完成时,此时获取到的数据依旧是旧数组的数据)。

CopyOnWriteArrayList适用于读多写少且数据量适中的情况下,如果读少写多且数据量较大,不适合使用,内存开销大,可能会有性能问题。

多线程环境使用队列

队列是一种“先进先出”的数据结构,线程安全问题主要在当队列为空时读取或者满时插入的情况下。为了解决这种问题,当队列为空时读取数据或者队列为满时想插入数据时需要让执行操作的线程进入阻塞等待。在java.util.concurrent中,给我们提供了以下几种队列:

  1. ArrayBlockingQueue:基于数组实现的阻塞队列;
  2. LinkedBlockingQueue:基于链表实现的阻塞队列;
  3. PriorityBlockingQueue:基于堆实现的带优先级的阻塞队列;
  4. TransferQueue:最多只包含一个元素的阻塞队列。

多线程环境下使用哈希表

HashMap是一个线程不安全的集合类。在多线程下哈希表可以使用:

  1. HashTable
  2. ConcurrentHashMap

1.HashTable

HashTable只是给关键方法加上了synchronized关键字。这相当于直接给HashTable对象本身加锁。

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

 

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

2.ConcurrentHashMap

针对上面这种情况,ConcurrentHashMap做出来一系列的优化。

1.ConcurrentHashMap是给每个hash表中的“链表”进行加锁(分段锁),将HashTable的一个大锁转换成一个个加在哈希桶的小锁,大大降低了锁冲突。只要有两个线程访问的恰好是同一个哈希桶上的数据才会出现锁冲突

2.引入了CAS这样的原子操作,像修改size这样的操作,不会进行加锁,直接借助CAS完成即可。

3.ConcurrentHashMap对读操作没有进行加锁(使用了volatile保证从内存读取结果,确保读操作,不会读到“修改一半的数据”),只针对写操作进行加锁。 

4.优化了Hash表的扩容方式:对于普通hash扩容,需要创建一个新的Hash表,再把数据搬运过去,这一系列操作,可能就在一次put操作内完成,这会导致put开销非常大,且耗时非常长。ConcurrentHashMap则是选择“化整为零”不会在一次操作中把所有的数据都搬运过去,而是每次搬运一部分。

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

面试题

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

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

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

这个是java1.7中采用的技术,java1.8已经不再使用了,简单的来说,就是把若干个哈希桶分成一个“段”(Segment),针对每个段进行加锁。

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

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

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

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

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

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

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

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


以上就是本篇所有内容,若有不足,欢迎指正~

  • 25
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 19
    评论
评论 19
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小猪同学hy

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值