|
3 | 3 | - [前言](#前言) |
4 | 4 | - [一、概述](#一概述) |
5 | 5 | - [集合框架图](#集合框架图) |
6 | | - - [Collection](#collection) |
7 | | - - [Map](#map) |
| 6 | + - [Collection](#Collection) |
| 7 | + - [Map](#Map) |
8 | 8 | - [工具类](#工具类) |
9 | 9 | - [通用实现](#通用实现) |
10 | 10 | - [二、深入源码分析](#二深入源码分析) |
11 | | - - [ArrayList](#arraylist) |
| 11 | + - [ArrayList](#ArrayList) |
12 | 12 | - [1. 概览](#1-概览) |
13 | 13 | - [2. 序列化](#2-序列化) |
14 | 14 | - [3. 扩容](#3-扩容) |
15 | 15 | - [4. 删除元素](#4-删除元素) |
16 | | - - [5. Fail-Fast](#5-fail-fast) |
17 | | - - [Vector](#vector) |
| 16 | + - [5. Fail-Fast](#5-Fail-Fast) |
| 17 | + - [Vector](#Vector) |
18 | 18 | - [1. 同步](#1-同步) |
19 | | - - [2. ArrayList 与 Vector](#2-arraylist-与-vector) |
20 | | - - [3. Vector 替代方案](#3-vector-替代方案) |
21 | | - - [synchronizedList](#synchronizedlist) |
22 | | - - [CopyOnWriteArrayList](#copyonwritearraylist) |
23 | | - - [LinkedList](#linkedlist) |
| 19 | + - [2. ArrayList 与 Vector](#2-ArrayList-与-Vector) |
| 20 | + - [3. Vector 替代方案](#3-Vector-替代方案) |
| 21 | + - [synchronizedList](#synchronizedList) |
| 22 | + - [CopyOnWriteArrayList](#CopyOnWriteArrayList) |
| 23 | + - [LinkedList](#LinkedList) |
24 | 24 | - [1. 概览](#1-概览-1) |
25 | 25 | - [2. add()](#2-add) |
26 | 26 | - [3. remove()](#3-remove) |
27 | 27 | - [4. get()](#4-get) |
28 | 28 | - [5. 总结](#5-总结) |
29 | | - - [6. ArrayList 与 LinkedList](#6-arraylist-与-linkedlist) |
30 | | - - [HashMap](#hashmap) |
| 29 | + - [6. ArrayList 与 LinkedList](#6-ArrayList-与-LinkedList) |
| 30 | + - [HashMap](#HashMap) |
31 | 31 | - [1. 存储结构](#1-存储结构) |
32 | 32 | - [JDK1.7 的存储结构](#jdk17-的存储结构) |
33 | 33 | - [JDK1.8 的存储结构](#jdk18-的存储结构) |
34 | 34 | - [2. 重要参数](#2-重要参数) |
35 | 35 | - [3. 确定哈希桶数组索引位置](#3-确定哈希桶数组索引位置) |
36 | | - - [4. 分析HashMap的put方法](#4-分析hashmap的put方法) |
| 36 | + - [4. 分析HashMap的put方法](#4-分析HashMap的put方法) |
37 | 37 | - [5. 扩容机制](#5-扩容机制) |
38 | 38 | - [6. 线程安全性](#6-线程安全性) |
39 | 39 | - [7. JDK1.8与JDK1.7的性能对比](#7-jdk18与jdk17的性能对比) |
40 | 40 | - [8. Hash较均匀的情况](#8-hash较均匀的情况) |
41 | 41 | - [9. Hash极不均匀的情况](#9-hash极不均匀的情况) |
42 | | - - [10. HashMap与HashTable](#10-hashmap与hashtable) |
| 42 | + - [10. HashMap与Hashtable](#10-HashMap与Hashtable) |
43 | 43 | - [11. 小结](#11-小结) |
44 | | - - [ConcurrentHashMap](#concurrenthashmap) |
| 44 | + - [ConcurrentHashMap](#ConcurrentHashMap) |
45 | 45 | - [1. 概述](#1-概述) |
46 | 46 | - [2. 存储结构](#2-存储结构) |
47 | 47 | - [2. size 操作](#2-size-操作) |
|
52 | 52 | - [2. 构造函数](#2-构造函数) |
53 | 53 | - [3. add()](#3-add) |
54 | 54 | - [4. 总结](#4-总结) |
55 | | - - [LinkedHashSet and LinkedHashMap](#linkedhashset-and-linkedhashmap) |
| 55 | + - [LinkedHashSet and LinkedHashMap](#LinkedHashSet-and-LinkedHashMap) |
56 | 56 | - [1. 概览](#1-概览-2) |
57 | 57 | - [2. get()](#2-get) |
58 | 58 | - [3. put()](#3-put) |
59 | 59 | - [4. remove()](#4-remove) |
60 | | - - [5. LinkedHashSet](#5-linkedhashset) |
61 | | - - [6. LinkedHashMap经典用法](#6-linkedhashmap经典用法) |
| 60 | + - [5. LinkedHashSet](#5-LinkedHashSet) |
| 61 | + - [6. LinkedHashMap经典用法](#6-LinkedHashMap经典用法) |
62 | 62 | - [三、容器中的设计模式](#三容器中的设计模式) |
63 | 63 | - [迭代器模式](#迭代器模式) |
64 | 64 | - [适配器模式](#适配器模式) |
65 | 65 | - [四、面试指南](#四面试指南) |
66 | | - - [1. ArrayList和LinkedList区别](#1-arraylist和linkedlist区别) |
67 | | - - [2. HashMap和HashTable区别,HashMap的key类型](#2-hashmap和hashtable区别hashmap的key类型) |
68 | | - - [3. HashMap和ConcurrentHashMap](#3-hashmap和concurrenthashmap) |
69 | | - - [4. Hashtable的原理](#4-hashtable的原理) |
| 66 | + - [1. ArrayList和LinkedList区别](#1-ArrayList和LinkedList区别) |
| 67 | + - [2. HashMap和Hashtable区别,HashMap的key类型](#2-HashMap和Hashtable区别HashMap的key类型) |
| 68 | + - [3. HashMap和ConcurrentHashMap](#3-HashMap和ConcurrentHashMap) |
| 69 | + - [4. Hashtable的原理](#4-Hashtable的原理) |
70 | 70 | - [5. Hash冲突的解决办法](#5-hash冲突的解决办法) |
71 | 71 | - [6. 什么是迭代器](#6-什么是迭代器) |
72 | 72 | - [7. 构造相同hash的字符串进行攻击,这种情况应该怎么处理?JDK7如何处理](#7-构造相同hash的字符串进行攻击这种情况应该怎么处理jdk7如何处理) |
73 | | - - [8. Hashmap为什么大小是2的幂次](#8-hashmap为什么大小是2的幂次) |
| 73 | + - [8. HashMap为什么大小是2的幂次](#8-HashMap为什么大小是2的幂次) |
74 | 74 | - [更新日志](#更新日志) |
75 | 75 |
|
76 | 76 | <!-- /TOC --> |
|
127 | 127 |
|
128 | 128 | - `TreeMap`:线程不同步,基于 **红黑树** (Red-Black tree)的 NavigableMap 实现,能够把它保存的记录根据键排序,默认是按键值的升序排序,也可以指定排序的比较器,当用 Iterator 遍历 TreeMap 时,得到的记录是排过序的。 |
129 | 129 | - **TreeMap 底层通过红黑树(Red-Black tree)实现**,也就意味着 `containsKey()`, `get()`, `put()`, `remove()` 都有着 `log(n)` 的时间复杂度。其具体算法实现参照了《算法导论》。 |
130 | | -- `HashTable`:**线程安全**,HashMap 的迭代器 \(Iterator\) 是 `fail-fast` 迭代器。**HashTable 不能存储 NULL 的 key 和 value。** |
| 130 | +- `Hashtable`:**线程安全**,HashMap 的迭代器 \(Iterator\) 是 `fail-fast` 迭代器。**Hashtable 不能存储 NULL 的 key 和 value。** |
131 | 131 | - `HashMap`:线程不同步。根据 `key` 的 `hashcode` 进行存储,内部使用静态内部类 `Node` 的数组进行存储,默认初始大小为 16,每次扩大一倍。当发生 Hash 冲突时,采用拉链法(链表)。JDK 1.8中:**当单个桶中元素个数大于等于8时,链表实现改为红黑树实现;当元素个数小于6时,变回链表实现。由此来防止hashCode攻击。** |
132 | 132 | - Java HashMap 采用的是冲突链表方式。 |
133 | 133 | - HashMap 是 Hashtable 的轻量级实现,可以接受为 null 的键值 (key\) 和值 \(value\),而 Hashtable 不允许。 |
@@ -1164,9 +1164,9 @@ class Key implements Comparable<Key> { |
1164 | 1164 |
|
1165 | 1165 |
|
1166 | 1166 |
|
1167 | | -### 10. HashMap与HashTable |
| 1167 | +### 10. HashMap与Hashtable |
1168 | 1168 |
|
1169 | | -1. HashTable 使用 synchronized 来进行同步。 |
| 1169 | +1. Hashtable 使用 synchronized 来进行同步。 |
1170 | 1170 | 2. HashMap 可以插入键为 null 的 Entry。 |
1171 | 1171 | 3. HashMap 的迭代器是 fail-fast 迭代器。 |
1172 | 1172 | 4. HashMap 不能保证随着时间的推移 Map 中的元素次序是不变的。 |
@@ -1196,17 +1196,17 @@ class Key implements Comparable<Key> { |
1196 | 1196 |
|
1197 | 1197 | ### 1. 概述 |
1198 | 1198 |
|
1199 | | - 众所周知,哈希表是中非常高效,复杂度为 O(1) 的数据结构,在 Java 开发中,我们最常见到最频繁使用的就是 HashMap 和 HashTable,但是在线程竞争激烈的并发场景中使用都不够合理。 |
| 1199 | + 众所周知,哈希表是中非常高效,复杂度为 O(1) 的数据结构,在 Java 开发中,我们最常见到最频繁使用的就是 HashMap 和 Hashtable,但是在线程竞争激烈的并发场景中使用都不够合理。 |
1200 | 1200 |
|
1201 | 1201 | **HashMap** :先说 HashMap,HashMap 是**线程不安全**的,在并发环境下,可能会形成**环状链表**(扩容时可能造成),导致 get 操作时,cpu 空转,所以,在并发环境中使 用HashMap 是非常危险的。 |
1202 | 1202 |
|
1203 | | - **HashTable** : HashTable 和 HashMap的实现原理几乎一样,差别无非是:(1)HashTable不允许key和value为null;(2)HashTable是线程安全的。 |
| 1203 | + **Hashtable** : Hashtable 和 HashMap的实现原理几乎一样,差别无非是:(1)Hashtable不允许key和value为null;(2)Hashtable是线程安全的。 |
1204 | 1204 |
|
1205 | | - 但是 HashTable 线程安全的策略实现代价却太大了,简单粗暴,get/put 所有相关操作都是 synchronized 的,这相当于给整个哈希表加了一把大锁,多线程访问时候,只要有一个线程访问或操作该对象,那其他线程只能阻塞,相当于将所有的操作串行化,在竞争激烈的并发场景中性能就会非常差。 |
| 1205 | + 但是 Hashtable 线程安全的策略实现代价却太大了,简单粗暴,get/put 所有相关操作都是 synchronized 的,这相当于给整个哈希表加了一把大锁,多线程访问时候,只要有一个线程访问或操作该对象,那其他线程只能阻塞,相当于将所有的操作串行化,在竞争激烈的并发场景中性能就会非常差。 |
1206 | 1206 |
|
1207 | 1207 | <div align="center"> <img src="assets/hashtable-ds.png" width="600"/></div> |
1208 | 1208 |
|
1209 | | - HashTable 性能差主要是由于所有操作需要竞争同一把锁,而如果容器中有多把锁,每一把锁锁一段数据,这样在多线程访问时不同段的数据时,就不会存在锁竞争了,这样便可以有效地提高并发效率。这就是ConcurrentHashMap 所采用的 "**分段锁**" 思想。 |
| 1209 | + Hashtable 性能差主要是由于所有操作需要竞争同一把锁,而如果容器中有多把锁,每一把锁锁一段数据,这样在多线程访问时不同段的数据时,就不会存在锁竞争了,这样便可以有效地提高并发效率。这就是ConcurrentHashMap 所采用的 "**分段锁**" 思想。 |
1210 | 1210 |
|
1211 | 1211 | <div align="center"> <img src="assets/hashmap-ds.png" width="600"/></div><br/> |
1212 | 1212 |
|
@@ -1710,47 +1710,47 @@ List list = Arrays.asList(1,2,3); |
1710 | 1710 |
|
1711 | 1711 | - **ArrayList、LinkedList 和 Vector如何选择?** |
1712 | 1712 | - 当对数据的主要操作为索引或只在集合的末端增加、删除元素时,使用 ArrayList 或 Vector 效率比较高; |
1713 | | - - 当对数据的操作主要为制定位置的插入或删除操作时,使用 LinkedList 效率比较高; |
| 1713 | + - 当对数据的操作主要为指定位置的插入或删除操作时,使用 LinkedList 效率比较高; |
1714 | 1714 | - 当在多线程中使用容器时(即多个线程会同时访问该容器),选用 Vector 较为安全; |
1715 | 1715 |
|
1716 | 1716 |
|
1717 | 1717 |
|
1718 | | -## 2. HashMap和HashTable区别,HashMap的key类型 |
| 1718 | +## 2. HashMap和Hashtable区别,HashMap的key类型 |
1719 | 1719 |
|
1720 | | -- **Hash Map和HashTable的区别** |
| 1720 | +- **HashMap和Hashtable的区别** |
1721 | 1721 |
|
1722 | | - - Hashtable 的方法是同步的,HashMap 非同步,所以在多线程场合要手动同步 |
| 1722 | + - Hashtable 的方法是同步的,HashMap 非同步,所以在多线程场合要手动同步。 |
1723 | 1723 | - Hashtable 不允许 null 值 (key 和 value 都不可以),HashMap 允许 null 值( key 和 value 都可以)。 |
1724 | 1724 | - 两者的遍历方式大同小异,Hashtable 仅仅比 HashMap 多一个 elements 方法。 |
1725 | 1725 |
|
1726 | 1726 | - Hashtable 和 HashMap 都能通过 values() 方法返回一个 Collection ,然后进行遍历处理。 |
1727 | 1727 |
|
1728 | 1728 | - 两者也都可以通过 entrySet() 方法返回一个 Set , 然后进行遍历处理。 |
1729 | 1729 |
|
1730 | | - - HashTable 使用 Enumeration,HashMap 使用 Iterator。 |
| 1730 | + - Hashtable 使用 Enumeration,HashMap 使用 Iterator。 |
1731 | 1731 | - 哈希值的使用不同,Hashtable 直接使用对象的 hashCode。而 HashMap 重新计算hash值,而且用于代替求模。 |
1732 | 1732 | - Hashtable 中 hash 数组默认大小是11,增加的方式是 old*2+1。HashMap 中 hash 数组的默认大小是16,而且一定是 2 的指数。 |
1733 | | - - HashTable 基于 Dictionary 类,而 HashMap 基于 AbstractMap 类 |
| 1733 | + - Hashtable 基于 Dictionary 类,而 HashMap 基于 AbstractMap 类 |
1734 | 1734 |
|
1735 | 1735 | - **HashMap中的key可以是任何对象或数据类型吗** |
1736 | 1736 |
|
1737 | 1737 | - 可以为null,但不能是可变对象,如果是可变对象的话,对象中的属性改变,则对象 HashCode 也进行相应的改变,导致下次无法查找到已存在Map中的数据。 |
1738 | 1738 | - 如果可变对象在 HashMap 中被用作键,那就要小心在改变对象状态的时候,不要改变它的哈希值了。我们只需要保证成员变量的改变能保证该对象的哈希值不变即可。 |
1739 | 1739 |
|
1740 | | -- **HashTable是线程安全的么** |
| 1740 | +- **Hashtable是线程安全的么** |
1741 | 1741 |
|
1742 | | - - HashTable 是线程安全的,其实现是在对应的方法上添加了 synchronized 关键字进行修饰,由于在执行此方法的时候需要获得对象锁,则执行起来比较慢。所以现在如果为了保证线程安全的话,使用 CurrentHashMap。 |
| 1742 | + - Hashtable 是线程安全的,其实现是在对应的方法上添加了 synchronized 关键字进行修饰,由于在执行此方法的时候需要获得对象锁,则执行起来比较慢。所以现在如果为了保证线程安全的话,使用 CurrentHashMap。 |
1743 | 1743 |
|
1744 | 1744 |
|
1745 | 1745 |
|
1746 | 1746 | ## 3. HashMap和ConcurrentHashMap |
1747 | 1747 |
|
1748 | 1748 | - **HashMap和Concurrent HashMap区别?** |
1749 | | - - HashMa p是非线程安全的,CurrentHashMap 是线程安全的。 |
| 1749 | + - HashMap 是非线程安全的,CurrentHashMap 是线程安全的。 |
1750 | 1750 | - ConcurrentHashMap 将整个 Hash 桶进行了分段 segment,也就是将这个大的数组分成了几个小的片段segment,而且每个小的片段 segment 上面都有锁存在,那么在插入元素的时候就需要先找到应该插入到哪一个片段 segment,然后再在这个片段上面进行插入,而且这里还需要获取 segment 锁。 |
1751 | 1751 | - ConcurrentHashMap 让锁的粒度更精细一些,并发性能更好。 |
1752 | 1752 | - **ConcurrentHashMap 线程安全吗, ConcurrentHashMap如何保证 线程安全?** |
1753 | | - - HashTable 容器在竞争激烈的并发环境下表现出效率低下的原因是所有访问 HashTable 的线程都必须竞争同一把锁,那假如容器里有多把锁,每一把锁用于锁容器其中一部分数据,那么当多线程访问容器里不同数据段的数据时,线程间就不会存在锁竞争,从而可以有效的提高并发访问效率,这就是 ConcurrentHashMap 所使用的**分段锁**,首先将数据分成一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据的时候,其他段的数据也能被其他线程访问。 |
| 1753 | + - Hashtable 容器在竞争激烈的并发环境下表现出效率低下的原因是所有访问 Hashtable 的线程都必须竞争同一把锁,那假如容器里有多把锁,每一把锁用于锁容器其中一部分数据,那么当多线程访问容器里不同数据段的数据时,线程间就不会存在锁竞争,从而可以有效的提高并发访问效率,这就是 ConcurrentHashMap 所使用的**分段锁**,首先将数据分成一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据的时候,其他段的数据也能被其他线程访问。 |
1754 | 1754 | - get 操作的高效之处在于整个 get 过程不需要加锁,除非读到的值是空的才会加锁重读。**get 方法里将要使用的共享变量都定义成 volatile**,如用于统计当前 Segement 大小的 count 字段和用于存储值的 HashEntry 的 value。定义成 volatile 的变量,能够在线程之间保持可见性,能够被多线程同时读,并且保证不会读到过期的值,但是只能被单线程写(有一种情况可以被多线程写,就是写入的值不依赖于原值),在 get 操作里只需要读不需要写共享变量 count 和 value,所以可以不用加锁。 |
1755 | 1755 | - put 方法首先定位到 Segment,然后在 Segment 里进行插入操作。 |
1756 | 1756 | - 插入操作需要经历两个步骤:(1)判断是否需要对 Segment 里的 HashEntry 数组进行扩容;(2)定位添加元素的位置然后放在HashEntry数组里。 |
@@ -1783,9 +1783,9 @@ List list = Arrays.asList(1,2,3); |
1783 | 1783 |
|
1784 | 1784 | **Hashtable 与 HashMap 的简单比较** |
1785 | 1785 |
|
1786 | | -1. HashTable 基于 Dictionary 类,而 HashMap 是基于 AbstractMap。Dictionary 是任何可将键映射到相应值的类的抽象父类,而 AbstractMap 是基于 Map 接口的实现,它以最大限度地减少实现此接口所需的工作。 |
| 1786 | +1. Hashtable 基于 Dictionary 类,而 HashMap 是基于 AbstractMap。Dictionary 是任何可将键映射到相应值的类的抽象父类,而 AbstractMap 是基于 Map 接口的实现,它以最大限度地减少实现此接口所需的工作。 |
1787 | 1787 | 2. HashMap 的 key 和 value 都允许为 null,而 Hashtable 的 key 和 value 都不允许为 null。HashMap 遇到 key 为 null 的时候,调用 putForNullKey 方法进行处理,而对 value 没有处理;Hashtable遇到 null,直接返回 NullPointerException。 |
1788 | | -3. **Hashtable 方法是同步,而HashMap则不是**。我们可以看一下源码,Hashtable 中的几乎所有的 public 的方法都是 synchronized 的,而有些方法也是在内部通过 synchronized 代码块来实现。所以有人一般都建议如果是涉及到多线程同步时采用 HashTable,没有涉及就采用 HashMap,但是在 Collections 类中存在一个静态方法:**synchronizedMap()**,该方法创建了一个线程安全的 Map 对象,并把它作为一个封装的对象来返回。 |
| 1788 | +3. **Hashtable 方法是同步,而HashMap则不是**。我们可以看一下源码,Hashtable 中的几乎所有的 public 的方法都是 synchronized 的,而有些方法也是在内部通过 synchronized 代码块来实现。所以有人一般都建议如果是涉及到多线程同步时采用 Hashtable,没有涉及就采用 HashMap,但是在 Collections 类中存在一个静态方法:**synchronizedMap()**,该方法创建了一个线程安全的 Map 对象,并把它作为一个封装的对象来返回。 |
1789 | 1789 |
|
1790 | 1790 |
|
1791 | 1791 |
|
@@ -1871,15 +1871,15 @@ HashMap 会动态的使用一个专门 TreeMap 实现来替换掉它。 |
1871 | 1871 |
|
1872 | 1872 |
|
1873 | 1873 |
|
1874 | | -## 8. Hashmap为什么大小是2的幂次 |
| 1874 | +## 8. HashMap为什么大小是2的幂次 |
1875 | 1875 |
|
1876 | | -首先来看一下 hashmap 的 put 方法的源码 |
| 1876 | +首先来看一下 HashMap 的 put 方法的源码 |
1877 | 1877 |
|
1878 | 1878 | ```java |
1879 | 1879 | public V put(K key, V value) { |
1880 | 1880 | if (key == null) |
1881 | 1881 | return putForNullKey(value); //将空key的Entry加入到table[0]中 |
1882 | | - int hash = hash(key.hashCode()); //计算key.hashcode()的hash值,hash函数由hashmap自己实现 |
| 1882 | + int hash = hash(key.hashCode()); //计算key.hashcode()的hash值,hash函数由HashMap自己实现 |
1883 | 1883 | int i = indexFor(hash, table.length); //获取将要存放的数组下标 |
1884 | 1884 | /* |
1885 | 1885 | * for中的代码用于:当hash值相同且key相同的情况下,使用新值覆盖旧值(其实就是修改功能) |
@@ -1911,7 +1911,7 @@ static int indexFor(int h, int length) { |
1911 | 1911 |
|
1912 | 1912 | ``` |
1913 | 1913 |
|
1914 | | -**hashmap 始终将自己的桶保持在2<sup>n</sup>,这是为什么?indexFor这个方法解释了这个问题** |
| 1914 | +**HashMap 始终将自己的桶保持在2<sup>n</sup>,这是为什么?indexFor这个方法解释了这个问题** |
1915 | 1915 |
|
1916 | 1916 | 大家都知道计算机里面位运算是基本运算,位运算的效率是远远高于取余 % 运算的 |
1917 | 1917 |
|
|
0 commit comments