diff --git a/notes/Java 容器.md b/notes/Java 容器.md index 0edaab90..03130b84 100644 --- a/notes/Java 容器.md +++ b/notes/Java 容器.md @@ -9,11 +9,10 @@ * [ArrayList](#arraylist) * [Vector](#vector) * [LinkedList](#linkedlist) + * [HashMap](#hashmap) + * [ConcurrentHashMap](#concurrenthashmap) * [LinkedHashMap](#linkedhashmap) * [TreeMap](#treemap) - * [HashMap](#hashmap) - * [ConcurrentHashMap - JDK 1.7](#concurrenthashmap---jdk-17) - * [ConcurrentHashMap - JDK 1.8](#concurrenthashmap---jdk-18) * [参考资料](#参考资料) @@ -122,39 +121,41 @@ public class ArrayList extends AbstractList implements List, RandomAccess, Cloneable, java.io.Serializable ``` -基于数组实现,保存元素的数组使用 transient 修饰,该关键字声明数组默认不会被序列化。ArrayList 具有动态扩容特性,因此保存元素的数组不一定都会被使用,那么就没必要全部进行序列化。ArrayList 重写了 writeObject() 和 readObject() 来控制只序列化数组中有元素填充那部分内容。 - -```java -transient Object[] elementData; // non-private to simplify nested class access -``` - 数组的默认大小为 10。 ```java private static final int DEFAULT_CAPACITY = 10; ``` -删除元素时需要调用 System.arraycopy() 对元素进行复制,因此删除操作成本很高。 +### 2. 序列化 + +基于数组实现,保存元素的数组使用 transient 修饰,该关键字声明数组默认不会被序列化。ArrayList 具有动态扩容特性,因此保存元素的数组不一定都会被使用,那么就没必要全部进行序列化。ArrayList 重写了 writeObject() 和 readObject() 来控制只序列化数组中有元素填充那部分内容。 ```java -public E remove(int index) { - rangeCheck(index); - - modCount++; - E oldValue = elementData(index); - - int numMoved = size - index - 1; - if (numMoved > 0) - System.arraycopy(elementData, index+1, elementData, index, numMoved); - elementData[--size] = null; // clear to let GC do its work - - return oldValue; -} +transient Object[] elementData; // non-private to simplify nested class access ``` -添加元素时使用 ensureCapacity() 方法来保证容量足够,如果不够时,需要使用 grow() 方法进行扩容,使得新容量为旧容量的 1.5 倍(oldCapacity + (oldCapacity >> 1))。扩容操作需要把原数组整个复制到新数组中,因此最好在创建 ArrayList 对象时就指定大概的容量大小,减少扩容操作的次数。 +### 3. 扩容 + +添加元素时使用 ensureCapacityInternal() 方法来保证容量足够,如果不够时,需要使用 grow() 方法进行扩容,新容量的大小为 `oldCapacity + (oldCapacity >> 1)`,也就是旧容量的 1.5 倍。 + +扩容操作需要调用 `Arrays.copyOf()` 把原数组整个复制到新数组中,因此最好在创建 ArrayList 对象时就指定大概的容量大小,减少扩容操作的次数。 ```java +public boolean add(E e) { + ensureCapacityInternal(size + 1); // Increments modCount!! + elementData[size++] = e; + return true; +} + +private void ensureCapacityInternal(int minCapacity) { + if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) { + minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity); + } + + ensureExplicitCapacity(minCapacity); +} + private void ensureExplicitCapacity(int minCapacity) { modCount++; @@ -176,7 +177,27 @@ private void grow(int minCapacity) { } ``` -### 2. Fail-Fast +### 4. 删除元素 + +需要调用 System.arraycopy() 将 index+1 后面的元素都复制到 index 位置上,复制的代价很高。 + +```java +public E remove(int index) { + rangeCheck(index); + + modCount++; + E oldValue = elementData(index); + + int numMoved = size - index - 1; + if (numMoved > 0) + System.arraycopy(elementData, index+1, elementData, index, numMoved); + elementData[--size] = null; // clear to let GC do its work + + return oldValue; +} +``` + +### 5. Fail-Fast modCount 用来记录 ArrayList 结构发生变化的次数。结构发生变化是指添加或者删除至少一个元素的所有操作,或者是调整内部数组的大小,仅仅只是设置元素的值不算结构发生变化。 @@ -203,39 +224,85 @@ private void writeObject(java.io.ObjectOutputStream s) } ``` -### 3. 和 Vector 的区别 +## Vector + +[Vector.java](https://github.com/CyC2018/JDK-Source-Code/tree/master/src/Vector.java) + +### 1. 同步 + +它的实现与 ArrayList 类似,但是使用了 synchronized 进行同步。 + +```java +public synchronized boolean add(E e) { + modCount++; + ensureCapacityHelper(elementCount + 1); + elementData[elementCount++] = e; + return true; +} + +public synchronized E get(int index) { + if (index >= elementCount) + throw new ArrayIndexOutOfBoundsException(index); + + return elementData(index); +} +``` + +### 2. ArrayList 与 Vector - Vector 和 ArrayList 几乎是完全相同的,唯一的区别在于 Vector 是同步的,因此开销就比 ArrayList 要大,访问速度更慢。最好使用 ArrayList 而不是 Vector,因为同步操作完全可以由程序员自己来控制; - Vector 每次扩容请求其大小的 2 倍空间,而 ArrayList 是 1.5 倍。 -为了获得线程安全的 ArrayList,可以调用 Collections.synchronizedList(new ArrayList<>()); 返回一个线程安全的 ArrayList,也可以使用 concurrent 并发包下的 CopyOnWriteArrayList 类; +### 3. Vector 替代方案 -### 4. 和 LinkedList 的区别 +为了获得线程安全的 ArrayList,可以使用 Collections.synchronizedList(); 得到一个线程安全的 ArrayList,也可以使用 concurrent 并发包下的 CopyOnWriteArrayList 类; -- ArrayList 基于动态数组实现,LinkedList 基于双向链表实现; -- ArrayList 支持随机访问,LinkedList 不支持; -- LinkedList 在任意位置添加删除元素更快。 +```java +List list = new ArrayList<>(); +List synList = Collections.synchronizedList(list); +``` -## Vector - -[Vector.java](https://github.com/CyC2018/JDK-Source-Code/tree/master/src/Vector.java) +```java +List list = new CopyOnWriteArrayList(); +``` ## LinkedList [LinkedList.java](https://github.com/CyC2018/JDK-Source-Code/tree/master/src/LinkedList.java) -## LinkedHashMap +### 1. 概览 -[LinkedHashMap.java](https://github.com/CyC2018/JDK-Source-Code/tree/master/src/HashMap.java) +基于双向链表实现,内部使用 Node 来存储链表节点信息。 -## TreeMap +```java +private static class Node { + E item; + Node next; + Node prev; +} +``` -[TreeMap.java](https://github.com/CyC2018/JDK-Source-Code/tree/master/src/TreeMap.java) +每个链表存储了 Head 和 Tail 指针: + +```java +transient Node first; +transient Node last; +``` + +

+ +### 2. ArrayList 与 LinkedList + +- ArrayList 基于动态数组实现,LinkedList 基于双向链表实现; +- ArrayList 支持随机访问,LinkedList 不支持; +- LinkedList 在任意位置添加删除元素更快。 ## HashMap [HashMap.java](https://github.com/CyC2018/JDK-Source-Code/tree/master/src/HashMap.java) +为了便于理解,以下内容以 JDK 1.7 为主。 + ### 1. 存储结构 使用拉链法来解决冲突,内部包含了一个 Entry 类型的数组 table,数组中的每个位置被当成一个桶。 @@ -248,28 +315,26 @@ transient Entry[] table;

-JDK 1.8 使用 Node 类型存储一个键值对,它依然继承自 Entry,因此可以按照上面的存储结构来理解。 - ```java -static class Node implements Map.Entry { - final int hash; +static class Entry implements Map.Entry { final K key; V value; - Node next; + Entry next; + int hash; - Node(int hash, K key, V value, Node next) { - this.hash = hash; - this.key = key; - this.value = value; - this.next = next; + Entry(int h, K k, V v, Entry n) { + value = v; + next = n; + key = k; + hash = h; } - public final K getKey() { return key; } - public final V getValue() { return value; } - public final String toString() { return key + "=" + value; } + public final K getKey() { + return key; + } - public final int hashCode() { - return Objects.hashCode(key) ^ Objects.hashCode(value); + public final V getValue() { + return value; } public final V setValue(V newValue) { @@ -279,16 +344,42 @@ static class Node implements Map.Entry { } public final boolean equals(Object o) { - if (o == this) - return true; - if (o instanceof Map.Entry) { - Map.Entry e = (Map.Entry)o; - if (Objects.equals(key, e.getKey()) && - Objects.equals(value, e.getValue())) + if (!(o instanceof Map.Entry)) + return false; + Map.Entry e = (Map.Entry)o; + Object k1 = getKey(); + Object k2 = e.getKey(); + if (k1 == k2 || (k1 != null && k1.equals(k2))) { + Object v1 = getValue(); + Object v2 = e.getValue(); + if (v1 == v2 || (v1 != null && v1.equals(v2))) return true; } return false; } + + public final int hashCode() { + return Objects.hashCode(getKey()) ^ Objects.hashCode(getValue()); + } + + public final String toString() { + return getKey() + "=" + getValue(); + } + + /** + * This method is invoked whenever the value in an entry is + * overwritten by an invocation of put(k,v) for a key k that's already + * in the HashMap. + */ + void recordAccess(HashMap m) { + } + + /** + * This method is invoked whenever the entry is + * removed from the table. + */ + void recordRemoval(HashMap m) { + } } ``` @@ -304,22 +395,166 @@ map.put("K3", "V3"); - 新建一个 HashMap,默认大小为 16; - 插入 <K1,V1> 键值对,先计算 K1 的 hashCode 为 115,使用除留余数法得到所在的桶下标 115%16=3。 - 插入 <K2,V2> 键值对,先计算 K2 的 hashCode 为 118,使用除留余数法得到所在的桶下标 118%16=6。 -- 插入 <K3,V3> 键值对,先计算 K3 的 hashCode 为 118,使用除留余数法得到所在的桶下标 118%16=6,插在 <K2,V2> 后面。 +- 插入 <K3,V3> 键值对,先计算 K3 的 hashCode 为 118,使用除留余数法得到所在的桶下标 118%16=6,插在 <K2,V2> 前面。 -

+应该注意到链表的插入是以头插法方式进行的,例如上面的 不是插在 后面,而是插入在链表头部。 查找需要分成两步进行: - 计算键值对所在的桶; - 在链表上顺序查找,时间复杂度显然和链表的长度成正比。 -### 3. 链表转红黑树 +

-应该注意到,从 JDK 1.8 开始,一个桶存储的链表长度大于 8 时会将链表转换为红黑树。 +### 3. put 操作 -### 4. 扩容 +```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 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; + } + } -因为从 JDK 1.8 开始引入了红黑树,因此扩容操作较为复杂,为了便于理解,以下内容使用 JDK 1.7 的内容。 + modCount++; + // 插入新键值对 + addEntry(hash, key, value, i); + return null; +} +``` + +HashMap 允许插入键位 null 的键值对,因为无法调用 null 的 hashCode(),也就无法确定该键值对的桶下标,只能通过强制指定一个桶下标来存放。HashMap 使用第 0 个桶存放键为 null 的键值对。 + +```java +private V putForNullKey(V value) { + for (Entry 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; +} +``` + +使用链表的头插法,也就是新的键值对插在链表的头部,而不是链表的尾部。 + +```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 e = table[bucketIndex]; + // 头插法,链表头部指向新的键值对 + table[bucketIndex] = new Entry<>(hash, key, value, e); + size++; +} +``` + +```java +Entry(int h, K k, V v, Entry n) { + value = v; + next = n; + key = k; + hash = h; +} +``` + +### 4. 确定桶下标 + +很多操作都需要先确定一个键值对所在的桶下标。 + +```java +int hash = hash(key); +int i = indexFor(hash, table.length); +``` + +(一)计算 hash 值 + +```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); +} +``` + +```java +public final int hashCode() { + return Objects.hashCode(key) ^ Objects.hashCode(value); +} +``` + +(二)取模 + +令 x = 1<<4,即 x 为 2 的 4 次方,它具有以下性质: + +``` +x : 00010000 +x-1 : 00001111 +``` + +令一个数 y 与 x-1 做与运算,可以去除 y 位级表示的第 4 位以上数: + +``` +y : 10110010 +x-1 : 00001111 +y&(x-1) : 00000010 +``` + +这个性质和 y 对 x 取模效果是一样的: + +``` +x : 00010000 +y : 10110010 +y%x : 00000010 +``` + +我们知道,位运算的代价比求模运算小的多,因此在进行这种计算时能用位运算的话能带来更高的性能。 + +确定桶下标的最后一步是将 key 的 hash 值对桶个数取模:hash%capacity,如果能保证 capacity 为 2 的幂次方,那么就可以将这个操作转换位位运算。 + +```java +static int indexFor(int h, int length) { + return h & (length-1); +} +``` + +### 5. 扩容-基本原理 设 HashMap 的 table 长度为 M,需要存储的键值对数量为 N,如果哈希函数满足均匀性的要求,那么每条链表的长度大约为 N/M,因此平均查找次数的复杂度为 O(N/M)。 @@ -399,69 +634,9 @@ void transfer(Entry[] newTable) { } ``` -### 5. 确定桶下标 - -很多操作都需要先确定一个键值对所在的桶下标,这个操作需要分三步进行。 - -(一)调用 hashCode() - -```java -public final int hashCode() { - return Objects.hashCode(key) ^ Objects.hashCode(value); -} -``` - -(二)高位运算 - -将 hashCode 的高 16 位和低 16 位进行异或操作,使得在数组比较小时,也能保证高低位都参与到了哈希计算中。 - -```java -static final int hash(Object key) { - int h; - return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); -} -``` - -(三)除留余数 - -令 x = 1<<4,即 x 为 2 的 4 次方,它具有以下性质: - -``` -x : 00010000 -x-1 : 00001111 -``` - -令一个数 y 与 x-1 做与运算,可以去除 y 位级表示的第 4 位以上数: - -``` -y : 10110010 -x-1 : 00001111 -y&(x-1) : 00000010 -``` - -这个性质和 y 对 x 取模效果是一样的: - -``` -x : 00010000 -y : 10110010 -y%x : 00000010 -``` - -我们知道,位运算的代价比求模运算小的多,因此在进行这种计算时能用位运算的话能带来更高的性能。 - -拉链法需要使用除留余数法来得到桶下标,也就是需要进行以下计算:hash%capacity,如果能保证 capacity 为 2 的幂次方,那么就可以将这个操作转换位位运算。 - -以下操作在 JDK 1.8 中没有,但是原理上相同。 - -```java -static int indexFor(int h, int length) { - return h & (length-1); -} -``` - ### 6. 扩容-重新计算桶下标 -在进行扩容时,需要把 Node 重新放到对应的桶上。HashMap 使用了一个特殊的机制,可以降低重新计算桶下标的操作。 +在进行扩容时,需要把键值对重新放到对应的桶上。HashMap 使用了一个特殊的机制,可以降低重新计算桶下标的操作。 假设原数组长度 capacity 为 8,扩容之后 new capacity 为 16: @@ -470,7 +645,7 @@ capacity : 00010000 new capacity : 00100000 ``` -对于一个 Key,它的 hashCode 如果在第 6 位上为 0,那么除留余数得到的结果和之前一样;如果为 1,那么得到的结果为原来的结果 + 8。 +对于一个 Key,它的 hash 如果在第 6 位上为 0,那么取模得到的结果和之前一样;如果为 1,那么得到的结果为原来的结果 + 8。 ### 7. 扩容-计算数组容量 @@ -505,11 +680,11 @@ static final int tableSizeFor(int cap) { } ``` -### 7. null 值 +### 8. 链表转红黑树 -HashMap 允许有一个 Node 的 Key 为 null,该 Node 一定会放在第 0 个桶的位置,因为这个 Key 无法计算 hashCode(),因此只能规定一个桶让它存放。 +应该注意到,从 JDK 1.8 开始,一个桶存储的链表长度大于 8 时会将链表转换为红黑树。 -### 8. 与 HashTable 的区别 +### 9. HashMap 与 HashTable - HashTable 是同步的,它使用了 synchronized 来进行同步。它也是线程安全的,多个线程可以共享同一个 HashTable。HashMap 不是同步的,但是可以使用 ConcurrentHashMap,它是 HashTable 的替代,而且比 HashTable 可扩展性更好。 - HashMap 可以插入键为 null 的 Entry。 @@ -517,18 +692,12 @@ HashMap 允许有一个 Node 的 Key 为 null,该 Node 一定会放在第 0 - 由于 Hashtable 是线程安全的也是 synchronized,所以在单线程环境下它比 HashMap 要慢。 - HashMap 不能保证随着时间的推移 Map 中的元素次序是不变的。 -## ConcurrentHashMap - JDK 1.7 +## ConcurrentHashMap [ConcurrentHashMap.java](https://github.com/CyC2018/JDK-Source-Code/blob/master/src/1.7/ConcurrentHashMap.java) -ConcurrentHashMap 和 HashMap 实现上类似,最主要的差别是 ConcurrentHashMap 采用了分段锁,每个分段锁维护着几个桶,多个线程可以同时访问不同分段锁上的桶。 - -相比于 HashTable 和用同步包装器包装的 HashMap(Collections.synchronizedMap(new HashMap())),ConcurrentHashMap 拥有更高的并发性。在 HashTable 和由同步包装器包装的 HashMap 中,使用一个全局的锁来同步不同线程间的并发访问。同一时间点,只能有一个线程持有锁,也就是说在同一时间点,只能有一个线程能访问容器。这虽然保证多线程间的安全并发访问,但同时也导致对容器的访问变成串行化的了。 - ### 1. 存储结构 -和 HashMap 类似。 - ```java static final class HashEntry { final int hash; @@ -540,6 +709,8 @@ static final class HashEntry { Segment 继承自 ReentrantLock,每个 Segment 维护着多个 HashEntry。 +ConcurrentHashMap 和 HashMap 实现上类似,最主要的差别是 ConcurrentHashMap 采用了分段锁,每个分段锁维护着几个桶,多个线程可以同时访问不同分段锁上的桶,从而使其并发度更高(并发度就是 Segment 的个数)。 + ```java static final class Segment extends ReentrantLock implements Serializable { @@ -572,101 +743,98 @@ static final int DEFAULT_CONCURRENCY_LEVEL = 16;

-### 2. HashEntry 的不可变性 +### 2. size 操作 -HashEntry 类的 value 域被声明为 Volatile 型,Java 的内存模型可以保证:某个写线程对 value 域的写入马上可以被后续的某个读线程 “看” 到。在 ConcurrentHashMap 中,不允许用 null 作为键和值,当读线程读到某个 HashEntry 的 value 域的值为 null 时,便知道产生了冲突——发生了重排序现象,需要加锁后重新读入这个 value 值。这些特性互相配合,使得读线程即使在不加锁状态下,也能正确访问 ConcurrentHashMap。 - -非结构性修改操作只是更改某个 HashEntry 的 value 域的值。由于对 Volatile 变量的写入操作将与随后对这个变量的读操作进行同步。当一个写线程修改了某个 HashEntry 的 value 域后,另一个读线程读这个值域,Java 内存模型能够保证读线程读取的一定是更新后的值。所以,写线程对链表的非结构性修改能够被后续不加锁的读线程 “看到”。 - -对 ConcurrentHashMap 做结构性修改,实质上是对某个桶指向的链表做结构性修改。如果能够确保:在读线程遍历一个链表期间,写线程对这个链表所做的结构性修改不影响读线程继续正常遍历这个链表。那么读 / 写线程之间就可以安全并发访问这个 ConcurrentHashMap。 - -结构性修改操作包括 put,remove,clear。下面我们分别分析这三个操作。 - -clear 操作只是把 ConcurrentHashMap 中所有的桶 “置空”,每个桶之前引用的链表依然存在,只是桶不再引用到这些链表(所有链表的结构并没有被修改)。正在遍历某个链表的读线程依然可以正常执行对该链表的遍历。 - -put 操作如果需要插入一个新节点到链表中时 , 会在链表头部插入这个新节点。此时,链表中的原有节点的链接并没有被修改。也就是说:插入新健 / 值对到链表中的操作不会影响读线程正常遍历这个链表。 - -在以下链表中删除 C 节点,C 节点之后的所有节点都原样保留,C 节点之前的所有节点都被克隆到新的链表中,并且顺序被反转。可以注意到,在执行 remove 操作时,原始链表并没有被修改,也就是说,读线程不会受到执行 remove 操作的并发写线程的干扰。 - -

- -

- -综上,可以得出一个结论:写线程对某个链表的结构性修改不会影响其他的并发读线程对这个链表的遍历访问。 - -### 3. Volatile 变量 - -由于内存可见性问题,未正确同步的情况下,写线程写入的值可能并不为后续的读线程可见。 - -下面以写线程 M 和读线程 N 来说明 ConcurrentHashMap 如何协调读 / 写线程间的内存可见性问题。 - -

- -假设线程 M 在写入了 volatile 型变量 count 后,线程 N 读取了这个 volatile 型变量 count。 - -根据 happens-before 关系法则中的程序次序法则,A appens-before 于 B,C happens-before D。 - -根据 Volatile 变量法则,B happens-before C。 - -根据传递性,连接上面三个 happens-before 关系得到:A appens-before 于 B; B appens-before C;C happens-before D。也就是说:写线程 M 对链表做的结构性修改,在读线程 N 读取了同一个 volatile 变量后,对线程 N 也是可见的了。 - -虽然线程 N 是在未加锁的情况下访问链表。Java 的内存模型可以保证:只要之前对链表做结构性修改操作的写线程 M 在退出写方法前写 volatile 型变量 count,读线程 N 在读取这个 volatile 型变量 count 后,就一定能 “看到” 这些修改。 - -ConcurrentHashMap 中,每个 Segment 都有一个变量 count。它用来统计 Segment 中的 HashEntry 的个数。这个变量被声明为 volatile。 +每个 Segment 维护了一个 count 变量来统计该 Segment 中的键值对个数。 ```java -transient volatile int count; +/** + * The number of elements. Accessed only either within locks + * or among other volatile reads that maintain visibility. + */ +transient int count; ``` -所有不加锁读方法,在进入读方法时,首先都会去读这个 count 变量。比如下面的 get 方法: +在执行 size 操作时,需要遍历所有 Segment 然后把 count 累计起来。 + +ConcurrentHashMap 在执行 size 操作时先尝试不加锁,如果连续两次不加锁操作得到的结果一致,那么可以认为这个结果是正确的。 + +尝试次数使用 RETRIES_BEFORE_LOCK 定义,该值为 2,retries 初始值为 -1,因此尝试次数为 3。 + +如果尝试的次数超过 3 次,就需要对每个 Segment 加锁。 ```java -V get(Object key, int hash) { - if(count != 0) { // 首先读 count 变量 - HashEntry e = getFirst(hash); - while(e != null) { - if(e.hash == hash && key.equals(e.key)) { - V v = e.value; - if(v != null) - return v; - // 如果读到 value 域为 null,说明发生了重排序,加锁后重新读取 - return readValueUnderLock(e); - } - e = e.next; - } - } - return null; + +/** + * 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[] 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 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; } ``` -在 ConcurrentHashMap 中,所有执行写操作的方法(put, remove, clear),在对链表做结构性修改之后,在退出写方法前都会去写这个 count 变量。所有未加锁的读操作(get, contains, containsKey)在读方法中,都会首先去读取这个 count 变量。 -根据 Java 内存模型,对同一个 volatile 变量的写 / 读操作可以确保:写线程写入的值,能够被之后未加锁的读线程 “看到”。 - -这个特性和前面介绍的 HashEntry 对象的不变性相结合,使得在 ConcurrentHashMap 中,读线程在读取散列表时,基本不需要加锁就能成功获得需要的值。这两个特性相配合,不仅减少了请求同一个锁的频率(读操作一般不需要加锁就能够成功获得值),也减少了持有同一个锁的时间(只有读到 value 域的值为 null 时 ,读线程才需要加锁后重读)。 - -### 4. 小结 - -ConcurrentHashMap 的高并发性主要来自于三个方面: - -- 用分离锁实现多个线程间的更深层次的共享访问。 -- 用 HashEntery 对象的不变性来降低执行读操作的线程在遍历链表期间对加锁的需求。 -- 通过对同一个 Volatile 变量的写 / 读访问,协调不同线程间读 / 写操作的内存可见性。 - -## ConcurrentHashMap - JDK 1.8 +### 3. JDK 1.8 的改动 [ConcurrentHashMap.java](https://github.com/CyC2018/JDK-Source-Code/blob/master/src/ConcurrentHashMap.java) -JDK 1.7 分段锁机制来实现并发更新操作,核心类为 Segment,它继承自重入锁 ReentrantLock。 +JDK 1.7 使用分段锁机制来实现并发更新操作,核心类为 Segment,它继承自重入锁 ReentrantLock,并发程度与 Segment 数量相等。 -JDK 1.8 的实现不是用了 Segment,Segment 属于重入锁 ReentrantLock。而是使用了内置锁 synchronized,主要是出于以下考虑: - -1. synchronized 的锁粒度更低; -2. synchronized 优化空间更大; -3. 在大量数据操作的情况下,ReentrantLock 会开销更多的内存。 +JDK 1.8 使用了 CAS 操作来支持更高的并发度,在 CAS 操作失败时使用内置锁 synchronized。 并且 JDK 1.8 的实现也在链表过长时会转换为红黑树。 +## LinkedHashMap + +[LinkedHashMap.java](https://github.com/CyC2018/JDK-Source-Code/tree/master/src/HashMap.java) + +## TreeMap + +[TreeMap.java](https://github.com/CyC2018/JDK-Source-Code/tree/master/src/TreeMap.java) + # 参考资料 - Eckel B. Java 编程思想 [M]. 机械工业出版社, 2002. @@ -679,4 +847,5 @@ JDK 1.8 的实现不是用了 Segment,Segment 属于重入锁 ReentrantLock。 - [探索 ConcurrentHashMap 高并发性的实现机制](https://www.ibm.com/developerworks/cn/java/java-lo-concurrenthashmap/) - [HashMap 相关面试题及其解答](https://www.jianshu.com/p/75adf47958a7) - [Java 集合细节(二):asList 的缺陷](http://wiki.jikexueyuan.com/project/java-enhancement/java-thirtysix.html) +- [Java Collection Framework – The LinkedList Class](http://javaconceptoftheday.com/java-collection-framework-linkedlist-class/) diff --git a/pics/49d6de7b-0d0d-425c-9e49-a1559dc23b10.png b/pics/49d6de7b-0d0d-425c-9e49-a1559dc23b10.png new file mode 100644 index 00000000..eea3c6b4 Binary files /dev/null and b/pics/49d6de7b-0d0d-425c-9e49-a1559dc23b10.png differ diff --git a/pics/HowLinkedListWorks.png b/pics/HowLinkedListWorks.png new file mode 100644 index 00000000..2640d80d Binary files /dev/null and b/pics/HowLinkedListWorks.png differ