容器中的设计模式:
- 迭代器模式;[例子:容器循环所使用的 Iterator 迭代器]
- 设配器模式:简单的说就是将原本两个不相干的类联系起来(类似于充电器,连接插座和用电器,将电压变成电器匹配的量级);[例子:Arrays.asList() 将数组转换成List]
默认长度 & 扩容大小:
- List
默认大小 | 默认扩容 | 备注 | |
---|---|---|---|
ArrayList | 10 | 原来的1.5倍 | 由数组实现,因此支持快速随机访问;扩容操作需要将原数组整个复制到新数组中,代价很高;删除元素是将删除index后的元素复制到index的位置,代价很高; |
Vector | 原来的2倍 | 与ArrayList相似,但是使用了synchronized进行同步,是线程安全的 |
得到一个线程 安全的ArrayList替代方案:
① Collections.synchronizedList()
② concurrent 并发包下的 CopyOnWriteArrayList 类
CopyOnWriteArrayList :
写操作在一个复制的数组上进行,读操作还是在原始数组中进行,读写分离,互不影响。 写操作需要加锁,防止并发写入时导致写入数据丢失。 写操作结束之后需要把原始数组指向新的复制数组。
适用场景:CopyOnWriteArrayList 在写操作的同时允许读操作,大大提高了读操作的性能,因此很适合读多写少的应用场景。
但是 CopyOnWriteArrayList 有其缺陷:
- 内存占用:在写操作时需要复制一个新的数组,使得内存占用为原来的两倍左右;
- 数据不一致:读操作不能读取实时性的数据,因为部分写操作的数据还未同步到读数组中。
所以 CopyOnWriteArrayList 不适合内存敏感以及对实时性要求很高的场景。
LinkedList:
基于双向链表实现,使用 Node 存储链表节点信息。
1 | private static class Node<E> { |
链表不支持随机访问,但插入删除只需要改变指针。
HashMap:
参数 | 含义 |
---|---|
capacity | table 的容量大小,默认为 16。需要注意的是 capacity 必须保证为 2 的 n 次方。 |
size | 键值对数量。 |
threshold | size 的临界值,当 size 大于等于 threshold 就必须进行扩容操作。 |
loadFactor | 装载因子,table 能够使用的比例,threshold = (int)(capacity* loadFactor)。 |
https://github.com/CyC2018/CS-Notes/blob/master/notes/Java%20%E5%AE%B9%E5%99%A8.md (源码分析:1.7为主)
https://github.com/Snailclimb/JavaGuide/blob/master/docs/java/collection/HashMap.md (详细讲解)
从 JDK 1.8 开始,一个桶存储的链表长度大于等于 8 时会将链表转换为红黑树。
这里需要注意:1.7 put方法在链表中采用头插法,即当数组位置相同,而key不同,就在这个链表的头部插入该元素;1.8中则是在链表尾部插入
为什么要从头插法改成尾插法?
A.因为头插法会造成死链,参考链接https://blog.csdn.net/chenyiminnanjing/article/details/82706942
B.JDK7用头插是考虑到了一个所谓的热点数据的点(新插入的数据可能会更早用到),但这其实是个伪命题,因为JDK7中rehash的时候,旧链表迁移新链表的时候,如果在新表的数组索引位置相同,则链表元素会倒置(就是因为头插) 所以最后的结果 还是打乱了插入的顺序 所以总的来看支撑JDK7使用头插的这点原因也不足以支撑下去了 所以就干脆换成尾插 一举多得
与 Hashtable 的比较
- Hashtable 使用 synchronized 来进行同步。
- HashMap 可以插入键为 null 的 Entry。
- HashMap 的迭代器是 fail-fast 迭代器。
- HashMap 不能保证随着时间的推移 Map 中的元素次序是不变的。
ConcurrentHashMap:
存储结构
https://mp.weixin.qq.com/s/AHWzboztt53ZfFZmsSnMSw (详细讲解)
Java7 中 ConcruuentHashMap 使用的分段锁,也就是每一个 Segment 上同时只有一个线程可以操作,每一个 Segment 都是一个类似 HashMap 数组的结构,它可以扩容,它的冲突会转化为链表。但是 Segment 的个数一但初始化就不能改变。
Segment 继承自 ReentrantLock。
Java8 中的 ConcruuentHashMap 使用的 Synchronized 锁加 CAS 的机制。结构也由 Java7 中的 Segment 数组 + HashEntry 数组 + 链表 进化成了 Node 数组 + 链表 / 红黑树,Node 是类似于一个 HashEntry 的结构。它的冲突再达到一定大小时会转化成红黑树,在冲突小于一定数量时又退回链表。
JDK8之后,ConcurrentHashMap舍弃了ReentrantLock,而重新使用了synchronized。其原因大致有一下几点:
- 加入多个分段锁浪费内存空间。
- 生产环境中, map 在放入时竞争同一个锁的概率非常小,分段锁反而会造成更新等操作的长时间等待。
- 为了提高 GC 的效率
新的ConcurrentHashMap中使用synchronized关键字+CAS操作保证了线程安全;详细信息 https://my.oschina.net/pingpangkuangmo/blog/817973#h2_12
1 | static final class Segment<K,V> extends ReentrantLock implements Serializable { |
默认的并发级别为 16,也就是说默认创建 16 个 Segment。
size 操作
每个 Segment 维护了一个 count 变量来统计该 Segment 中的键值对个数。
1 | /** |
在执行 size 操作时,需要遍历所有 Segment 然后把 count 累计起来。
ConcurrentHashMap 在执行 size 操作时先尝试不加锁,如果连续两次不加锁操作得到的结果一致,那么可以认为这个结果是正确的。
尝试次数使用 RETRIES_BEFORE_LOCK 定义,该值为 2,retries 初始值为 -1,因此尝试次数为 3。
如果尝试的次数超过 3 次,就需要对每个 Segment 加锁。
1 | /** |
JDK 1.8 的改动
JDK 1.7 使用分段锁机制来实现并发更新操作,核心类为 Segment,它继承自重入锁 ReentrantLock,并发度与 Segment 数量相等。
JDK 1.8 使用了 CAS 操作来支持更高的并发度,在 CAS 操作失败时使用内置锁 synchronized。
并且 JDK 1.8 的实现也在链表过长时会转换为红黑树。
LinkedHashMap:
继承自 HashMap,因此具有和 HashMap 一样的快速查找特性。
内部维护了一个双向链表,用来维护插入顺序或者 LRU 顺序。
1 | /** |
accessOrder 决定了顺序,默认为 false,此时维护的是插入顺序。
1 | final boolean accessOrder; |
LinkedHashMap 最重要的是以下用于维护顺序的函数,它们会在 put、get 等方法中调用。
1 | void afterNodeAccess(Node<K,V> p) { } |
afterNodeAccess()
当一个节点被访问时,如果 accessOrder 为 true,则会将该节点移到链表尾部。也就是说指定为 LRU 顺序之后,在每次访问一个节点时,会将这个节点移到链表尾部,保证链表尾部是最近访问的节点,那么链表首部就是最近最久未使用的节点。
1 | void afterNodeAccess(Node<K,V> e) { // move node to last |
afterNodeInsertion()
在 put 等操作之后执行,当 removeEldestEntry() 方法返回 true 时会移除最晚的节点,也就是链表首部节点 first。
evict 只有在构建 Map 的时候才为 false,在这里为 true。
1 | void afterNodeInsertion(boolean evict) { // possibly remove eldest |
removeEldestEntry() 默认为 false,如果需要让它为 true,需要继承 LinkedHashMap 并且覆盖这个方法的实现,这在实现 LRU 的缓存中特别有用,通过移除最近最久未使用的节点,从而保证缓存空间足够,并且缓存的数据都是热点数据。
1 | protected boolean removeEldestEntry(Map.Entry<K,V> eldest) { |
LRU 缓存
以下是使用 LinkedHashMap 实现的一个 LRU 缓存:
- 设定最大缓存空间 MAX_ENTRIES 为 3;
- 使用 LinkedHashMap 的构造函数将 accessOrder 设置为 true,开启 LRU 顺序;
- 覆盖 removeEldestEntry() 方法实现,在节点多于 MAX_ENTRIES 就会将最近最久未使用的数据移除。
1 | class LRUCache<K, V> extends LinkedHashMap<K, V> { |