常用线程安全类型(ArrayList、LinkedList、CopyOnWriteArrayList、HashMap、LinkedHashMap、ConcurrentHashMap)

JDK基础数据类型与集合类

最基础的类型分位三类:

  • 原生类型
  • 数组类型
  • 对象引用类型

基于这几种基础类型的不同嵌套,在java.util的工具包里又构建出了很多不同种类、不同形态、不同作用的一些集合类:

  • 线性数据结构
    • List:ArrayList、LinkedList、Vector、Stack
    • Set:LinkedSet、HashSet、TreeSet
    • Queue:Deque->LinkedList
  • Map:HashMap、LinkedHashMap、TreeMap
    • Dictionary->HashTable->properties

ArrayList(非线程安全)

基本特点:

基于数组,便于按index访问,超过数组需要扩容,扩容成本较高。

补充:

  • 下标访问,速度比较快。
  • 为什么要扩容,因为他是数组,数组创建是需要指定大小的。
  • 修改比较复杂:当我们往中间或者头部插入元素时,需要把后面所有的元素都往后挨个挪动一个位置。

用途:

大部分情况下操作一组数据都可以用ArrayList

原理:

使用数组模拟列表,默认大小10,扩容x1.5,newCapacity = oldCapacity + (oldCapacity >> 1)

内部实现是数组:transient Object[] elementData;

安全问题:

  • 写冲突
    • 两个写,相互操作冲突
  • 读写冲突
    • 读,特别是iterator的时候,数据个数变了,拿到了非预期数据或者报错
    • 产生ConcurrentModificationException

LinkedList(非线程安全)

基本特点:

使用链表实现,无需扩容。

补充:

  • 因为是链表,不是数组,没有大小限制
  • 修改比较容易:因为有指针,都是每个指向下一个。中间插数据时,只需要修改对应指针,不需要移动任何严肃其他元素。
  • 双向链表

用途:

不知道容量,插入变动多的情况

原理:

使用双向指针将所有节点连起来

内部实现transient Node<E> first;

什么是Node:

安全问题:

  • 写冲突
    • 两个写,相互操作冲突
  • 读写冲突
    • 读,特别是iterator的时候,数据个数变了,拿到了非预期数据或者报错
    • 产生ConcurrentModificationException

List线程安全的简单办法

既然线程安全是写冲突和读写冲突导致的,最简单的办法就是读写都加锁。

例如:

  • ArrayList的方法上都加上synchronized -> Vector
  • Collections.synchronizedList,强制将List的操作加上同步
  • Arrays.asList,不允许添加删除,但是可以set替换元素。(不可以修改元素个数)
  • Collections.unmodifiableList,不允许修改内容,包括添加删除和set。

CopyOnWriteArrayList(类似快照,读写分离)

在List上加synchronized缺点:

相当于加了一把很大的锁,作用在了所有的get、set方法上。谁先抢到读,谁先抢到写,是无法预知的,导致最终结果也是一个不可预知的结果。

核心改进原理(既保证线程安全,又能正常进行并发操作):

  • 写加锁,保证不会写混乱
  • 写在一个Copy副本上,而不是原始数据上(GC young区用复制,old区用本区内的移动
  • 读不加锁,并发读,因为用的是老的快照数组
  • 使用迭代器的时候,也是拿到老的快照数组来做操作。此后有List的元素变动,就跟这次迭代没关系了。

适用场景:

读比较频繁,写的少。(因为读一直都是在并发执行的,而且读写分离不会出现读的数据混乱)

想想:

淘宝商品的item快照。商品价格会变,每次下单都会生成一个当时商品信息的快照。

所以快照是我们并发、程序优化、业务优化的一个非常重要的手段。

可以参照CopyOnWriteArrayList的读写实现的源码,理解一下快照的使用精髓,以及他是如何避免并发冲突的。

HashMap

基本特点:

数组+链表,空间换时间,哈希冲突不大的情况下查找数据性能很高。

用途:

存放指定key的对象,缓存的对象。

原理:

使用hash原理,存K-V数据,初始容量16,扩容x2,负载因子0.75

JDK8以后,在链表长度到8&数组长度到64时,使用红黑树

安全问题:

  • 写冲突
  • 读写问题,可能会死循环
  • Keys()无序问题

补充:扩容会导致一部分数据之前槽对应的entry移动到其他的槽(因为取余)。

  • 由于链表的变化,又有其他的写入和读取操作时,就有可能导致死循环
  • 从而扩容会导致乱序

补充:

  • 获取数据:

当获取其中某个key对应的value时,先通过hashcode取模,获取到对应数组元素位置,进而拿到链表。再循环这条链表(链表里放的是entry,entry里放的就是key-value),比较每个key的值跟我们要的key的值是否一样,就把对应的entry拿出来。那里面就有他的value。

  • 哈希冲突:

尽量不要把hashmap装满了,否则就会导致哈希冲突,影响性能。

  • 负载因子:
    • 槽的数量(数组长度)与元素的数量的比例。决定是否扩容。(当超过3/4的槽已经有数据时,就需要扩容了,因为在不扩容的话就要充满了。当有一大半的槽都有数据时,这时候随机来个数据,大概率就会命中当前已有数据的这些槽,导致会让对应槽的链表边长。)
    • 一般情况下,负载因子越低,哈希冲突的情况就会越小。但是反过来,负载因子越低,也就会导致大部分时间内很多空闲的槽没有被使用上,从而浪费内存空间。
      • 太小:太小性能很好,但空间浪费多
      • 太大,空间浪费没那么严重,但是性能就下降了。
  • 初始容量:
    • 太大:可能只放少量数据,导致空间浪费
    • 太小:导致扩容频繁
  • 扩容因子:
    • 太大:空间浪费
    • 太小:频繁扩容
  • 复杂度(与红黑树比较):
    • 数组+链表:N(想从链表中去拿key,需要循环一遍当前链表)
    • 红黑树:logN
    • 因为红黑树本身的维护、构建、平衡等操作的成本比较大,所以要在数组+链表达到一定规模时启用。
      • 类比数组的Sort排序操作,少量数据,采用冒泡算法;超过固定数据,采用快排。

LinkedHashMap

基本特点:

继承自HashMap,对entry集合添加了一个双向链表。

用途:

保证有序,特别是Java8 stream操作的的toMap时使用

原理:

同LinkedList,包括插入顺序和访问顺序

安全问题:

同HashMap

ConcurrentHashMap-Java7 分段锁

分段锁:(每个段一个小锁)

默认16个Segment,降低锁粒度。 concurrentLevel = 16

想想:

Segment[]  ~  分库

HashEntry[] ~ 分表

ConcurrentHashMap-Java8

去掉了Segment

  • 红黑树本身就可以让不同的线程去操作不同的树的分支
  • CAS乐观锁 - 无锁技术

比对:

  • Java 7为实现并行访问,引入了Segment这一结构,实现了分段锁。理论上最大并发度与Segment个数相等。
  • Java 8进一步提高并发性,摒弃了分段锁的方案,而是直接使用了一个大的数组。

并发集合类总结

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值