ArrayList和LinkedList
-
底层数据结构,分别是动态数组和双向链表。
-
内存分配,数组是连续内存空间且需要指定大小。链表是非连续的,且链表空间占用更大。
-
插入和删除元素,链表更快,只需改变链表元素指针。数组需要移动复制整个数组。
-
随机查找,数组更快,支持索引下标查找。链表需要遍历查找。
-
使用场景,数组适合存储大量数据且不需要频繁插入和删除。链表适合需要频繁插入和删除的数据。
-
两者都是非线程安全的。
Vector和Stack
-
Vector 和 ArrayList 的结构基本一致,只是在每个方法上都添加了 synchronized 关键字,但由于序列化安全,性能等原因通常不被使用。
-
Stack 是栈结构,继承 Vector,同样不推荐使用。
ArrayList 扩容
-
ArrayList 是动态数组,在没有空间插入新元素时会进行扩容。
-
扩容操作是创建一个1.5倍长度的新数组,然后复制原数组元素。
快速失败和安全失败
-
快速失败,是指集合在每次迭代时,都会判断集合元素是否发生变化(增加或减少),如果是就会抛出异常,终止迭代。但无法发现aba的问题。todo
-
foreach 操作是快速失败的,使用 iterator 进行迭代,可以调用其 remove 方法来删除集合元素。todo
-
安全失败,是指集合发生改变不会影响集合的迭代,这是因为迭代过程是复制一个原集合对象,在复制集合上操作。
-
java.util.concurrent 包中集合类都是安全失败的,是线程安全的,缺点是迭代过程无法感知集合的变化。
CopyOnWriteArrayList
-
CopyOnWriteArrayList 底层数据结构也是动态数组,和 ArrayList 的区别是,所有修改操作都是先对集合加锁,然后拷贝一份集合,之后在拷贝集合上进行修改,完成后再将原引用指向新拷贝的集合,最后释放锁,整个修改都是加锁是为了避免修改丢失。
-
CopyOnWriteArrayList 的读操作不会加锁,因此修改操作是串行的,读和写之间是可以并行的。
-
迭代操作也是安全失败的,因为修改是在拷贝的新数组上。
-
缺点是修改需要额外占用内存空间,对于大集合是不友好的,且修改不是实时可见的。
Collections
-
Collections 是一个工具类,提供了排序、查找、包装线程安全的方法。
-
Collections.sort() 对集合进行排序(改进的归并排序)。
-
Collections.binarySearch(list,key) 从排序集合查找元素(二分查找)。
-
Collections.synchronizedXXX() 包装集合为线程安全对象,通过给每个方法添加 synchronized 关键字。
-
Collections.unmodifiableXXX() 返回一个不可变集合。
数组
-
创建数组时需要指定大小和数据类型,此时整个内存空间已经完成分配,每个数组元素均有默认值。
-
数组是引用类型,分配在堆区。
-
java 中没有多维数组,所谓的多维数组只不过是一维数组的每个元素都是一个新数组,且不要求新数组的长度都相同。
-
Arrays 是数组工具类,提供了排序,查找,复制等方法,排序要求数据元素实现 comparable 接口(如果是基本类型,采用的是优化后的快排,如果是引用类型,采用的是改进的归并排序)。查找返回数组下标位置,返回负数表示不存在。复制是浅复制,其引用类型仍指向原始堆空间的对象。
复制数组,获取指定的长度 newLength,截取或用 null 填充
T[] Arrays.copyOf(T[] ori,int newLength)
复制数组,在for循环复制的基础上做了优化
System.arraycopy(src,beginIndex,dest,beginIndex,length)
HashMap
-
底层数据结构,是动态数组 + 链表或红黑树。当链表长度大于8且数组长度大于64时,链表结构转为红黑树,如果红黑树大小缩小到6时,又转为链表结构,这样做都是为了提高查找效率。
-
存入元素时,对键值key进行哈希求余得到数组下标,如果该下标处已有元素,遍历查找该下标的链表是否相同的key(equal相等),如果有覆盖该键值对的value,如果没有则在链表尾插入新键值对。
-
哈希取余,实际会先对 key.hashCode() 结果高 16 位和低 16 位做异或操作,这样设计混合了哈希值的高位和低位,相比于直接使用哈希值低位值,随机性更大。
-
解决哈希冲突有:链表(冲突元素组成链表),开放寻址(直接向后查找位置),再哈希(将冲突元素再次哈希计算位置)和公共溢出区(建立一个公共区域,存放冲突元素)等方式。
-
hashmap 底层的数组,会始终保持 2 的整数倍,这是为了方便哈希求余,同时也减少了扩容时数组元素的迁移。length为2的倍数时,可以使用 &(length-1) 代替%length,运算效率更高。同时扩容迁移元素时,要么还在原位置,或者原位置加上数组大小的位置。
当 length 是 2 的整数次幂,那么其 2 进制就最高位是 1,其余位都是 0,那么(length-1) 是最高位是0,其余位都是 1,进行 & 运算后就是只保留了低位,去除最高位,这和取余%的效果一致,但效率会高很多。
-
hashMap 当元素个数大于(数组大小 * 负载因子)时触发扩容,默认负载因子是 0.75,数组大小是 16,那么当加入第13个元素时触发扩容,扩容2倍大小。通过指定合适的负载因子和数组大小,能平衡空间和效率。
-
jdk8 将链表插入方式从头插法改为尾插法,是因为头插法在多线程扩容情况下可能产生链表环,导致get操作形成死循环。并且jdk8在扩容时元素不再是重新计算哈希位置放入,而是直接放在原来位置或在原来位置的基础上移动扩容的大小。
-
非线程安全,多线程 put 元素时可能出现覆盖,多线程 get 和 put 时触发扩容也会造成找不到元素。
HashSet
-
HashSet 的底层数据结构和HashMap是完全一致的,不同的是其存入元素的键值对中 value 固定是 new Object。
-
hashSet 添加元素时,如果存在 equals 相等的元素,返回该值,表示添加失败,如果不存在返回null,表示没有相同值,添加成功。
TreeMap 和 TreeSet
-
TreeMap 底层是红黑树的结构,是有序集合,要求存储元素实现 compare 接口,其查找,插入,删除的时间复杂度都是 O(logN)。
-
TreeSet 底层和 TreeMap 是同样的数据结构,区别是键值对的 value 固定为 new Object()。
LinkedHashMap
-
LinkedHashMap 在 HashMap 的基础上, 给每个键值对添加了两个指针,使得可以根据插入顺序或访问顺序(lru)来获取元素,是有序集合。
-
在需要键值对有序的场景,选择 LinkedHashMap。
ConcurrentHashMap
-
线程安全的 hashmap,jdk7 使用分段锁实现,jdk8 使用 cas+synchronized 实现,性能更高。
-
cas + synchronized 的方式,其底层数据结构和 hashMap 完全一致。
-
插入元素时,如果下标元素 node[index] 为空,采用 cas 写入值,写入失败会自旋尝试若干次。如果仍失败表示 node[index] 有值。
-
当下标元素 node[index] 有值时,通过 synchronized 对 node[index] 处的链表加锁,只有获取到锁才能完成插入。这样加锁只发生在哈希冲突的场景,且加锁范围控制在单个数组下标,其他下标元素的操作可以并行。get 操作不需要加锁。
-
如果需要扩容,会阻塞所有读写操作,并会让阻塞的线程也参与到扩容操作中。
-
分段锁实现,底层是 segment 数组,每个 segment 元素都可以理解为一个小的 hashMap。然后操作时先定位到 segment,然后对segment 加锁,完成操作后释放锁。这样设计对不同 segment 的操作是可以并行的,相同 segment 的操作串行,保证了线程安全同时缩小了加锁范围,并发度更好。其get 操作不加锁,键值对使用 volatile 修饰。
-
定位 segment 是通过对键值 key 哈希求余,之后对 segment 的操作和 hashmap 完全一致。segment 个数在创建时已经固定,不能被修改,因此扩容只会增加 segment 中的 entry 数组大小。
queue
-
先进先出的队列操作,实现类有 LinkedList(插入顺序),PriorityQueue(优先级队列,按优先级顺序)。
-
Deque 双向队列,支持队列头尾双向操作,方便实现先进后出的栈结构,实现类有 LinkedList。