集合

132 阅读7分钟

集合

List
  1. ArrayList实现List接口,有序集合,可重复,线程不安全,底层是通过数组实现的,是一块连续的内存空间,通过索引可以快速定位数组下标,查询快,数组长度一旦确定,不能修改,所以增删操作需要创建新的数组,比较慢;
  2. Vector实现List接口,有序集合,可重复,底层通过数组实现,线程安全的,某一时刻只有一个线程能写Vector
  3. LinkedList底层通过双向链表实现,链式结构存储元素,增删快,查询慢,线程不安全,每个节点用静态内部类Node表示,Next指向下一个节点,Prev指向上一个,专门提供了头尾的操作,可用于队列
Set
  1. HashSet实现了Set接口,但底层是通过来HashMap实现的,HashSet会把元素存储在HashMap的Key集合里,所以不允许重复,本质是通过Equals()和HashCode()来判断的,存取都比较快,不保证存储元素的顺序
  2. TreeSet有序不重复,Key不能为空,Value可以为空,使用二叉树的原理对新增的对象按照指定的顺序排序
  3. LinkedHashSet继承于HashSet,又基于LinkedHashMap来实现的,底层使用LinkedHashMap来保存元素,它所有的方法操作上跟HashSet相同
Map

HashMap

  1. HashMap是一个线程不安全的集合
  2. 内部使用数组+链表+红黑树的结构来存储数据
  3. 所以他融合了数组和链表的一些特点,查询快,增删也快
  4. HashMap真正存放数据的是一个静态内部类Node,有四个变量
static class Node<K,V> implements Map.Entry<K,V> {
    final int hash;  
    final K key;
    V val;
    Node<K,V> next; // 指向下一个元素
  1. 还有几个重要的参数,初始容量大小默认16(经验值,只要是2的N次幂都行),负载因子0.75,转化红黑树的阈值8

  2. 总的来说,HashMap默认使用数组和单链表来存储数据,Hash冲突的时候,会把数据存到链表里,但是链表的长度不能一直增加,因为查询的时间复杂度取决于链表的长度,所以当冲突链表达到8个,并且数组长度大于64的时候,会把链表转换成红黑树,这是一种会通过旋转和变色来达到自平衡状态的二叉查找树,查询和修改的时间复杂度都是O(logN),性能比较均衡,如果没有达到转换红黑树的条件,就进行扩容操作,在1.7中扩容是表头插入,会改变链表中元素顺序,并发可能会导致链表成环的问题;所以在1.8中改成了尾部插入法,保持链表顺序,避免链表成环。

  3. 存储过程

    1. 首先判断数组是否为空,如果为空,调用Resize方法进行第一次扩容,并且初始化数组

    2. 然后通过数组长度-1跟Key的Hash做与运算,得到元素存储的下标

    3. 如果当前位置没有元素,直接插入

    4. 然后判断一下链表长度是否超过8,并且数组的长度是否超过64,超过转换成红黑树

    5. 如果当前位置有元素了,说明Hash冲突

      1. Equas方法判断Key的值是否相等,相等,直接覆盖
      2. 不相等,就根据链表或者红黑树的方式去添加元素
    6. 最后数据插入完成,再判断一次是否扩容

1657076223637.jpg

  1. Resize扩容

    1. 当元素个数超过数组长度的0.75倍,就会进行扩容

    2. 扩容分两步,创建一个新的空数组,长度是原来的2倍

    3. 遍历原数组,把所有的节点ReHash到新数组,非常消耗性能

      1. 元素放进Map的时候,确认下标的方法是数组长度-1跟Key的Hash做与运算(充分散列,减少Hash冲突),现在数组长度变了,运算的结果也就不一样了,需要重新算

ConcurrentHashMap

  1. ConcurrentHashMap是HashMap的线程安全版本
  2. 内部使用数组+链表+红黑树的结构来存储数据
  3. 所以他融合了数组和链表的一些特点,查询快,增删也快
  4. ConcurrentHashMap真正存放数据的是一个静态内部类Node,有四个变量
static class Node<K,V> implements Map.Entry<K,V> {
    final int hash;  
    final K key;
    volatile V val;
    volatile Node<K,V> next;
// volatile 修饰的value,和next(指向下一个元素) 保证多线程操作时,变量修改可见性
  1. 还有几个重要的参数,初始容量大小默认16,负载因子0.75,转化红黑树的阈值8,还有控制初始化和扩容的sizeCtl

  2. 总的来说,ConcurrentHashMap默认使用数组和单链表来存储数据,Hash冲突的时候,会把数据存到链表里,但是链表的长度不能一直增加,因为查询的时间复杂度取决于链表的长度,所以当冲突链表达到8个,并且数组长度大于64的时候,会把链表转换成红黑树,这是一种会通过旋转和变色来达到自平衡状态的二叉查找树,查询和修改的时间复杂度都是O(logN),性能比较均衡,如果没有达到转换红黑树的条件,就进行扩容操作,在1.7中扩容是表头插入,会改变链表中元素顺序,并发可能会导致链表成环的问题;所以在1.8中改成了尾部插入法,保持链表顺序,避免链表成环。

  3. 存储过程

    1. 首先判断参数是否合法,key或value不能为null
    2. 然后遍历Table,如果Table为空,就初始化
    3. 然后通过数组长度-1跟Key的Hash做与运算,得到元素存储的下标
    4. 如果这个位置没有值,就通过CAS方式插入
    5. 如果遇到表连接点ForwardingNode(用于指向下一个Table),说明有其他线程正在扩容,则调用HelpTransfer方法协助扩容,ConcurrentHashMap支持多个线程同时扩容,整个扩容过程,通过CAS设置sizeCtl,transferIndex扩容索引等变量来协调多个线程扩容的操作
    6. 如果这个位置存在节点,说明Hash冲突,用Synchronized上锁,接着判断这个节点的类型
    7. 如果是链表,就用链表的方式插入,hash相同,equals也相同,就覆盖value,不相同就插入到链表的尾部
    8. 如果是树节点,就按树的方式插入值putTreeVal
    9. 如果加入这个节点之后链表的长度大于8,数组长度大于64,就转换成红黑树
    10. 最后插入完成,再判断一次是否扩容
  4. JDK1.8中取消了Segment分段锁,采用 CAS+ Synchronized 来保证并发安全,Synchronized 只锁定当前链表或红黑树的首节点,锁粒度比Segment小,并发效率更高。

    1. Segment继承了可重入锁ReentrantLock,每个锁控制的是一段,当操作不同Segment的时候可以并发执行,操作同个的时候要竞争和等待,当每个Segment越来越大的时候,锁的粒度也就变大了,性能会降低。
    2. CAS,Compare And Swap,比较与替换,是一种乐观锁,有三个参数,分别是目标内存地址,旧值,新值,它会拿旧值跟内存的值去比较,如果相等,就把新值更新到内存里,不相等,就继续循环,直到操作成功为止。线程不会阻塞,减少线程上下文切换,提高性能,是一种无锁化修改值的操作,但是如果自旋时间过长,会消耗CPU,而且还会有ABA问题,CAS 会认为值没有发生过变化,并发包下有个原子类,可以通过控制变量值的版本号来解决这个问题。
  5. 查询过程:给定一个key来确定value的时候,必须满足两个条件 key相同hash值相同,对于节点可能在链表或树上的情况,需要分别去查找

  6. private transient volatile int sizeCtl (transient不会被序列化) 用来控制table的初始化和扩容操作

    1. 正数或0表示还没有初始化
    2. -1代表Table正在初始化
    3. -N表示有N-1个线程正在进行扩容操作