0%

Java集合

⭐ArrayList

ArrayList底层是用数组实现的,数组长度可以在构造函数中指定,默认是0。

  • 调用add()方法时,如果长度不够,会触发扩容。所以在初始化时指定数量可以减少扩容次数。

    调用指定下标的add(int index, E element)方法时,也会拷贝数组。

    比如要在下标为5的位置插入一个元素,就会从下标为5这个位置到数组末尾复制出一个的数组,然后把新的数组放在5+1这个位置上,最后在5这个位置插入数据。

  • 调用remove()方法时,会拷贝出一个新数组。

    比如要删除下标为5的元素,就会从5+1这个位置到数组末尾复制出一个新数组,然后放到下标为5这个位置。

更适合读多写少的场景

扩容:初始化时如果没有指定数组长度,第一次会扩容为10。否则扩容为原来的1.5倍。

⭐LinkedList

LinkedList底层使用双向链表。

LinkedList插入操作的时间复杂度是O(1)的前提是:已经有了那个要插入节点的指针,但是在实现的时候,LinkedList需要先通过循环获取到那个节点的Node,然后再执行插入操作。所以LinkedList插入数据的效率也不一定比ArrayList高。

⭐HashMap

HashMap底层采用数组+链表组成

  • 调用put()方法时,会根据key的哈希值,计算出要添加的数组下标,如果目标位置不为空,就会创建一个链表保存出现Hash冲突的数据。
  • 调用get()方法时,根据key的哈希值,计算出目标数据的下表,如果目标位置是一个链表,则通过equals()方法与链表中的每个元素作比较。

数组的长度默认是16,插入的数据越多,就会导致Hash冲突越多,链表就越长,进而影响查询数据的效率,所以当链表长度达到8时会转换为红黑树,当删除数据后长度小于6时重新变为链表。

并且当数组中元素数量大于数组长度 * 0.75时,就会进行扩容,扩容分为两步:

  1. 创建一个空数组,长度是原来的2倍。

  2. 再遍历原来数组中的元素,重新经过Hash运算后保存到新数组中。

    因为数组长度变了,Hash规则也会变。(index = key.hashCode() & array.length - 1)

线程A、B同时插入数据,线程A通过key的哈希值计算出数组的下标,并且目标位置为空,然后线程A挂起。

线程B成功插入数据。

线程A再次执行的时候会直接覆盖掉线程B写的数据。

⭐ConcurrentHashMap

ConcurrentHashMap底层是数组+链表组成的,不过在jdk 1.7和jdk 1.8中实现方式稍有不同:

在jdk 1.7中,是基于分段锁实现的,ConcurrentHashMap会把内部的数据拆分成一个个Segment,Segment继承自ReentrantLock,并且内部维护了一个HashEntry数组,HashEntry是key-value结构的对象,内部使用volatile关键字来保证可见性。

  • 调用put()方法时,通过key的哈希值得到该元素要添加到的Segment,然后对Segment进行加锁,不会影响到其它Segment。

    默认情况下,Segment的数量是16个,也就是说最多可以支持16个线程同时读写。

  • 调用get()方法时,只需要根据key的哈希值就可以定位到Segment,再通过key的哈希值就可以定位到具体的元素。

    第一次通过key的哈希值与Segment数组长度计算出key所在的Segment,第二次通过key的哈希值与HashEntry数组长度定位到具体的元素。

ConcurrentHashMap是通过链表解决Hash冲突的,会导致查询效率降低。

在jdk 1.8中,抛弃了分段锁,采用了CAS + synchronized来保证并发安全性,并且在解决Hash冲突时引入了红黑树,当链表长度大于8时会转为红黑树。

  • 调用put()方法时,根据key的哈希值定位到要添加的位置,如果不为空就用synchronized对当前Node加锁并写入数据。如果目标位置为空就通过CAS写入,如果CAS写入失败则重新判断位置是否为空,直到成功将数据写入。
  • 调用get()方法时,根据key的哈希值定位到具体的数据。

抛弃分段锁的原因

  1. 分段锁会浪费更多的内存空间。
  2. jdk8只会对Node加锁,降低了锁粒度。

⭐HashSet

HashSet内部维护了一个HashMap,所有元素都存到map中的key上,所以HashSet天然就是不可重复的。

map中的value是一个统一的值,是用static final修饰的Object对象

  • 调用add()方法时,底层调用的是HashMap的put()方法。

HashSet没有提供get()方法,获取数据时需要通过迭代器遍历集合。

CopyOnWrite模式

CopyOnWrite模式就是在执行写操作的时候,会把数据复制出来一份,这样做的好处是读数据不需要加锁。

CopyOnWrite模式内部维护了一个数组,所有的读操作都是在这个数组上进行的。如果在遍历的过程中,其它线程执行写操作,会复制出一个新的数组,然后在新的数组上执行写操作,最后再把原来的数组替换掉。

只适合读多写少的场景

常用的实现类有:CopyOnWriteArrayList、CopyOnWriteArraySet。