当前位置:首页 » 《随便一记》 » 正文

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

20 人参与  2024年09月14日 19:21  分类 : 《随便一记》  评论

点击全文阅读


文章目录

线程安全的集合类多线程环境使用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早期的实现方式,它与现在的锁桶,思想上是一样的,但是实现上有差别.
"分段锁"把所有的桶,分段,每一段有一个锁(一个锁,管好几个链表)

它存在几个明显的问题:

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

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

本文到这里就结束啦~


点击全文阅读


本文链接:http://zhangshiyu.com/post/160026.html

<< 上一篇 下一篇 >>

  • 评论(0)
  • 赞助本站

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。

关于我们 | 我要投稿 | 免责申明

Copyright © 2020-2022 ZhangShiYu.com Rights Reserved.豫ICP备2022013469号-1