
文章目录1、集合框架图2、JDK1.7 hashMap线程不安全体现在哪3、JDK1.8 hashMap线程不安全体现在哪4、ArrayList和Vector的原理分析及区别ArrayList原理分析(JDK1.8)Vector原理分析(JDK1.8)ArrayList和Vector的区别5、HashSet和TreeSet的原理分析及区别HashSet实现原理(JDK1.8)TreeSet实现原理(JDK1.8)HashSet和TreeSet的区别6、HashMap和HashTable原理分析及区别HashMap实现原理(JDK1.8)HashTable实现原理HashMap和HashTable的区别TreeMap和HashMap的区别7、HashMap jdk1.8源码分析8、ConcurrentHashMap jdk1.8源码分析9、其他容器10、HashMap面试题11、ConcurrentHashMap面试题https://thinkwon.blog.csdn.net/article/details/1045885511、集合框架图2、JDK1.7 hashMap线程不安全体现在哪在HashMap扩容的是时候会调用resize方法中的transfer()方法在这里由于是头插法所以在多线程情况下可能出现循环链表所以后面的数据定位到这条链表的时候会造成数据丢失。和读取的可能导致死循环。A-B-null扩容(扩容时无论是同一链表复制到新桶同一个位置还是不同的位置都会出现死循环)A,B复制到新桶同一个下标i时出现死循环线程1执行第一步eA;nextB挂起线程2直接扩容完成节点状态B.nextAA.nextnull;然后线程1继续执行执行完后就会出现A.nextBB.nextA;A,B复制到新桶不同下标i时出现死循环线程1执行第一步eA;nextB挂起线程2直接扩容完成节点状态B.nextnullA.nextnull;然后线程1继续执行执行完后就会出现A.nextAB.nextB3、JDK1.8 hashMap线程不安全体现在哪1.8的HashMap对此做了优化resize采用了尾插法 高低位拆分链表子链表一次性赋值即不改变原来链表的顺序所以不会出现1.7的循环链表的问题。但是它也不是线程线程安全的。不安全性如下在多线程情况下put时计算出的插入的数组下标可能是相同的这时可能出现子链表被覆盖从而导致数据丢失。4、ArrayList和Vector的原理分析及区别ArrayList原理分析(JDK1.8)1. 采用动态对象数组实现默认构造方法创建了一个空数组 2. 第一次添加元素扩展容量为10之后扩展容量原来容量大小 原来容量大小/2 -- oldCapacity (oldCapacity 1) 即为原来容量的1.5倍 3. 数组不适合删除和插入操作 4. 为了防止扩容次数太多建议在创建ArrayList时指定初始容量 5. 线程不安全适合在单线程使用Vector原理分析(JDK1.8)1. 采用动态对象数组实现默认构造方法创建一个容量为10的数组 2. 进行扩容时如果有指定增量则为原来容量 增量 , 没有指定增量时(增量为0),则为原来容量 原来容量, 即为原来容量的2倍 3. 数组不适合删除和插入操作 4. 为了防止扩容次数太多建议在创建Vector时指定初始容量 5. 线程安全适合多线程访问时使用但是在单线程使用下效率较低ArrayList和Vector的区别都是对象数组实现的都不适合删除和插入操作。都允许存入null值且可以是多个。ArrayList不是线程安全的Vector是线程安全的。创建ArrayList对象构造函数创建的是空的对象数组当第一次添加元素时扩充容量为10之后进行扩容是原来容量的1.5倍而Vector创建对象时构造函数创建的是容量为10的对象数组当扩容时如果有指定增量则是原来容量 增量否则是原来容量的2倍。5、HashSet和TreeSet的原理分析及区别HashSet实现原理(JDK1.8)1.HashSet是基于哈希表(HashMap(数组链表))实现的 2.不允许有重复只能存一个null元素 3.不保证顺序恒久不变 4.添加元素时把元素当做HashMap的key而value是一个固定的Object对象 5.添加重复元素时set不会替换key所以不变且是通过equals来判断是否相同。如果想让两个对象的内容相同时是同一个对象则可以重写hashCode方法和equals方法。TreeSet实现原理(JDK1.8)1.有序基于TreeMap实现(二叉树数据结构)。 2.对象比较大小比较对象必须实现Comparable接口或者在TreeSet的构造器传入Comparator接口的实现对象 3.当add重复元素时TreeSet不改变可以用来去除重复元素 4.不允许放入null值HashSet和TreeSet的区别hashset是哈希表实现的TreeSet是二叉树实现的。hashSet是无序的Treeset是自动排好序的需要存入的对象实现Comparable接口或者传入Comparator。HashSet可以放null且只能放一个TreeSet不能放null值。HashSet和TreeSet都不是线程安全的。6、HashMap和HashTable原理分析及区别HashMap实现原理(JDK1.8)hashMap的实现原理(JDK1.8) 1.基于哈希表(数组链表二叉树(红黑树)) JDK1.8 2.默认加载因子为0.75默认数组大小是16 3.如何将对象存储在哈希表中 //hash()内部实现: hash (key.hashCode()) ^ (h 16) //下标索引计算 : (n - 1) hash n: 数组长度 通过hash()方法计算key的hash值然后用这个hash值取余数组的长度16(默认), 来确定该对象在数组存储的位置 当这个位置有多个对象时以链表结构存储在JDK1.8后当链表的长度大于8时会转为红黑树存储结构。 目的是为了取值更快存储的数据量越大性能表现更卓越。 4.扩充原理: 当数组的存储容量大于阈值75%(默认)数组进行扩充扩充为原来的2倍 newCap oldCap 1 5.线程不安全适合在单线程下使用。 6.如果存储相同key的元素时会覆盖掉原来的valuekey不做处理。 7.key是通过hashCode和equals方法比较是否相等。HashTable实现原理1.JDK1.0开始 2.基于哈希表实现(数组链表) 3.默认数组大小为11加载因子0.75 4.扩充方式原数组的大小 1 1 5.线程安全HashMap和HashTable的区别HashMap实现Map接口HashTable继承Dictionary类。HashMap不是线程安全的HashTable是线程安全的。HashMap允许一条记录的键为null允许多条记录的值为null而HashTable的key和value都不能为null。HashMap的初始容量是16HashTable的初始容量为11负载因子默认都是0.75。HashMap的扩容是原来容量的2倍只有链表长度大于8且数组长度大于64才会转为红黑树否则只是扩容HashTable的扩容是原理容量的2倍1。HashMap的实现是数组 链表 红黑树而HashTable的实现是数组 链表。HashMap需要重新计算Hash值而Hashtable直接使用对象的HashCode值。HashMap和HashTable判断key是否相等使用的是hash和equals。TreeMap和HashMap的区别HashMap是基于数组 链表 红黑树实现TreeMap是基于红黑树实现的HashMap是无序的TreeMap是基于键进行排序的。HashMap的key和value 可以存nullkey只能存一个TreeMap的key不能为nullvalue可以为nullHashMap适用于插入删除定位元素TreeMap适用于按自然顺序或自定义顺序遍历键。HashMap和TreeMap都是线程不安全。7、HashMap jdk1.8源码分析https://blog.csdn.net/Linging_24/article/details/1327968328、ConcurrentHashMap jdk1.8源码分析https://blog.csdn.net/Linging_24/article/details/1327456209、其他容器https://blog.csdn.net/qq_45738250/article/details/12882779110、HashMap面试题1. JDK8 HashMap 底层结构数组Node [] 单向链表 红黑树桶内链表长度≥8且数组容量 ≥64 → 链表转红黑树红黑树节点数量≤6→ 红黑树退化成链表为什么 8 和 6泊松分布哈希正常情况下链表长度几乎不会到 8留差值避免频繁树化 / 退化震荡。2. Node 结构static class NodeK,V implements Map.EntryK,V { final int hash; final K key; V value; NodeK,V next; }红黑树节点TreeNode继承 Node增加 prev/left/right/color 等红黑树属性。3. hash () 方法为什么扰动static final int hash(Object key) { int h; return (key null) ? 0 : (h key.hashCode()) ^ (h 16); }高 16 位和低 16 位异或保留高位信息。取桶下标(n - 1) hashn 是数组长度2 的幂只会用到 hash 低位。如果不扰动高位全部丢失哈希冲突概率变大。桶下标不用%运算比取模更快前提数组长度是 2 的幂。4. 容量、负载因子、扩容阈值默认容量16必须是 2 的幂默认负载因子loadFactor0.75阈值threshold capacity * loadFactor元素数量超过阈值触发扩容。扩容容量变成2 倍为什么 0.75空间和哈希冲突的折中。太小浪费空间太大冲突变多。为什么容量必须 2 的幂保证(n-1)hash等价取模运算高效。5. put () 流程高频计算 key 的 hash 值判断数组是否为空 / 长度 0 → 先扩容初始化数组计算桶下标i (n-1)hash如果桶为空直接新建 Node 放入桶不为空桶首节点 hash、key 相等 → 覆盖旧 value如果桶首节点是 TreeNode → 红黑树插入否则遍历链表找到相同 key覆盖 value遍历到链表末尾新增节点新增后链表长度≥8判断数组容量容量 64不树化直接扩容容量≥64链表转为红黑树put 成功后size如果 sizethreshold触发扩容。✅ JDK8 插入节点尾插法JDK7 是头插 这就是 JDK8单线程扩容不再死循环的核心原因6. resize () 扩容机制扩容数组为原来 2 倍。旧数组上的节点重新分配到新桶旧 hash 旧容量oldCap结果 0新下标 旧下标结果0新下标 旧下标 oldCap 不需要重新计算 hash直接通过这个判断分到两个链表。JDK8 resize 是尾插链表顺序不变不会形成环形链表。⚠️ 但是HashMap依然线程不安全并发 put 会发生数据覆盖丢失数据只是没有死循环了。7. JDK7 和 JDK8 HashMap 核心对比特性JDK7JDK8底层数组 单向链表数组 链表 红黑树插入方式头插法尾插法扩容头插链表反转并发会环形链表死循环尾插顺序不变无死循环树化无链表≥8容量≥64 转红黑树hash 扰动4 次移位扰动1 次高低 16 位异或11、ConcurrentHashMap面试题1. JDK8 CHM 底层结构数组 Node [] 单向链表 红黑树和 HashMap 结构一致。链表≥8数组容量≥64 → 转红黑树红黑树节点≤6 → 退链表Node 的 val 用volatile修饰保证可见性。2. putVal () 完整流程key/value 不能为 null直接抛 NPEHashMap 允许 1 个 null key计算 hashspread()扰动如果 table 未初始化CAS 竞争初始化数组懒加载定位桶下标i(n-1)hash桶为空CAS 无锁直接写入 Node成功就结束桶头节点是ForwardingNode(hashMOVED)正在扩容当前线程 helpTransfer 帮忙一起迁移数据桶有数据synchronized(桶头节点)只锁这一个桶其他桶不受影响遍历链表找到相同 key 覆盖 value尾插新增节点如果是 TreeNode走红黑树插入插入完成后 addCount 计数判断是否触发扩容 / 树化✅ 锁粒度桶级锁不是全局锁。只锁当前操作的桶其他桶并发读写不受影响。3. 为什么用 synchronized 而不是 ReentrantLockJDK8 之后synchronized有锁升级偏向锁→轻量级→重量级性能很好竞争不激烈时开销极低不需要额外维护锁对象节省内存JVM 底层持续优化 synchronizedReentrantLock 是 API 层面锁4. get () 怎么保证线程安全读有没有加锁get 不加锁用volatile修饰数组引用、Node 的 value保证内存可见性直接按 hash 找桶遍历链表 / 红黑树读取弱一致性迭代器遍历期间其他线程新增 / 修改不一定立刻感知不会抛 ConcurrentModificationExceptionHashMap 迭代器是 fail-fast 快速失败5. 扩容 transfer 渐进式扩容触发条件元素数量超过阈值 sizeCtl或者链表≥8 但数组 64优先扩容而不是树化新建 nextTable容量 2 倍多线程协助扩容一个线程扩容时其他线程 put/get 碰到 ForwardingNode就帮忙迁移数据helpTransfer迁移完的桶标记为ForwardingNode指向新数组防止重复迁移全部迁移完成把 table 替换成 nextTable一句话扩容不是单线程独自搬多线程协同搬家。迁移逻辑和hashmap一样依靠hash oldCap拆分成两组只不过一个是无锁迁移一个是有锁迁移支持多线程迁移。6. size () 计数怎么实现为什么不用全局锁借鉴 LongAdder分段计数思想baseCountCounterCell[]竞争低直接累加 baseCount竞争激烈线程去不同 CounterCell 格子累加分散压力size () baseCount 所有 CounterCell 求和高并发下统计性能远优于 JDK77. JDK7 vs JDK8 ConcurrentHashMap 对比JDK7JDK8底层Segment 数组 HashEntry 链表分段锁Node 数组 链表 红黑树锁Segment 继承 ReentrantLock锁整个段CAS synchronized 锁桶头节点并发度Segment 数量固定默认 16初始化后无法提升随数组长度动态变大扩容每个 Segment 独立扩容全局数组扩容多线程协助 transfersize多次尝试不加锁失败锁住所有 Segment 统计baseCountCounterCell 分片计数结构两层哈希表先 hash 定位 segment再定位 entry单层哈希表和 HashMap 一致8. 高频坑题① 为什么 key/value 不能为 nullHashMap 允许 keynull放在 0 号桶CHM 禁止。原因并发场景下get(key)返回 null分不清是不存在 key还是key 存在 valuenull无法区分会产生并发歧义。② CHM 是不是绝对线程安全只保证单个方法原子性复合操作不安全例if(map.get(k)null) map.put(k,v)这两行代码不是原子操作多线程会出现数据覆盖。复合场景要用computeIfAbsent、merge原子方法。③ CHM 和 Hashtable 区别Hashtable 是方法上加synchronized全局锁并发差CHM 锁粒度细并发高。Hashtable 数组扩容是 2 倍 1CHM 扩容 2 倍。Hashtable 也不允许 null。④ 红黑树退化条件和 HashMap 一样桶内节点数≤6红黑树退化为链表。7 是缓冲阈值防止频繁树化 / 退化震荡。9、JDK8 ConcurrentHashMap 的 hash 扰动 spread () 和 HashMap 的 hash 方法有什么区别相同部分核心扰动逻辑一样都做h ^ (h 16)把高 16 位混合到低 16 位减少哈希冲突。CHM 多一步 0x7fffffff作用清除 int 的符号位bit31保证最终 hash 一定是正数。