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

月临水 2024-09-30 13:35:01 阅读 56

文章目录

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


线程安全的集合类

之前学习的集合类大部分都不是线程安全的.

比如ArrayList,Queue,HashMap等等,这都是线程不安全的.

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

多线程环境使用ArrayList

自己加锁

使用标准库中提供的带锁的List,Collections.synchronizedList(new ArrayList);

synchronizedList就相当于构造出了一个核心方法,自带synchronized的List.

在这里插入图片描述

使用CopyOnWriteArrayList

这个集合类,没有加锁,但是通过"写实拷贝"来实现的线程安全.通过写时拷贝,避免两个线程同时修改一个变量.

在这里插入图片描述

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

在这里插入图片描述

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

写实拷贝的缺点:

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

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

多线程环境使用队列

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

在这里插入图片描述

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

在这里插入图片描述

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

在这里插入图片描述

TransferQueue: 最多只包含一个元素的阻塞队列

在这里插入图片描述

多线程环境使用哈希表

Hashtable

Hashtable只是简单的把关键方法加上了synchronized关键字.

在这里插入图片描述

在这里插入图片描述

这相当于直接针对Hashtable对象本身加锁.

这样做有几个缺点:

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

ConcurrentHashMap

Hashtable虽然是可选项,但是我们更推荐使用ConcurrentHashMap.

ConcurrentHashMap相比于HashMap和Hashtable来说,它的改进力度非常大.

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

ConcurrentHashMap优化了锁的粒度[最核心]

Hashtable的加锁,就是直接给put/get等方法加上synchronized,也就是给this加锁.

给this加锁,这就意味着整个哈希表对象,就是一把锁.任何一个针对这个哈希表的操作,都会触发锁竞争.

ConcurrentHashMap是给每个hash表中的"链表"进行加锁(多把锁).“锁桶”

在这里插入图片描述

上述的设定方式,既可以保证线程安全,又可以大大降低锁冲突的概率.只有同时进行两次的两次修改,恰好在修改同一个链表上的元素的时候,才会触发锁竞争.

ConcurrentHashMap引入了CAS原子操作

针对像size这样的操作,直接借助CAS完成,并不会加锁.

ConcurrentHashMap针对读操作,做了特殊处理.

上述的加锁,只是针对写操作加锁.

对于读操作,它是通过volatile以及一些精巧的代码来实现的.确保读操作,不会读到"修改一半的数据".

ConcurrentHashMap针对hash表的扩容,进行了特殊的优化.

普通hash表扩容,需要创建新的hash表,把元素都搬过去.这一系列操作,很有肯能就在一次put中完成了,这样就会使这次put的开销特别大,耗时非常长.

ConcurrentHashMap针对hash扩容操作,使用了"化整为零"的思想.

它不会在一次操作中,把所有的数据搬走,而是一次只搬运一部分.此时后续的每次操作,都会触发一部分key的搬运,最终把所有的key都搬运完成.等到全部元素都搬运完成后,再把老数组删掉.

在这里插入图片描述

当新旧数据同时存在的时候

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

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

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

"分段锁"把所有的桶,分段,每一段有一个锁(一个锁,管好几个链表)

它存在几个明显的问题:

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

咱们很多时候学习一些"原理"类的知识,一方面是能够有更好的理解,能够更深度的使用和排查问题.

另一方面,也是学习大佬们的设计理念,等到我们后续遇到了类似的场景,这些理念都可以给我们解决问题提供一些参考.

本文到这里就结束啦~

在这里插入图片描述



声明

本文内容仅代表作者观点,或转载于其他网站,本站不以此文作为商业用途
如有涉及侵权,请联系本站进行删除
转载本站原创文章,请注明来源及作者。