JavaEE:多线程进阶(线程安全的集合类)
文章目录
- 线程安全的集合类
- 多线程环境使用ArrayList
- 多线程环境使用队列
- 多线程环境使用哈希表
- Hashtable
- ConcurrentHashMap
线程安全的集合类
之前学习的集合类大部分都不是线程安全的.
比如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早期的实现方式,它与现在的锁桶,思想上是一样的,但是实现上有差别.
"分段锁"把所有的桶,分段,每一段有一个锁(一个锁,管好几个链表)
它存在几个明显的问题:
- 它这里降低锁冲突,做的还不够彻底.
- 分段锁的实现方式,更复杂
咱们很多时候学习一些"原理"类的知识,一方面是能够有更好的理解,能够更深度的使用和排查问题.
另一方面,也是学习大佬们的设计理念,等到我们后续遇到了类似的场景,这些理念都可以给我们解决问题提供一些参考.
本文到这里就结束啦~