Java 集合
-
ArrayList 和 LinkedList 分别是数组和双向链表实现,前者适合下标查找,后者适合增删场景多,前者需要预先定义数组长度,可能存在空间浪费,后者内存空间不连续,每个节点占用空间更多;
-
ArrayList 扩容,是在插入元素之前检查当前元素个数 +1,如果超过数组大小,就触发扩容,创建一个 1.5 倍的新数组,拷贝原数组元素;
-
ArrayList 的序列化,并不是直接序列化 elementData 数组,而是定义了 readObject 和 writeObject 的两个方法,只对使用的数组元素进行序列化,反序列化类似;
-
快速失败,是指在迭代过程中判断如果集合内容发生改变,则抛出异常,终止遍历,判断条件是每次迭代,判断 modCount 值是否发生改变,该值在其他线程改变集合内容时会同步改变,但无法发现 ABA 的现象;安全失败,是指遍历元素时,不直接操作集合,而是复制集合内容,在拷贝的集合上操作,因此是线程安全的,缺点是迭代过程中无法感知其他线程的修改;
-
CopyOnWriteArrayList 是 ArrayList 的线程安全版本,采用读写分离的并发策略,读操作是无锁的,写操作时,先加锁,每次拷贝原数组,在副本上执行写操作,完成后将容器引用指向副本,因此写写是串行的,读写,读读都是并发的;
-
HashMap 的数据结构是数组 + 链表 + 红黑树,通过哈希算法得到存放元素的数组下标,元素冲突时链表结构存放,当链表元素大于 8 并且数组大小大于 64 时,链表转为红黑树,提高查找效率,当红黑树元素小于 6 时,转为链表;
-
红黑树,是一种特殊平衡二叉树,保证了插入,删除,查找的时间复杂度都为 O(logN),但旋转次数少于平衡二叉树,效率更高;
-
hashmap 的容量始终保持着 2 的整数倍,即使构造时传入不是 2 的倍数,也会向上找到最近的 2 的倍数,这样哈希取余获取数组下标时,使用 & (length-1) 代替 %length,length 是 2 的整数次幂,其 2 进制表示只有一个 1,其他全是 0,那么(length-1) 就是高位全 0,低位全 1,& 运算后只保留了低位,这与取余 % 的效果一致,但位运算比取余效率高很多,且保证扩容后也是 2 的倍数,这样扩容后已经产生 hash 碰撞的元素,就可以完美转移到新 table 中;
-
hashMap 定位数组下标,使用的是扰动函数+哈希取余,扰动函数是对 key.hashCode() 的 高 16 位和低 16 位做异或操作,这样设计混合了哈希值的高位和低位,这样比只获取哈希值的低位值,随机性更大,且保留了高位信息;
-
哈希冲突解决,链地址法,开放定址法,再哈希法和公共溢出区等方案,分别是将冲突元素组成链表,从冲突位置向后查找,再次哈希重新计算位置,建立公共溢出区,存放冲突元素等;
-
链表转红黑树的条件是数组长度大于 64,且链表长度大于 8,红黑树节点数少于 6 时,转回为链表,hashMap 扩容是根据当前元素个数大于(数组大小 * 负载因子)时触发,负载因子默认值是 0.75,是考虑空间和时间(哈希冲突)来平衡选择的,扩容后的元素迁移,因为数组大小是 2 的整数次幂,所以新元素是在原位置,或者原位置再移动原数组大小位置;
-
hashmap 在 jdk8 后新增了红黑树的结构,查找复杂度由 O(n) 降为 O(logn),链表的插入方式从头插法改为了尾插法,原因是头插法在多线程扩容情况下可能产生环,形成死循环,扩容时,jdk7 是重新哈希计算位置,而 jdk 8 是直接放原位置或者原位置移动新增容量大小的位置;
-
hashmap 线程不安全,体现在 put 元素时多线程可能出现 put 丢失(线程 A 发现 table[index] 为空,开始新建 node,此时线程 B 进入,计算得到同样的 index,发现 table[index] 为空,插入新值,随后线程 A 继续执行,覆盖了线程 B 插入的值),put 和 get 并发时,可能导致 get 获取到 null(线程 A get 元素时,遍历链表查找元素,此时线程 B put 新元素,触发扩容,导致线程 A 要查找的元素分配到了其他位置,此时线程 A 继续查找,就导致存在的元素,返回 null);
-
线程安全的 hashMap,有 Collections.synchronizedMap 和 ConcurrentHashMap,前者是在 Collections 的每个接口方法中使用 synchronized 加同一个对象锁来实现同步的,后者是在 jdk7 和 jdk8 中分别使用的分段锁和 cas+synchronized 实现的;
-
concurrentHashMap 的分段锁实现,数据结构是 segment 数组,每个 segment 数据结构类似一个 hashMap,是由 hashEntry 的数组构成,hashEntry 本身是链表节点结构,put 操作时,key 哈希计算得到 segment 数组下标,因为 segment 继承 ReentranLock,操作时需要加锁,然后获取 hashEntry 的数组下标,遍历链表插入或替换元素,因此它的并发度就是 segment 的个数,同一 segment 的操作是串行的,不同 segment 是并行的,并且 segment 个数在初始化时就固定了,之后不会发生改变,扩容只会扩容 hashEntry 数组大小,get 操作时,因为 value 使用 volatile 修饰,不需要加锁;
-
concurrentHashMap 的 cas + synchronized 实现,它的数据结构和 hashMap 完全一致,数组 + 链表 + 红黑树,put 操作时,key 哈希计算得到 node<k,v>[] 的下标,如果 node[index] 为空,会 cas 写入值,cas 写入失败会自旋尝试若干次,如果仍失败,或者 node[index] 有值,使用 synchronized 对 node[index] 的链表阻塞加锁,获取锁后遍历插入元素或替换元素,get 操作,同样不需要加锁,不过扩容操作,是需要阻塞所有读写操作的,但会让阻塞线程也加入到扩容中,减少阻塞时间,cas + synchronized 的设计,使得 concurrentHashMap 的并发度更高的,加锁是针对每个 node 数组元素的;
-
hashMap 的元素是无序的,但是 LinkedHashMap 是有序的,维护了元素的插入顺序,其节点 Entry 在 HashMap.Node 的基础上添加了 before 和 after 两个指针,head 和 tail 分别指向双向链表的头和尾;
-
treeMap 也是有序的,是按照自然顺序或者内部元素 comprator 排序的,treeMap 的数据结构和 hashmap 不同,采用的是红黑树来实现的;
-
hashSet 是基于 hashMap 实现的,其 add 方法,是往内部 hashMap 中 put 一个 value 为 new Object 的键值对,通过返回值是否为 null 来判断元素是否插入成功,hashMap 的 put 方法,返回的是插入 key 位置的旧值,因此 null 表示没有旧值,即 hashset add 成功,不为 null,表示有旧值,hashset add 失败;