mirror of
https://github.com/CyC2018/Interview-Notebook.git
synced 2026-10-29 23:23:43 +00:00
Update Java 容器 - 进阶.md
新增进阶篇,收录 ArrayList 序列化、HashMap 计算数组容量、ConcurrentHashMap 的 size 操作和 WeakHashMap 缓存示例等源码细节。
This commit is contained in:
@@ -0,0 +1,255 @@
|
||||
# Java 容器 - 进阶
|
||||
<!-- GFM-TOC -->
|
||||
* [Java 容器 - 进阶](#java-容器---进阶)
|
||||
* [三、源码分析](#三源码分析)
|
||||
* [ArrayList](#arraylist)
|
||||
* [HashMap](#hashmap)
|
||||
* [ConcurrentHashMap](#concurrenthashmap)
|
||||
* [WeakHashMap](#weakhashmap)
|
||||
* [参考资料](#参考资料)
|
||||
<!-- GFM-TOC -->
|
||||
|
||||
|
||||
## 三、源码分析
|
||||
|
||||
以下源码基于 JDK 1.8,ConcurrentHashMap 的 size 操作基于 JDK 1.7。
|
||||
|
||||
### ArrayList
|
||||
|
||||
#### 序列化
|
||||
|
||||
主线见[Java 容器 · ArrayList 的序列化](Java%20容器.md#4-序列化)。
|
||||
|
||||
ArrayList 基于数组实现,并且具有动态扩容特性,因此保存元素的数组不一定都会被使用,那么就没必要全部进行序列化。
|
||||
|
||||
保存元素的数组 elementData 使用 transient 修饰,该关键字声明数组默认不会被序列化。
|
||||
|
||||
```java
|
||||
transient Object[] elementData; // non-private to simplify nested class access
|
||||
```
|
||||
|
||||
ArrayList 实现了 writeObject() 和 readObject() 来控制只序列化数组中有元素填充那部分内容。
|
||||
|
||||
```java
|
||||
private void readObject(java.io.ObjectInputStream s)
|
||||
throws java.io.IOException, ClassNotFoundException {
|
||||
elementData = EMPTY_ELEMENTDATA;
|
||||
|
||||
// Read in size, and any hidden stuff
|
||||
s.defaultReadObject();
|
||||
|
||||
// Read in capacity
|
||||
s.readInt(); // ignored
|
||||
|
||||
if (size > 0) {
|
||||
// be like clone(), allocate array based upon size not capacity
|
||||
ensureCapacityInternal(size);
|
||||
|
||||
Object[] a = elementData;
|
||||
// Read in all elements in the proper order.
|
||||
for (int i=0; i<size; i++) {
|
||||
a[i] = s.readObject();
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```java
|
||||
private void writeObject(java.io.ObjectOutputStream s)
|
||||
throws java.io.IOException{
|
||||
// Write out element count, and any hidden stuff
|
||||
int expectedModCount = modCount;
|
||||
s.defaultWriteObject();
|
||||
|
||||
// Write out size as capacity for behavioural compatibility with clone()
|
||||
s.writeInt(size);
|
||||
|
||||
// Write out all elements in the proper order.
|
||||
for (int i=0; i<size; i++) {
|
||||
s.writeObject(elementData[i]);
|
||||
}
|
||||
|
||||
if (modCount != expectedModCount) {
|
||||
throw new ConcurrentModificationException();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
序列化时需要使用 ObjectOutputStream 的 writeObject() 将对象转换为字节流并输出。而 writeObject() 方法在传入的对象存在 writeObject() 的时候会去反射调用该对象的 writeObject() 来实现序列化。反序列化使用的是 ObjectInputStream 的 readObject() 方法,原理类似。
|
||||
|
||||
```java
|
||||
ArrayList list = new ArrayList();
|
||||
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(file));
|
||||
oos.writeObject(list);
|
||||
```
|
||||
|
||||
### HashMap
|
||||
|
||||
#### 计算数组容量
|
||||
|
||||
主线见[Java 容器 · 计算数组容量](Java%20容器.md#7-计算数组容量)。
|
||||
|
||||
HashMap 构造函数允许用户传入的容量不是 2 的 n 次方,因为它可以自动地将传入的容量转换为 2 的 n 次方。
|
||||
|
||||
先考虑如何求一个数的掩码,对于 10010000,它的掩码为 11111111,可以使用以下方法得到:
|
||||
|
||||
```text
|
||||
mask |= mask >> 1 11011000
|
||||
mask |= mask >> 2 11111110
|
||||
mask |= mask >> 4 11111111
|
||||
```
|
||||
|
||||
mask+1 是大于原始数字的最小的 2 的 n 次方。
|
||||
|
||||
```text
|
||||
num 10010000
|
||||
mask+1 100000000
|
||||
```
|
||||
|
||||
以下是 HashMap 中计算数组容量的代码:
|
||||
|
||||
```java
|
||||
static final int tableSizeFor(int cap) {
|
||||
int n = cap - 1;
|
||||
n |= n >>> 1;
|
||||
n |= n >>> 2;
|
||||
n |= n >>> 4;
|
||||
n |= n >>> 8;
|
||||
n |= n >>> 16;
|
||||
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
|
||||
}
|
||||
```
|
||||
|
||||
### ConcurrentHashMap
|
||||
|
||||
#### size 操作
|
||||
|
||||
这是 JDK 1.7 的实现,主线见[Java 容器 · size 操作](Java%20容器.md#2-size-操作),JDK 1.8 的计数方式见[JDK 1.8 的改动](Java%20容器.md#3-jdk-18-的改动)。
|
||||
|
||||
每个 Segment 维护了一个 count 变量来统计该 Segment 中的键值对个数。
|
||||
|
||||
```java
|
||||
/**
|
||||
* The number of elements. Accessed only either within locks
|
||||
* or among other volatile reads that maintain visibility.
|
||||
*/
|
||||
transient int count;
|
||||
```
|
||||
|
||||
在执行 size 操作时,需要遍历所有 Segment 然后把 count 累计起来。
|
||||
|
||||
ConcurrentHashMap 在执行 size 操作时先尝试不加锁,如果连续两次不加锁操作得到的结果一致,那么可以认为这个结果是正确的。
|
||||
|
||||
尝试次数使用 RETRIES_BEFORE_LOCK 定义,该值为 2,retries 初始值为 -1,因此尝试次数为 3。
|
||||
|
||||
如果尝试的次数超过 3 次,就需要对每个 Segment 加锁。
|
||||
|
||||
```java
|
||||
/**
|
||||
* Number of unsynchronized retries in size and containsValue
|
||||
* methods before resorting to locking. This is used to avoid
|
||||
* unbounded retries if tables undergo continuous modification
|
||||
* which would make it impossible to obtain an accurate result.
|
||||
*/
|
||||
static final int RETRIES_BEFORE_LOCK = 2;
|
||||
|
||||
public int size() {
|
||||
// Try a few times to get accurate count. On failure due to
|
||||
// continuous async changes in table, resort to locking.
|
||||
final Segment<K,V>[] segments = this.segments;
|
||||
int size;
|
||||
boolean overflow; // true if size overflows 32 bits
|
||||
long sum; // sum of modCounts
|
||||
long last = 0L; // previous sum
|
||||
int retries = -1; // first iteration isn't retry
|
||||
try {
|
||||
for (;;) {
|
||||
// 超过尝试次数,则对每个 Segment 加锁
|
||||
if (retries++ == RETRIES_BEFORE_LOCK) {
|
||||
for (int j = 0; j < segments.length; ++j)
|
||||
ensureSegment(j).lock(); // force creation
|
||||
}
|
||||
sum = 0L;
|
||||
size = 0;
|
||||
overflow = false;
|
||||
for (int j = 0; j < segments.length; ++j) {
|
||||
Segment<K,V> seg = segmentAt(segments, j);
|
||||
if (seg != null) {
|
||||
sum += seg.modCount;
|
||||
int c = seg.count;
|
||||
if (c < 0 || (size += c) < 0)
|
||||
overflow = true;
|
||||
}
|
||||
}
|
||||
// 连续两次得到的结果一致,则认为这个结果是正确的
|
||||
if (sum == last)
|
||||
break;
|
||||
last = sum;
|
||||
}
|
||||
} finally {
|
||||
if (retries > RETRIES_BEFORE_LOCK) {
|
||||
for (int j = 0; j < segments.length; ++j)
|
||||
segmentAt(segments, j).unlock();
|
||||
}
|
||||
}
|
||||
return overflow ? Integer.MAX_VALUE : size;
|
||||
}
|
||||
```
|
||||
|
||||
### WeakHashMap
|
||||
|
||||
#### ConcurrentCache
|
||||
|
||||
主线见[Java 容器 · WeakHashMap](Java%20容器.md#weakhashmap)。
|
||||
|
||||
Tomcat 中的 ConcurrentCache 使用了 WeakHashMap 来实现缓存功能。
|
||||
|
||||
ConcurrentCache 采取的是分代缓存:
|
||||
|
||||
- 经常使用的对象放入 eden 中,eden 使用 ConcurrentHashMap 实现,不用担心会被回收(伊甸园);
|
||||
- 不常用的对象放入 longterm,longterm 使用 WeakHashMap 实现,这些老对象会被垃圾收集器回收;
|
||||
- 当调用 get() 方法时,会先从 eden 区获取,如果没有找到的话再到 longterm 获取,当从 longterm 获取到就把对象放入 eden 中,从而保证经常被访问的节点不容易被回收;
|
||||
- 当调用 put() 方法时,如果 eden 的大小超过了 size,那么就将 eden 中的所有对象都放入 longterm 中,利用虚拟机回收掉一部分不经常使用的对象。
|
||||
|
||||
```java
|
||||
public final class ConcurrentCache<K, V> {
|
||||
|
||||
private final int size;
|
||||
|
||||
private final Map<K, V> eden;
|
||||
|
||||
private final Map<K, V> longterm;
|
||||
|
||||
public ConcurrentCache(int size) {
|
||||
this.size = size;
|
||||
this.eden = new ConcurrentHashMap<>(size);
|
||||
this.longterm = new WeakHashMap<>(size);
|
||||
}
|
||||
|
||||
public V get(K k) {
|
||||
V v = this.eden.get(k);
|
||||
if (v == null) {
|
||||
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);
|
||||
this.eden.clear();
|
||||
}
|
||||
this.eden.put(k, v);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 参考资料
|
||||
|
||||
- [JDK 8 源码 java.util](https://github.com/openjdk/jdk8u/tree/master/jdk/src/share/classes/java/util)
|
||||
- [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)
|
||||
Reference in New Issue
Block a user