LinkedList
- 数据结构
- LinkedList实现了List接口和Deque接口的双向链表。
- LinkedList底层的链表结构是它支持高效的插入和删除操作,具有队列特性。
- 是线程不安全的,可以通过调用静态方法Collections类中的synchronizedList方法,变成线程安全的。
List list = Collections.synchronizedList(new LinkedList(...));
ArrayList
- 数据结构
- 底层是动态数组(Object[]),可扩容,其容量能动态增长。
- 实现了RandomAccess接口,可支持快速随机访问,可通过元素序号快速获取元素对象
- 实现了Cloneable接口,即覆盖了clone(),能被克隆;
- 实现了Serializable接口,则支持序列化,能通过序列化去传输。
- 内存空间占用角度分析:ArrayList的空间主要体现在会预留一定的容量空间;LinkedList空间主要是每个元素需要占用更多的空间(存储前驱节点和后继节点,和元素数据)
- 扩容机制
- 注:modCount的作用是迭代器在遍历时,做线程安全检查的。
- add()方法,添加一个元素到集合的最后;
- ensureCapacityInternal(); 判断是否需要扩容;
- calculateCapacity();获取当前集合的最小长度;为空,则取默认长度10;
- ensureExplicityCapacity();判断当前集合长度+1后长度a,与当前集合对应数组长度b比较;a>b,则调用grow()进行扩容。否则将元素添加进来。modCount++;
- grow();扩容;在原集合长度基础上加上,右移一位数值,其相当于扩容到原来的1.5倍;
- hugeCapacity();如果扩容后的集合长度,大于最大的数组长度分配值 (Integer.MAX_VALUE - 8),则,大容量扩容到Integer.MAX_VALUE长度。
- Arrays.copyOf();底层是调用System.arraycopy()方法。复制原有数组值,到新建的一个扩容后的数组中。
HashMap
- JDK1.8之前
- 由 Node数组 + 链表组成。数组是HashMap的主体,链表则主要是为了解决哈希冲突而存在的;
- HashMap扩容:
- 默认情况下数组大小是16,当数组中元素超过数组大小size*loadFactor时,就会进行数组扩容;
- 扩容是在原数组基础上,扩大一倍,然后重新计算每个元素在数组中位置,是一个比较耗性能的操作。
- put插入值时,插入到链表的头部(会出现环形死循环);
- 扩容时,会颠倒链表的顺序;
- 拉链法解决冲突:创建一个数组链表,数组中元素是一个链表,若遇到哈希冲突,则将冲突的值加到链表中即可。
- 多线程下rehash时,会出现死循环。主要原因是在于进行扩容时,会将数据从旧的Hash表迁移到新的Hash表中,同时会调转数组中链表的顺序。
- JDK1.8之后
- 数据结构
- 由Node数组 + 链表/红黑树组成;线程不安全;键和值都允许为空;
- 满足以下2个条件,则会执行链表转红黑树处理,以此来加快搜索速度:
- 链表长度大于阈值(默认是8);
- HashMap数组长度超过64;
- 当数组容量未达到64时,以2倍进行扩容;
- put插入元素时,使用尾插法方式,将值插入到链表的尾部;且扩容时,链表顺序不会改变。
- 线程不安全的原因:
- 多个线程put时,get的值可能不一致,put的操作不是原子性的;
- 当多个线程put值时,假如同时去获取同一位置上链表的头节点,当线程A写入新的头节点后,线程B写入新的头节点;这样就导致B的写入操作会覆盖A的写入操作。
- 删除键值对时,会删除刚刚修改位置的元素。
- 多个线程put时,get的值可能不一致,put的操作不是原子性的;
- 数据结构
ConcurrentHashMap
java.util.concurrent并发包中线程安全的集合,减少锁的竞争,提高并发率,键和值不能为空。
- JDK1.7版本
- 数据结构:
- Segment数组+HashEntry数组+链表实现。使用分段锁;
- 每个Segment上同时只有一个线程可操作,可扩容,冲突会转化为链表,但Segment的个数一旦初始化就不能改变,默认是16个,也就是默认最多支持16个线程并发。
- 同一个Segment操作是同步,不同的Segment操作是异步。
- put(key,value)方法处理流程:
- 计算key的hash值,获取指定位置的Segment;
- 指定位置为空,则初始化新的Segment;初始化Segment流程如下(CAS):
- 检查计算得到的位置Segment是否为null;
- 是,则使用Segment[0]的容量和负载因子创建一个HashEntry数组;
- 再次检查计算得到的指定位置的Segment是否为null;
- 使用创建的HashEntry数组初始化这个Segment;
- 自旋判断计算得到的指定位置的Segment是否为null;是则使用CAS(比较并赋值)在这个位置为Segment赋值。
- 执行Segment.put()方法插入HashEntry的key.value值:
- Segment类继承了ReentrantLock类;
- 通过ReentrantLock.tryLock()方法获取锁,获取不到时用scanAndLockForPut方法继续获取;
- 计算put的数据要放入的index位置,然后获取这个位置上的HashEntry;
- 遍历获取到的HashEntry数组。
- 如果这个位置上的HashEntry不存在:
- 如果当前容量大于扩容阈值,小于最大容量,进行扩容;
- 直接头插法插入;
- 如果这个位置上HashEntry存在:
- 判断链表当前元素key和hash值是否和要put的key和hash值一致,一致则替换值;
- 不一致,获取链表下一个节点,直到发现相同进行值替换。否则插入。
- 如果当前容量大于扩容阈值,小于最大容量,进行扩容;
- 直接链表头插法插入。
- 如果这个位置上的HashEntry不存在:
- 如果要插入的位置之前已存在,替换后返回旧值,否则返回null。
- 扩容rehash
- 扩容只会扩容到原来的2倍,老数组中的数据移动到新的数组中。待插入的HashEntry对象,会在扩容后使用【链表头插法】插入到指定位置。
- 其不会对整个容器进行扩容,而只是对某个Segment 进行扩容;
- get(key)方法
- 计算key的hash值,确定Segment位置;
- 遍历Segment中HashEntry数组中,查找出相同的key的value值;
- 数据结构:
- JDK1.8版本
- 数据结构:
- 底层是Node数组 + 链表/红黑树实现;
- 采用CAS(比较和交换)和synchronized同步锁来保证并发安全,当冲突链表达到一定长度时,链表会转换成红黑树,在冲突小于一定数量时又退回为链表。
- synchronized只锁定当前链表或红黑树的首节点,所以只要hash值不冲突,就不会产生并发,提升效率。
- put(key,value)方法处理流程:
- 计算key的hash值,定位到对用的Node数组中位置;
- 判断Node数据是否需初始化,初始化方法 initTable();
- sizeCtl变量,决定着当前的初始化状态。
- sizeCtl=-1;说明正在初始化,
- sizeCtl=-N;说明有N-1个线程正在进行扩容;
- 表示Node数组初始化大小,如果Node数组没有初始化;
- 或者表示Node数组容量,如果Node数组已初始化。
- sizeCtl变量,决定着当前的初始化状态。
- 通过自旋和CAS操作写入数据:
- 首先使用无锁操作CAS插入头节点,如果插入失败,说明已有其他的线程正在插入头节点;再次循环进行操作,如果头节点已经存在,则通过synchronized 获取头节点锁,进行后续操作。
- 如果当前位置的hashcode == MOVE == -1,则说明内部正在进行扩容,执行扩容。
- 判断链表长度是否大于8(TREEIFY_THRESHOLD),且Node数组长度大于64(MIN_TREEIFY_CAPACITY),则将链表结构转化为红黑树结构。
- get(key)方法:
- 计算key的hash值,并定位Node数组中位置;
- 查找到指定位置,如果头节点即为所要找的值,则返回对应的value;
- 如果头节点hash值小于0,说明正在扩容或为红黑树,则循环查找;
- 如果是链表,遍历查找。
- 数据结构: