- 继承体系
- Collection、Map、List、Set、Queue
- 是否有序、是否可重复、键值对
- 集合fail-fast机制
- 错误检测机制, 防止对非线程安全集合的并发修改
- ConcurrentModificationException
- 如何创建不可变集合
- Collections#unmodifiableXxx
- 进行修改操作时抛出异常UnsupportedOperationException
- 如何迭代时删除元素
- 迭代器遍历删除
- fori删除会导致有些元素访问不到; foreach删除抛出异常, 因为底层使用迭代器
- ArrayList和LinkedList区别
- 数组、链表
- 连续内存、常数时间内随机访问
- ArrayList和Vector区别
- 是否线程安全、扩容策略
- Vector的size为什么要同步, 扩容策略2和1.5区别
- ?
- 倍增则均摊O(1)复杂度, 固定增长O(N)
- 倍增导致下一次申请的内存必然大于之前所有分配总和, 可能无法复用
- 如何创建线程安全集合
- Collections#synchronizedXxx
- List实现类
- ArrayList、LinkedList、CopyOnWriteArrayList、Vector
-
HashMap和Hashtable区别
- 线程安全性、null键值
- 哈希值, 前者重新计算
- 扩容, 前者扩容为2倍+1, 后者2倍扩容.
- 结构/解决哈希冲突办法, 前者数组+链表, 后者数组+链表+红黑树
-
HashMap在JDK和JDK8区别
- 数组 + 链表 + 红黑树
- 插入改尾插, 不会出现多线程下循环链表的问题
- 哈希值处理, JDK7不处理
-
基本原理?HashMap为什么使用数组+链表?哈希冲突的解决办法
- 使用Node数组存储k-v值, Node实际是一个单向链表
- 开放定址法、链地址法、再哈希法、公共溢出区
- 开放定制: 线性探测、平方探测, ThreadLocal使用线性探测
- 链地址法: 链表存储冲突元素
- 再哈希法: 冲突则使用另一个哈希函数再哈希
- 公共溢出区: 冲突的全部使用List存储, 那么若哈希冲突则需要遍历这个List
-
LinkedList代替数组可以吗
- 可以, 但是效率很低, 因为需要本质上是需要常数时间内的随机访问
-
ArrayList可以吗
- 不可以, 因为ArrayList的扩容固定了, 而HashMap要求倍增
-
HashMap什么条件下扩容?为什么扩容2次幂?hash值计算&为什么?为什么默认装载因子0.75
- 当容量到达 数组长度*装载因子 扩容
- 为了便于使用位运算计算元素所在桶, 高效计算
- hash ^ (hash >>> 16), 容量较少时利用到高位, 降低膨胀几率
- 太大空间利用率低太小碰撞几率增加. 0.6-0.8合适, 取0.75折衷结果.
- 若Hash分布均匀, 则碰撞符合泊松分布, 达到8个节点的概率小于千万分之一
-
put流程
- 数组为null或长度为0先初始化, 默认容量16
- 计算哈希值计算索引, 没有碰撞则直接存储
- 碰撞若第一个节点k相同直接替换val
- 不同则判断节点类型, 分别插入树和链表, k同则值换
- 若链表节点插入新节点, 若链表长度大于等于8且同容量大于等于64, 树化, 小于64扩容
- 最后容量大于扩容阈值则扩容
-
get流程
- k计算hash值计算索引
- 桶节点为null则没有值, 返回null
- 否则第一个节点k值相等返回值
- 否则对链表节点/树查找
-
哈希算法
- 把大范围值映射到小范围(或固定长度小范围), 又称摘要算法Digest
- MD4、MD5, MD Message Diagest, 输出长度16B. MD5输出较短, 短时间内破解是可能的, 不推荐使用
- SHA-1, 数组20B, SHA-256输出32B, SHA-256输出64B
- BASE64
- 哈希算法输出越长, 越难产生碰撞, 越安全
- 可以加盐, 也就是加上随机随机字符再进行哈希运算, 那么就难以逆推
- 逆推: 计算常用密码的MD5, 然后根据MD5碰撞尝试是否为这个常用密码
- 加盐: 碰撞出字符串, 我们也会加上盐, 加盐后MD5就比对不一样了
- 文件校验: 是否被修改、文件是否一致
-
String的哈希计算
- 31为权, 对每一个字符进行加权多项式计算, 自然溢出值为hashCode
- 31为质奇数, 运算可优化为位运算
- 简单、运算块、分布较为均匀
- 缓存hashCode + 不可变对象
- 【原理】
-
JDK8改了啥?为什么不直接使用红黑树?BST可以吗?阈值为什么是8?什么时候退化
- 加了红黑树、哈希运算优化、链表尾插
- 红黑树需要旋转平衡, 元素较少时实际效率差别不大, 较多时才能使用查询效率抵换平衡花费
- BST可能退化为链表
- k随机均匀、装载因子0.75, 则一个桶中元素遵循λ为0.5的泊松分布, 8时概率低于千万分之一
- 6, 有一个节点的缓冲, 避免频繁转换
-
并发问题
- JDK7多线程扩容死循环, JDK8没有
- put丢失无法get
- 使用线程安全集合Map
-
什么键适合作为k
- 不可变对象, String、包装类
- 键可以为null
- 可变k可能导致无法get出来
-
自定义对象作为键要如何处理
- 最好是不可变对象, final 修饰实例属性, 构造函数初始化
- hashCode 和 equals 进行重写
- hashCode 缓存
- 不提供getter
- 传入可变类进行初始化则需要clone
-
Map实现类
- 有序?线程安全?按什么排序?
- HashMap、LinkedHashMap、TreeMap、WeakHashMap
- ConcurrentHashMap、ConcurrentSkipListMap
- Collections.synchronizedMap、unmodifiableMap
- 强软弱虚引用
-
缓存淘汰策略
- LRU: 最近最少使用, 最长时间没有访问的. LinkedList 重写 removeEldestEntry 根据策略移出元素
- LFU: 最不经常使用的
-
ConcurrentHashMap
- JDK7的锁Segment, 每个Segment多个桶, JDK8直接锁桶, 丢弃Segment的实现并加入红黑树, 同时使用synchronized而非Lock
- JDK8没有哈希冲突时直接CAS, 否则才会锁住整个桶
- 不支持键值的null
- 写加锁, 读不加锁
- Hashtable强一致性, ConcurrentHashMap 若一致性
- 协助扩容
- 认领一个区间进行扩容, 会反应到实例变量中, 因此每一个线程会领取到合适的区间
- 最小16个桶
- len / 8 / NCPU, 若NCPU为1则全部桶
- 当处理完自己的区间, 回去看一下是否领取完毕, 没有则再次领取一个区间
- 扩容时节点新new, 所以不影响原数组, 不影响get操作. 已处理好的则一个新的数据类型节点转发到新的数组桶
- 认领一个区间进行扩容, 会反应到实例变量中, 因此每一个线程会领取到合适的区间
-
ConcurrentSkipListMap
- 跳表, 可以进行二分查找的链表
- 在链表上面添加多级索引, 空间换时间, 最底层所有元素双向链表, 可范围查找
- 插入元素时, 随机数找到其需要建立索引等层级, 然后插入各层位置
- 随机数 & 0x80000001: 非负偶数才处理
- 判断值从第二为开始有几个连续1, 就几层+1, 最底层1层, 最小也是1层., 0110110为3, 01100为1
- 超过现有层级则最多超过1层
-
线程安全的Map都不允许null值null键
- getKey得到了值并不能判断val是null还是没有此元素, 而单线程不担心因为本来就不不在多线程下使用
-
什么时候使用TreeMap
- 当需要有序遍历的时候
-
Set实现类
- HashSet、LinkedHashSet、TreeSet
- CopyOnWriteArraySet、ConcurrentSkipListSet
- Collections.newSetFromMap 转 Map 为 Set, 可获得 ConcurrentHashSet
-
并发队列: ArrayBlockingQueue、LinkedBlockingQueue、ConcurrentLinkedQueue 区别
- ArrayBlockQueue: 定容, 一把锁 ReentrantLock + Condition 实现
- ConcurrentLinkedQueue: 非定容, CAS + 重试 实现, 入队速度很快
- LinkedBlockingQueue: 定容(MAX), 两把锁实现, 出队入队互不影响
-
Queue实现类
-
PriorityQueue 【非定容, 不允许为null】
-
小根堆实现, 及返回最小元素
+-----------------------------+
| 操作 | 抛出异常 | 返回特定值|
+-----------------+-----------+
| 入队 | add | offer |
| 出队 | remov | poll |
| 队首 | element | peek |
+------+----------+-----------+ -
ConcurrentLinkedQueue【非定容】
- 非阻塞队列
- 线程安全
-
ArrayDeque
-
LinkedList
-
非线程安全的均非定容
-
-
阻塞队列
-
ArrayBlockQueue 【定容, 必须指定容量, 不允许为null】
- 只是用了一把锁来控制入队与出队
- 可传入使用公平锁和非公平锁, 默认非公平
+-----------------------------+----------------+
| 操作 | 抛出异常 | 返回特定值| 阻塞 | 超时 |
+-----------------+-----------+-------+--------+
| 入队 | add | offer | put | offer |
| 出队 | remov | poll | take | poll |
| 队首 | element | peek | | |
+------+----------+-----------+-------+--------+
-
LinkedBlockingQueue【定容, 默认容量整型最大值, 不允许为null】
- 两把锁分别锁入队和出队
-
Synchronous【没有被消费前阻塞, 不允许为null】
- [ˈsɪŋkrənəs] 同步的, 同时的
- 无缓冲阻塞队列
- 两线程间移交元素
- 实际会缓冲, 比如生产者多, 则内部存储起来, 且阻塞生产者, 直到被消费
-
PriorityBlockingQueue【非定容, 不允许为null】
-
LinkedTransferQueue【不允许为null】
- LinkedBlockingQueue、SynchronousQueue(公平模式)、ConcurrentLinkedQueue三者的集合体
- 生产者可以控制是否阻塞
- 消费者还是会阻塞
-
DelayQueue【非定容, 不允许为null】
- 阻塞队列
- SchduledThreadPoolExecutor不是直接使用, 而是重新进行实现的
-阻塞队列与非阻塞队列的区别
- 是否提供阻塞方法: put、take -
-
线程池
- Executors.newSingleThreadExecutor: 1核心线程1最大线程 + LinkedBlockingQueue
- 问题: MAX定容, 任务过多导致OOM
- newFixedThreadPool: 核心最大一样, LinkedBlockingQueue
- 问题: MAX定容, 任务过多导致OOM
- newCachedThreadPool: 无核心线程, 最大MAX + SynchronousQueue + 60s超时
- 问题: MAX线程数导致OOM
- DelayedWorkQueue: 核心设置 + 最大MAX + DelayedWorkQueue
- 问题: 最大线程数MAX, 但是 DelayedWorkQueue 最大MAX
- 因此问题还行MAX队列
- Executors.newSingleThreadExecutor: 1核心线程1最大线程 + LinkedBlockingQueue
【List】
【没有固定容量的】
ArrayList
- 默认容量10
- 传入容量立即分配空间
- 1.5倍扩容
- 不会默认缩容
- 常数时间内的随机访问
- 适合尾部增删
LinkedList
CopyOnWriteArrayList
- 读写分离, 写时复制, 阻塞写不阻塞读
- 弱一致性, 不保证实时一致性, 只保证最终一致性
- ArrayList的线程安全版本
- 适合读多几乎不写的场景
Vector
【Map】
【无序、有序、线程安全】
+-------------------------+--------------------------------------+
| Map | 键值 |
+-------------------------+--------------------------------------+
| HashMap | 均允许为null |
| LinkedHashMap | 均允许为null | 继承HashMap
| WeakHashMap | 均允许为null |
| TreeMap | 有比较器则允许, 没有则k不能为null |
| ConcurrentHashMap | 均不允许为null | 固定装载因子
| ConcurrentSkipListMap | 均不允许为null |
+-------------------------+--------------------------------------+
HashMap
- 允许null键null值
- 数组 + 链表 + 红黑树
- bucket桶
- 容量必须为2的次幂
- 默认装载因子0.75
- 树化阈值8 + 64桶
- 链化阈值6
- Node单链表节点, newNode创建, 便于LinkedHashMap扩展维护双链表
- hash函数特殊
- 不会立即开辟空间
- put可以相同时替换key并调用 afterNodeAccess, 便于LinkedHashMap扩展维护LRU
- put结束后调用了 afterNodeInsertion(removeEldestEntry), 便于LinkedHashMap扩展维护LRU
LinkedHashMap
- 允许null键null值
- 继承 HashMap
- 重写 newNode 维护双链表
- 能按照插入顺序排序, 也能按照访问顺序排序, 可以实现LRU缓存
TreeMap
- 继承 SortedMap、Navigable, 注意 LinkedHashMap 没有
- 红黑树(红色或黑色、Root黑色、红色孩子黑色、任意节点到所有叶子节点路径上黑色节点数相同、BST)
- 有比较器则允许null键, 否则不允许
ConcurrentHashMap
- 不允许null键null值
- HashMap线程安全版本, 数组 + 链表 + 红黑树
- 相比Hashtable效率极大提高, 不过不是强一致性的(Hashtable的get和size均同步)
- synchronized + CAS + 自旋 + 分段锁
- size存储类似 LongAddr, 分段存储, 不同线程哈希到不同的段
- 读操作不加锁, 不是强一致性的
ConcurrentSkipListMap
- 键值均不允许为null
- 跳表实现, 实质是可以进行二分查找的有序链表, 原链表基础上加上多级索引(缓存), 通过索引实现快速查找
- Redis使用跳表实现有序列表是因为跳表实现较为简单易读, 且范围查询效率较高
- List存储
- 索引层数决定
- ThreadLocalRandom.nextSecondarySeed 随机数
- 正偶数
- 有多少个1就建立几层
- 超过现有最高层则最多高一层且头结点也新建一层
【Set】
+-----------------------+-------------+
| Set | 元素 |
+-----------------------+-------------+
| HashSet | 允许为null | HashMap字段, Object值, 没有get, 三个参数的构造函数使用LinkedHashMap
| LinkedHashSet | 允许为null | 继承HashSet, 这里HashSet的三参构造函数有了用处, 基本上没有自己的API, 不支持访问顺序排序
| TreeSet | 有比较器允许| 字段默认TreeMap, 也可传NavigableMap子类(ConcurrentSkipListMap)
| ConcurrentSkipListSet | 不允许 | ConcurrentSkipListMap字段
| CopyOnWriteArraySet | 允许 | CopyOnWriteArrayList字段
+-----------------------+-------------+
LinkedHashSet并没有实现SortedSet接口, LinkedHashMap也没有SortedMap
【Queue】
+-----------------------------+----------------+
| 操作 | 抛出异常 | 返回特定值| 阻塞 | 超时 |
+-----------------+-----------+-------+--------+
| 入队 | add | offer | put | offer |
| 出队 | remov | poll | take | poll |
| 队首 | element | peek | | |
+------+----------+-----------+-------+--------+
【均不允许元素为null】
+---------------------------+--------------------------+
| 队列 | 是否定容 |
+---------------------------+-------------- -----------+
| ArrayBlockingQueue | 是 |
| LinkedBlockingQueue | 定容, 默认MAX |
| SynchronousQueue | 双方均会阻塞 |
| PriorityBlockingQueue | 否 |
| LinkedTransferQueue | 消费者阻塞, 生产者可控 |
| DelayQueue | 否 |
| ScheXxx.DelayedWorkQueue | 否 |
+---------------------------+--------------------------+
+-----------------------+
| Queue | 全不允许元素为null
+-----------------------+
| PriorityQueue | 小根堆, 非定容
| ArrayBlockingQueue | 定容, 必须传入容量, 立即扩容, 出队入队一把锁
| LinkedBlockingQueue | 定容, 默认容量MAX, 出队入队分别锁, 效率较高
| SynchronousQueue | 交换元素
| PriorityBlockingQueue | 非定容, 小根堆
| LinkedTransferQueue | 相对SynchronousQueue效率更高
| ConcurrentLinkedQueue | 线程安全, 但不是阻塞队列, 非定容
| DelayQueue | PriorityQueue + 锁实现
| ArrayDeque | 循环数组实现双端队列
| LinkedList | 双链表
+-----------------------+
================================================================================
- 线程安全 Collections.synchronizedXxx
- 不可变 Collections.unmodifiableXxx
- Collections.newSetFromMap
- Collections.EMPTY_XXX
- 单个元素 Collections.singletonXxx
- N个重复元素的List Collections.nCopies
LRU、LFU
多少个1的位运算(与上数-1)
LRU & LFU缓存机制的原理及实现 - 知乎 (zhihu.com)
Redis LRU 算法和LFU算法 - 知乎 (zhihu.com)
LRU(Least Recently Used):最近最少使用淘汰,最长没有访问
LFU(Least Frequently Used):挑选最不经常使用的数据淘汰,访问次数最少
哈希算法: 随机、MD4、MD5, 大范围映射到小范围
面试 ConcurrentHashMap ,看这一篇就够了! - 知乎 (zhihu.com)
为什么ConcurrentHashMap是弱一致的 | 并发编程网 – ifeve.com
ThreadLocal
- Thread 的实例变量 ThreadLocal.ThreadLocalMap
- 单个 ThreadLocal 实例, 每个 ThreadMap<ThreadLocal实例, 值>
- ThreadLocal如何实现线程安全: 每个 Thread 都有自己的 Map, 都有一个键为 ThreadLocal 实例, 于是一个 ThreadLocal 实例在多个
Thread 中共存, 但是每个 Thread 中相同的 ThreadLocal 实例对应的值是多例的, 每个线程不一样
- Map
- 哈希冲突解决: HashMap 链地址, ThreadLocal 开放定址-线性探测
- 2倍扩容, 易于计算 index
- 阈值 2 / 3 = 0.666
- 没有找到回收元素且当前元素数量 > 阈值, 则扩容 https://zhuanlan.zhihu.com/p/402281916
- 内存泄露
- Map 底层是 Entry, 键为 ThreadLocal 实例弱引用
- 为什么是弱引用: 因为一般使用线程池, 那么 Thread 就是一个长生命周期对象, 若没有显式调用 remove, 则即便 ThreadLocal 实例不使用也无法被回收
- ThreadLocal 在每次 set、get 赋值都会移除被回收的对象
-- 问题: 线程池中, 一次使用了 ThreadLocal, 但是后面就不再使用了, 就产生了内存泄露
- set、get、remove 已经见减少了内存泄露的可能性
- 但是若 static 的 ThreadLocal, 则延长了生命周期
- 或者说使用了 ThreadLocal, 后续又不再调用 set、get
- 解决: 每一次使用完毕都 remove