mirror of
https://github.com/CyC2018/Interview-Notebook.git
synced 2026-11-02 20:33:42 +00:00
Update Java 容器 - 进阶.md
补充 JDK 1.7 HashMap 源码与 ConcurrentHashMap 存储结构;调整 ArrayList 序列化、size 操作及 ConcurrentCache 的说明与代码;更新参考链接。
This commit is contained in:
+224
-20
@@ -12,7 +12,7 @@
|
||||
|
||||
## 三、源码分析
|
||||
|
||||
以下源码基于 JDK 1.8,ConcurrentHashMap 的 size 操作基于 JDK 1.7。
|
||||
以下源码基于 JDK 1.8;标题写明 JDK 1.7 的小节和 ConcurrentHashMap 的 size 操作基于 JDK 7u 的最终版本。
|
||||
|
||||
### ArrayList
|
||||
|
||||
@@ -20,15 +20,13 @@
|
||||
|
||||
主线见[Java 容器 · ArrayList 的序列化](Java%20容器.md#4-序列化)。
|
||||
|
||||
ArrayList 基于数组实现,并且具有动态扩容特性,因此保存元素的数组不一定都会被使用,那么就没必要全部进行序列化。
|
||||
|
||||
保存元素的数组 elementData 使用 transient 修饰,该关键字声明数组默认不会被序列化。
|
||||
elementData 用 transient 修饰,不参与默认的序列化:
|
||||
|
||||
```java
|
||||
transient Object[] elementData; // non-private to simplify nested class access
|
||||
```
|
||||
|
||||
ArrayList 实现了 writeObject() 和 readObject() 来控制只序列化数组中有元素填充那部分内容。
|
||||
writeObject() 和 readObject() 只处理前 size 个元素。writeObject() 末尾比较 modCount,期间发生了结构修改就抛出 ConcurrentModificationException,即主线中的 [Fail-Fast](Java%20容器.md#5-fail-fast)。
|
||||
|
||||
```java
|
||||
private void readObject(java.io.ObjectInputStream s)
|
||||
@@ -75,7 +73,7 @@ private void writeObject(java.io.ObjectOutputStream s)
|
||||
}
|
||||
```
|
||||
|
||||
序列化时需要使用 ObjectOutputStream 的 writeObject() 将对象转换为字节流并输出。而 writeObject() 方法在传入的对象存在 writeObject() 的时候会去反射调用该对象的 writeObject() 来实现序列化。反序列化使用的是 ObjectInputStream 的 readObject() 方法,原理类似。
|
||||
序列化时需要使用 ObjectOutputStream 的 writeObject() 将对象转换为字节流并输出。对象的类定义了私有的 writeObject() 时,ObjectOutputStream 通过反射调用它,由它代替默认的序列化过程;ArrayList 的 writeObject() 先用 defaultWriteObject() 写出 size 等非 transient 字段,再逐个写出元素。反序列化使用的是 ObjectInputStream 的 readObject() 方法,原理类似。
|
||||
|
||||
```java
|
||||
ArrayList list = new ArrayList();
|
||||
@@ -120,11 +118,211 @@ static final int tableSizeFor(int cap) {
|
||||
}
|
||||
```
|
||||
|
||||
代码先减 1,是为了让本来就是 2 的幂的值保持不变:传入 16 时 n 为 15,结果仍为 16,不减 1 会得到 32。JDK 21 改为 -1 >>> Integer.numberOfLeadingZeros(cap - 1),一步得到同样的掩码,结果相同。
|
||||
|
||||
#### JDK 1.7 的 HashMap
|
||||
|
||||
主线以 JDK 8 为准,两者的区别见[Java 容器 · HashMap](Java%20容器.md#hashmap)。以下源码取自 JDK 7u 的最终版本,省略了泛型的类型转换。
|
||||
|
||||
Entry 有 hash、key、value、next 四个字段,table 是 Entry 数组:
|
||||
|
||||
```java
|
||||
transient Entry[] table = EMPTY_TABLE;
|
||||
|
||||
static class Entry<K,V> implements Map.Entry<K,V> {
|
||||
final K key;
|
||||
V value;
|
||||
Entry<K,V> next;
|
||||
int hash;
|
||||
|
||||
Entry(int h, K k, V v, Entry<K,V> n) {
|
||||
value = v;
|
||||
next = n;
|
||||
key = k;
|
||||
hash = h;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
put() 先在桶中查找相同的键,找不到再调用 addEntry() 插入。键为 null 时由 putForNullKey() 放在第 0 个桶:
|
||||
|
||||
```java
|
||||
public V put(K key, V value) {
|
||||
if (table == EMPTY_TABLE) {
|
||||
inflateTable(threshold);
|
||||
}
|
||||
// 键为 null 单独处理
|
||||
if (key == null)
|
||||
return putForNullKey(value);
|
||||
int hash = hash(key);
|
||||
// 确定桶下标
|
||||
int i = indexFor(hash, table.length);
|
||||
// 先找出是否已经存在键为 key 的键值对,如果存在的话就更新这个键值对的值为 value
|
||||
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
|
||||
Object k;
|
||||
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
|
||||
V oldValue = e.value;
|
||||
e.value = value;
|
||||
e.recordAccess(this);
|
||||
return oldValue;
|
||||
}
|
||||
}
|
||||
|
||||
modCount++;
|
||||
// 插入新键值对
|
||||
addEntry(hash, key, value, i);
|
||||
return null;
|
||||
}
|
||||
|
||||
private V putForNullKey(V value) {
|
||||
for (Entry<K,V> e = table[0]; e != null; e = e.next) {
|
||||
if (e.key == null) {
|
||||
V oldValue = e.value;
|
||||
e.value = value;
|
||||
e.recordAccess(this);
|
||||
return oldValue;
|
||||
}
|
||||
}
|
||||
modCount++;
|
||||
addEntry(0, null, value, 0);
|
||||
return null;
|
||||
}
|
||||
```
|
||||
|
||||
addEntry() 在插入前判断是否扩容:size 大于等于 threshold、并且要插入的桶不为空时,容量扩为原来的两倍,再重新计算桶下标。createEntry() 使用头插法,新的键值对插在链表的头部:
|
||||
|
||||
```java
|
||||
void addEntry(int hash, K key, V value, int bucketIndex) {
|
||||
if ((size >= threshold) && (null != table[bucketIndex])) {
|
||||
resize(2 * table.length);
|
||||
hash = (null != key) ? hash(key) : 0;
|
||||
bucketIndex = indexFor(hash, table.length);
|
||||
}
|
||||
|
||||
createEntry(hash, key, value, bucketIndex);
|
||||
}
|
||||
|
||||
void createEntry(int hash, K key, V value, int bucketIndex) {
|
||||
Entry<K,V> e = table[bucketIndex];
|
||||
// 头插法,链表头部指向新的键值对
|
||||
table[bucketIndex] = new Entry<>(hash, key, value, e);
|
||||
size++;
|
||||
}
|
||||
```
|
||||
|
||||
hash() 对 hashCode 做四次移位和异或。hashSeed 默认为 0,只有设置了 jdk.map.althashing.threshold、并且容量达到这个值时才启用替代哈希,这时 String 键改用 sun.misc.Hashing.stringHash32()。indexFor() 用与运算代替取模:
|
||||
|
||||
```java
|
||||
final int hash(Object k) {
|
||||
int h = hashSeed;
|
||||
if (0 != h && k instanceof String) {
|
||||
return sun.misc.Hashing.stringHash32((String) k);
|
||||
}
|
||||
|
||||
h ^= k.hashCode();
|
||||
|
||||
// This function ensures that hashCodes that differ only by
|
||||
// constant multiples at each bit position have a bounded
|
||||
// number of collisions (approximately 8 at default load factor).
|
||||
h ^= (h >>> 20) ^ (h >>> 12);
|
||||
return h ^ (h >>> 7) ^ (h >>> 4);
|
||||
}
|
||||
|
||||
static int indexFor(int h, int length) {
|
||||
return h & (length-1);
|
||||
}
|
||||
```
|
||||
|
||||
resize() 创建新数组,transfer() 把旧数组中的节点逐个取出,重新计算桶下标后头插到新数组中,同一个桶中节点的顺序会反转。rehash 只在替代哈希的开关变化时为 true,默认为 false。两个线程同时执行 transfer() 时链表可能成环,过程见[Java 容器 · 线程安全](Java%20容器.md#10-线程安全)。
|
||||
|
||||
```java
|
||||
void resize(int newCapacity) {
|
||||
Entry[] oldTable = table;
|
||||
int oldCapacity = oldTable.length;
|
||||
if (oldCapacity == MAXIMUM_CAPACITY) {
|
||||
threshold = Integer.MAX_VALUE;
|
||||
return;
|
||||
}
|
||||
|
||||
Entry[] newTable = new Entry[newCapacity];
|
||||
transfer(newTable, initHashSeedAsNeeded(newCapacity));
|
||||
table = newTable;
|
||||
threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
|
||||
}
|
||||
|
||||
void transfer(Entry[] newTable, boolean rehash) {
|
||||
Entry[] src = table;
|
||||
int newCapacity = newTable.length;
|
||||
for (int j = 0; j < src.length; j++) {
|
||||
Entry<K,V> e = src[j];
|
||||
while(null != e) {
|
||||
Entry<K,V> next = e.next;
|
||||
if (rehash) {
|
||||
e.hash = null == e.key ? 0 : hash(e.key);
|
||||
}
|
||||
int i = indexFor(e.hash, newCapacity);
|
||||
e.next = newTable[i];
|
||||
newTable[i] = e;
|
||||
e = next;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
### ConcurrentHashMap
|
||||
|
||||
#### JDK 1.7 的存储结构
|
||||
|
||||
主线见[Java 容器 · JDK 1.8 的改动](Java%20容器.md#3-jdk-18-的改动),以下源码取自 JDK 7u 的最终版本。
|
||||
|
||||
每个 Segment 维护着一个 HashEntry 数组,HashEntry 的 value 和 next 用 volatile 修饰,读操作通常不需要加锁:
|
||||
|
||||
```java
|
||||
static final class HashEntry<K,V> {
|
||||
final int hash;
|
||||
final K key;
|
||||
volatile V value;
|
||||
volatile HashEntry<K,V> next;
|
||||
}
|
||||
```
|
||||
|
||||
Segment 继承自 ReentrantLock。
|
||||
|
||||
```java
|
||||
static final class Segment<K,V> extends ReentrantLock implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 2249069246763182397L;
|
||||
|
||||
static final int MAX_SCAN_RETRIES =
|
||||
Runtime.getRuntime().availableProcessors() > 1 ? 64 : 1;
|
||||
|
||||
transient volatile HashEntry<K,V>[] table;
|
||||
|
||||
transient int count;
|
||||
|
||||
transient int modCount;
|
||||
|
||||
transient int threshold;
|
||||
|
||||
final float loadFactor;
|
||||
}
|
||||
```
|
||||
|
||||
```java
|
||||
final Segment<K,V>[] segments;
|
||||
```
|
||||
|
||||
默认的并发级别为 16,Segment 数组的长度为 16。构造时只创建第 0 个 Segment,其余由 ensureSegment() 在第一次用到时创建。
|
||||
|
||||
```java
|
||||
static final int DEFAULT_CONCURRENCY_LEVEL = 16;
|
||||
```
|
||||
|
||||
|
||||
#### size 操作
|
||||
|
||||
这是 JDK 1.7 的实现,主线见[Java 容器 · size 操作](Java%20容器.md#2-size-操作),JDK 1.8 的计数方式见[JDK 1.8 的改动](Java%20容器.md#3-jdk-18-的改动)。
|
||||
这是 JDK 1.7 的实现,主线见[Java 容器 · JDK 1.8 的改动](Java%20容器.md#3-jdk-18-的改动),JDK 1.8 的计数方式见[Java 容器 · size 操作](Java%20容器.md#2-size-操作)。
|
||||
|
||||
每个 Segment 维护了一个 count 变量来统计该 Segment 中的键值对个数。
|
||||
|
||||
@@ -138,11 +336,9 @@ transient int count;
|
||||
|
||||
在执行 size 操作时,需要遍历所有 Segment 然后把 count 累计起来。
|
||||
|
||||
ConcurrentHashMap 在执行 size 操作时先尝试不加锁,如果连续两次不加锁操作得到的结果一致,那么可以认为这个结果是正确的。
|
||||
ConcurrentHashMap 在执行 size 操作时先尝试不加锁,每一轮同时累加各 Segment 的 count 和 modCount。相邻两轮的 modCount 之和相同,说明统计期间没有修改,这一轮的 count 之和就是正确的。只比较 count 不够:一次删除加一次插入后元素个数不变,但 modCount 变了。
|
||||
|
||||
尝试次数使用 RETRIES_BEFORE_LOCK 定义,该值为 2,retries 初始值为 -1,因此尝试次数为 3。
|
||||
|
||||
如果尝试的次数超过 3 次,就需要对每个 Segment 加锁。
|
||||
RETRIES_BEFORE_LOCK 为 2,retries 初始值为 -1,因此不加锁最多统计 3 轮;3 轮中都没有相邻两轮一致时,第 4 轮统计前对每个 Segment 加锁,加锁后不会再有修改,统计结果一定正确。
|
||||
|
||||
```java
|
||||
/**
|
||||
@@ -181,7 +377,7 @@ public int size() {
|
||||
overflow = true;
|
||||
}
|
||||
}
|
||||
// 连续两次得到的结果一致,则认为这个结果是正确的
|
||||
// 相邻两轮的 modCount 之和相同,说明统计期间没有修改
|
||||
if (sum == last)
|
||||
break;
|
||||
last = sum;
|
||||
@@ -206,10 +402,12 @@ Tomcat 中的 ConcurrentCache 使用了 WeakHashMap 来实现缓存功能。
|
||||
|
||||
ConcurrentCache 采取的是分代缓存:
|
||||
|
||||
- 经常使用的对象放入 eden 中,eden 使用 ConcurrentHashMap 实现,不用担心会被回收(伊甸园);
|
||||
- 不常用的对象放入 longterm,longterm 使用 WeakHashMap 实现,这些老对象会被垃圾收集器回收;
|
||||
- 新放入的对象放在 eden 中,eden 使用 ConcurrentHashMap 实现,强引用其中的对象,不会被回收;
|
||||
- eden 满了以后,其中的对象整批移入 longterm,longterm 使用 WeakHashMap 实现,键没有其它强引用时会被垃圾收集器回收;
|
||||
- 当调用 get() 方法时,会先从 eden 区获取,如果没有找到的话再到 longterm 获取,当从 longterm 获取到就把对象放入 eden 中,从而保证经常被访问的节点不容易被回收;
|
||||
- 当调用 put() 方法时,如果 eden 的大小超过了 size,那么就将 eden 中的所有对象都放入 longterm 中,利用虚拟机回收掉一部分不经常使用的对象。
|
||||
- 当调用 put() 方法时,如果 eden 的大小达到了 size,就将 eden 中的所有对象都放入 longterm 中,再清空 eden。
|
||||
|
||||
WeakHashMap 不是线程安全的,eden 是 ConcurrentHashMap 并不能保护它,所以访问 longterm 时都要用 synchronized (longterm) 加锁。以下代码取自 Tomcat 9 的 org.apache.el.util.ConcurrentCache,省略了注释:
|
||||
|
||||
```java
|
||||
public final class ConcurrentCache<K, V> {
|
||||
@@ -229,16 +427,21 @@ public final class ConcurrentCache<K, V> {
|
||||
public V get(K k) {
|
||||
V v = this.eden.get(k);
|
||||
if (v == null) {
|
||||
v = this.longterm.get(k);
|
||||
if (v != null)
|
||||
synchronized (longterm) {
|
||||
v = this.longterm.get(k);
|
||||
}
|
||||
if (v != null) {
|
||||
this.eden.put(k, v);
|
||||
}
|
||||
}
|
||||
return v;
|
||||
}
|
||||
|
||||
public void put(K k, V v) {
|
||||
if (this.eden.size() >= size) {
|
||||
this.longterm.putAll(this.eden);
|
||||
synchronized (longterm) {
|
||||
this.longterm.putAll(this.eden);
|
||||
}
|
||||
this.eden.clear();
|
||||
}
|
||||
this.eden.put(k, v);
|
||||
@@ -249,7 +452,8 @@ public final class ConcurrentCache<K, V> {
|
||||
## 参考资料
|
||||
|
||||
- [JDK 8 源码 java.util](https://github.com/openjdk/jdk8u/tree/master/jdk/src/share/classes/java/util)
|
||||
- [JDK 7u 源码:HashMap.java](https://github.com/openjdk/jdk7u/blob/master/jdk/src/share/classes/java/util/HashMap.java)
|
||||
- [JDK 7u 源码:ConcurrentHashMap.java](https://github.com/openjdk/jdk7u/blob/master/jdk/src/share/classes/java/util/concurrent/ConcurrentHashMap.java)
|
||||
- [JDK 21 源码:ArrayList.java](https://github.com/openjdk/jdk21u/blob/master/src/java.base/share/classes/java/util/ArrayList.java)
|
||||
- [JDK 21 源码:HashMap.java](https://github.com/openjdk/jdk21u/blob/master/src/java.base/share/classes/java/util/HashMap.java)
|
||||
- [探索 ConcurrentHashMap 高并发性的实现机制](https://www.ibm.com/developerworks/cn/java/java-lo-concurrenthashmap/)
|
||||
- [The principle of ConcurrentHashMap analysis](http://www.programering.com/a/MDO3QDNwATM.html)
|
||||
- [Tomcat 9 源码:ConcurrentCache.java](https://github.com/apache/tomcat/blob/9.0.x/java/org/apache/el/util/ConcurrentCache.java)
|
||||
|
||||
Reference in New Issue
Block a user