在之前的文章中,我们深入了解了 HashMap
和 HashTable
的底层代码原理,包括它们的数据结构和工作原理。在本章中,我们将继续探讨 ConcurrentHashMap
、HashSet
和 LinkedHashMap
这些与 HashMap
有关的关键数据结构,深入了解它们的实现方式以及各自的特性和用途。让我们继续探索这些重要的 Java 集合类。如果您有任何特定的问题或需要更多详细信息,请随时提出。
ConcurrentHashMap
ConcurrentHashMap
是 Java 集合框架中的一个重要类,它提供了高度并发的哈希表实现。与普通的 HashMap
不同,ConcurrentHashMap
允许多个线程同时读取和写入,而不会导致数据不一致或死锁。它在多线程环境中提供了出色的性能和可伸缩性,并且在 Java 5 及以后的版本中引入。
以下是 ConcurrentHashMap
的一些关键特性和用途:
-
线程安全性:
ConcurrentHashMap
是线程安全的数据结构。它通过使用分段锁机制来实现高度并发的读取和写入操作。这意味着多个线程可以同时读取不同部分的哈希表,而不会互相阻塞。这种设计使得在多线程环境中可以实现更高的性能。 -
分段锁:
ConcurrentHashMap
内部使用多个独立的哈希表段(segment),每个段可以看作是一个小的哈希表。每个段都有自己的锁,当线程访问某一段时,只有该段被锁定,其他段仍然可以被并发访问。这种分段锁设计允许多个线程同时执行读操作,只有写操作需要锁定对应的段。 -
高性能:由于并发读取操作不会阻塞,
ConcurrentHashMap
在读多写少的场景中表现出色。它是处理高并发读操作的理想选择。 -
支持高并发写操作:虽然
ConcurrentHashMap
的主要优势在于读操作的高并发性能,但它仍然支持高并发的写操作,因为不同的段可以被不同线程同时锁定。 -
可伸缩性:
ConcurrentHashMap
具有良好的可伸缩性。您可以根据需要增加哈希表的段数,以适应更多的并发访问。这使得它适用于不同规模的应用。 -
迭代性能:
ConcurrentHashMap
的迭代性能也得到了优化,使得在迭代哈希表中的元素时能够获得高效的性能。 -
高级功能:
ConcurrentHashMap
提供了许多高级功能,如支持自定义并发级别、compute
方法用于原子更新、merge
方法用于原子合并等。
总之,ConcurrentHashMap
是一个强大的多线程环境中的哈希表实现,它允许高度并发的读取和写入操作,并具有出色的性能和可伸缩性。它通常用于需要高并发访问的场景,例如多线程应用程序中的缓存、计数器、任务分配等。
示例代码 - 使用 ConcurrentHashMap
以下是一个简单的示例代码,演示如何使用 ConcurrentHashMap
来管理多个线程安全的计数器。在这个示例中,我们创建一个 ConcurrentHashMap
,其中键是字符串,值是整数计数器。多个线程可以同时增加和获取计数器的值,而不会发生竞争条件。
import java.util.concurrent.ConcurrentHashMap;
public class ConcurrentHashMapExample {
public static void main(String[] args) {
// 创建一个 ConcurrentHashMap
ConcurrentHashMap<String, Integer> counterMap = new ConcurrentHashMap<>();
// 启动多个线程来增加计数器的值
for (int i = 0; i < 5; i++) {
String key = "counter" + i;
Thread thread = new Thread(() -> {
for (int j = 0; j < 10000; j++) {
// 使用 compute 方法来原子地更新计数器
counterMap.compute(key, (k, v) -> (v == null) ? 1 : v + 1);
}
});
thread.start();
}
// 等待所有线程完成
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 打印每个计数器的值
counterMap.forEach((key, value) -> {
System.out.println(key + ": " + value);
});
}
}
在这个示例中,我们创建了一个 ConcurrentHashMap
来存储多个计数器,每个计数器都有一个唯一的键(例如,“counter0”、“counter1” 等)。然后,我们启动了多个线程,每个线程对计数器进行增加操作。使用 compute
方法来原子地更新计数器的值,确保线程安全。最后,我们打印出每个计数器的最终值。
ConcurrentHashMap
在这种情况下非常适合,因为它允许多个线程同时访问和修改不同的计数器,而不会出现竞争条件。这是一个简单的示例,演示了如何在多线程环境中使用 ConcurrentHashMap
。
ConcurrentHashMap
底层代码解析
ConcurrentHashMap
是 Java 集合框架中的一个关键类,它提供了高并发性能的哈希表实现。在内部,ConcurrentHashMap
使用了分段锁机制,允许多个线程同时读取和写入,而不会出现锁竞争的情况。
以下是 ConcurrentHashMap
的主要代码片段,我将添加注释来解释其关键部分:
public class ConcurrentHashMap<K, V> extends AbstractMap<K, V>
implements ConcurrentMap<K, V>, Serializable {
// 哈希表的默认初始容量
static final int DEFAULT_INITIAL_CAPACITY = 16;
// 哈希表的最大容量
static final int MAXIMUM_CAPACITY = 1 << 30;
// 默认的负载因子
static final float DEFAULT_LOAD_FACTOR = 0.75f;
// 段的数量,默认为 16
static final int DEFAULT_CONCURRENCY_LEVEL = 16;
// 一个段的最小容量
static final int MIN_SEGMENT_TABLE_CAPACITY = 2;
// 每个段内部存储键值对的数组
transient volatile Segment<K, V>[] segments;
// ...
// 内部类,表示哈希表的段
static final class Segment<K, V> extends ReentrantLock implements Serializable {
// ...
// 存储键值对的数组
transient volatile HashEntry<K, V>[] table;
// ...
// 内部类,表示哈希表的节点
static final class HashEntry<K, V> {
final int hash;
final K key;
volatile V value;
final HashEntry<K, V> next;
// ...
}
// ...
}
// ...
// 内部方法,用于获取段的索引
final int segmentFor(int hash) {
return (hash >>> segmentShift) & segmentMask;
}
// 内部方法,用于插入键值对
final V put(K key, int hash, V value, boolean onlyIfAbsent) {
// ...
}
// ...
// 其他方法,如get, remove, size, clear, keySet, values, entrySet 等
}
上述代码摘录展示了 ConcurrentHashMap
的主要结构和关键部分。它使用了分段锁的思想,将整个哈希表划分为多个段(Segment
),每个段都是一个独立的哈希表,具有自己的锁。这意味着不同的线程可以同时访问不同的段,而不会相互阻塞。每个段内部使用数组来存储键值对,具有自己的哈希冲突解决机制。
ConcurrentHashMap
在并发读取和写入的场景中表现出色,因为不同的线程可以同时操作不同的段,而不会出现锁竞争。这使得它成为处理高并发情况下的理想选择。
是对其中一部分核心代码进行更详细的分析和讲解。
1. put
方法核心代码分析
put
方法用于向 ConcurrentHashMap
中插入键值对。以下是 put
方法的核心代码,我将逐行进行讲解和分析:
final V put(K key, int hash, V value, boolean onlyIfAbsent) {
Segment<K, V> s; // 获取要操作的段
if (value == null)
return null;
if ((s = segmentFor(hash)) == null)
throw new NullPointerException();
return s.put(key, hash, value, onlyIfAbsent); // 调用段的 put 方法
}
-
Segment<K, V> s;
:首先声明了一个Segment
对象s
,用于表示要操作的段。 -
if (value == null) return null;
:检查传入的值是否为null
,如果是null
,则直接返回null
。 -
if ((s = segmentFor(hash)) == null)
:通过segmentFor
方法根据键的哈希值确定要操作的段,并将其赋值给s
。如果segmentFor
返回null
,则抛出NullPointerException
异常。 -
return s.put(key, hash, value, onlyIfAbsent);
:最后,调用确定的段s
的put
方法进行实际插入操作,传递键key
、哈希值hash
、值value
和onlyIfAbsent
参数。
2. get
方法核心代码分析
get
方法用于根据键获取值。以下是 get
方法的核心代码,我将逐行进行讲解和分析:
//会发现源码中没有一处加了锁
public V get(Object key) {
Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
//计算hash
int h = spread(key.hashCode());
if ((tab = table) != null && (n = tab.length) > 0 &&
(e = tabAt(tab, (n - 1) & h)) != null) {
//读取首节点的Node元素
if ((eh = e.hash) == h) {
//如果该节点就是首节点就返回
if ((ek = e.key) == key || (ek != null && key.equals(ek)))
return e.val;
}
//hash值为负值表示正在扩容,这个时候查的是ForwardingNode的find方法来定位到nextTable来
//eh=-1,说明该节点是一个ForwardingNode,正在迁移,此时调用ForwardingNode的find方法去nextTable里找。
//eh=-2,说明该节点是一个TreeBin,此时调用TreeBin的find方法遍历红黑树,由于红黑树有可能正在旋转变色,所以find里会有读写锁。
//eh>=0,说明该节点下挂的是一个链表,直接遍历该链表即可。
else if (eh < 0)
return (p = e.find(h, key)) != null ? p.val : null;
while ((e = e.next) != null) {//既不是首节点也不是ForwardingNode,那就往下遍历
if (e.hash == h &&
((ek = e.key) == key || (ek != null && key.equals(ek))))
return e.val;
}
}
return null;
}
-
int hash = hash(key.hashCode());
:首先,根据传入键的哈希码计算哈希值hash
。 -
return segmentFor(hash).get(key, hash);
:然后,通过segmentFor
方法根据哈希值确定要操作的段,然后调用该段的get
方法,传递键key
和哈希值hash
来获取值。
3. remove
方法核心代码分析
remove
方法用于删除指定键的键值对。以下是 remove
方法的核心代码,我将逐行进行讲解和分析:
public V remove(Object key) {
int hash = hash(key.hashCode());
return segmentFor(hash).remove(key, hash, null);
}
-
int hash = hash(key.hashCode());
:首先,根据传入键的哈希码计算哈希值hash
。 -
return segmentFor(hash).remove(key, hash, null);
:然后,通过segmentFor
方法根据哈希值确定要操作的段,然后调用该段的remove
方法,传递键key
、哈希值hash
和一个null
参数来删除键值对。
4. resize
方法核心代码分析
resize
方法用于扩容哈希表。以下是 resize
方法的核心代码,我将逐行进行讲解和分析:
final void reinitialize() {
if (table != null) {
for (int i = 0; i < segments.length; ++i)
segments[i].clear(); // 清空各个段
}
table = null;
sizeCtl = 0;
}
-
if (table != null)
:首先,检查哈希表是否已经创建(table
不为null
)。 -
for (int i = 0; i < segments.length; ++i)
:然后,遍历各个段,执行清空操作。 -
segments[i].clear();
:通过调用clear
方法清空各个段内的数据。 -
table = null;
:最后,将哈希表table
置为null
。 -
sizeCtl = 0;
:重置哈希表的控制变量sizeCtl
。
这些是 ConcurrentHashMap
中的一些核心方法的关键代码分析。理解这些代码有助于了解 ConcurrentHashMap
的内部实现和线程安全机制。
ConcurrentHashMap
中的分段锁是通过将整个哈希表分成多个段(Segment)来实现的,每个段都拥有自己的锁。这样,多个线程可以同时访问不同段的数据,而不会相互阻塞,从而提高了并发性能。
分段锁
下面是分段锁的原理分析和代码讲解:
原理分析:
-
分段:
ConcurrentHashMap
通过将哈希表分成多个段来实现并发控制。每个段是一个独立的哈希表,拥有自己的锁。这样,不同段之间的操作可以并行进行,提高了并发性能。 -
锁粒度:每个段内部的锁粒度较小,仅锁定该段内的数据。这允许多个线程在不同段上并发执行操作,减小了锁的竞争。
-
分段数:分段的数量通常与
ConcurrentHashMap
的初始化容量相关,每个段负责管理一部分键值对。随着数据的增加,ConcurrentHashMap
可以动态地增加段的数量,以保持并发性能。
代码讲解:
以下是 ConcurrentHashMap
中分段锁的代码示例:
public class ConcurrentHashMap<K, V> extends AbstractMap<K, V>
implements ConcurrentMap<K, V>, Serializable {
// ...
static final class Segment<K, V> extends ReentrantLock implements Serializable {
// ...
// 存储键值对的数组
transient volatile HashEntry<K, V>[] table;
// ...
// 内部类,表示哈希表的节点
static final class HashEntry<K, V> {
final int hash;
final K key;
volatile V value;
final HashEntry<K, V> next;
// ...
}
// ...
}
// ...
}
-
ConcurrentHashMap
中的每个段是Segment<K, V>
类的实例,它继承自ReentrantLock
,表示一个可重入锁。 -
Segment
内部有一个存储键值对的数组table
,用于存储数据。不同段拥有不同的table
。 -
在每个段内部,有一个内部类
HashEntry
,表示哈希表的节点。键值对被存储在HashEntry
中。哈希冲突时,会形成链表或红黑树结构。 -
Segment
内部的锁用于保护该段的数据结构,以确保多个线程同时访问该段时的线程安全性。
总结来说,ConcurrentHashMap
通过将哈希表分成多个段,并为每个段提供独立的锁,实现了分段锁机制。这种设计允许多个线程在不同段上并发执行操作,提高了并发性能,同时保持了线程安全。
哈希桶/链表/红黑树
ConcurrentHashMap
内部使用了哈希桶、链表或红黑树等数据结构来存储键值对,具体的数据结构选择取决于哈希冲突的情况。以下是这些数据结构的原理讲解和代码分析:
1. 哈希桶 (Hash Bucket) 原理:
-
哈希桶是一种数组结构,用于存储键值对。
ConcurrentHashMap
将哈希表的每个段分成多个哈希桶,每个哈希桶负责存储一部分键值对。 -
哈希函数将键映射到特定的哈希桶,通常是通过取哈希码的某个部分再取模操作来实现的。
-
哈希冲突:当不同键映射到同一个哈希桶时,发生哈希冲突。此时,键值对将以链表的形式存储在该哈希桶中。
2. 链表 (Linked List) 原理:
-
链表是一种线性数据结构,用于存储键值对。当哈希冲突发生时,新的键值对将被追加到链表的末尾。
-
链表的查询操作需要顺序扫描链表,时间复杂度为 O(n),其中 n 是链表的长度。
3. 红黑树 (Red-Black Tree) 原理:
-
红黑树是一种自平衡二叉搜索树,用于存储键值对。当链表中的键值对数量达到一定阈值时,链表会被转换为红黑树,以提高查询效率。
-
红黑树的查询操作具有较低的时间复杂度 O(log n),其中 n 是树的高度。
下面是相关代码分析:
哈希桶的代码示例:
static final class Segment<K, V> extends ReentrantLock implements Serializable {
// 存储键值对的哈希桶
transient volatile HashEntry<K, V>[] table;
// ...
}
在每个 Segment
内部,有一个数组 table
用于存储键值对。不同的哈希桶可能有不同的 table
。
链表的代码示例:
static final class HashEntry<K, V> {
final int hash;
final K key;
volatile V value;
final HashEntry<K, V> next;
// ...
}
在 HashEntry
中,next
字段用于链接下一个哈希冲突的键值对,从而形成链表结构。
红黑树的代码示例:
static final class TreeNode<K, V> extends Node<K, V> {
TreeNode<K, V> parent; // 父节点
TreeNode<K, V> left; // 左子节点
TreeNode<K, V> right; // 右子节点
TreeNode<K, V> prev; // 链表中的前一个节点
boolean red; // 是否为红色节点
// ...
}
在 ConcurrentHashMap
中,链表可以在键值对数量达到一定阈值后转换为红黑树。TreeNode
表示红黑树中的节点,其中的 parent
、left
、right
等字段用于表示树结构。
这些数据结构的选择和管理使 ConcurrentHashMap
能够在高并发场景中高效地存储和检索键值对,并且在哈希冲突较多时自动升级为红黑树以提高查询性能。这些结构的组合是 ConcurrentHashMap
实现高效并发操作的关键。
和HashMap区别
ConcurrentHashMap
和 HashMap
都是 Java 中用于存储键值对的集合类,但它们在并发性和线程安全性上有显著的区别。以下是它们的主要区别:
-
线程安全性:
-
ConcurrentHashMap
是线程安全的数据结构,它可以被多个线程同时访问和修改,而不需要额外的同步操作。它通过使用分段锁(Segment)来实现并发控制,每个段拥有自己的锁,不同段的操作可以并行执行。 -
HashMap
不是线程安全的,如果多个线程同时访问和修改一个HashMap
实例,可能会导致数据不一致和线程安全问题。在多线程环境下,必须使用额外的同步机制(例如,Collections.synchronizedMap()
或显式的锁)来保证线程安全性。
-
-
性能:
- 在低并发情况下,
HashMap
的性能通常比ConcurrentHashMap
略好,因为不需要额外的并发控制开销。但在高并发环境中,ConcurrentHashMap
的性能通常更好,因为它可以允许多个线程同时读取和写入数据。
- 在低并发情况下,
-
迭代器的弱一致性:
-
ConcurrentHashMap
的迭代器提供了弱一致性的保证,即迭代器可以同时遍历正在修改的和未修改的元素,但不保证迭代器在某个特定时间点的视图是一致的。这对于并发迭代非常有用。 -
HashMap
的迭代器在并发环境中可能会导致ConcurrentModificationException
异常,因为它不提供并发迭代的支持,必须在多线程环境中进行额外的同步。
-
-
初始容量和负载因子:
-
ConcurrentHashMap
在初始化时可以指定多个段(Segment),每个段内有自己的初始容量和负载因子。这允许更好地控制并发度和内存使用。 -
HashMap
则只有一个全局的初始容量和负载因子,不能针对不同需求进行调优。
-
-
null 值和键:
-
ConcurrentHashMap
不允许存储 null 值和 null 键。如果尝试存储 null 值或键,将会抛出NullPointerException
。 -
HashMap
允许存储 null 值和键。
-
综上所述,ConcurrentHashMap
适用于高并发环境,提供了线程安全性和性能的平衡。而 HashMap
适用于单线程或低并发环境,需要额外的同步来确保线程安全。选择合适的集合类取决于应用程序的并发需求和性能要求。
JDK1.7和1.8区别
Java 1.7中的ConcurrentHashMap:
-
分段锁:在Java 1.7中,
ConcurrentHashMap
使用了分段锁机制,将整个数据结构分为多个段,每个段都有自己的锁。这使得在多线程环境中,不同段的操作可以并发执行,从而提高了性能。 -
底层数据结构:Java 1.7中的
ConcurrentHashMap
底层使用了一个数组和一个链表的组合来存储键值对。这个数据结构在Java 1.7中被称为“分段数组+链表”。
Java 1.8中的ConcurrentHashMap:
-
CAS操作和红黑树:Java 1.8对
ConcurrentHashMap
进行了一些重大改进。最重要的改进是引入了CAS(比较并交换)操作和红黑树数据结构。在Java 1.8中,ConcurrentHashMap
采用了一种更先进的算法来提高性能。当链表的长度超过一定阈值时,链表会转化为红黑树,这可以显著减少查找时间,使查找的时间复杂度降低到O(log n)。 -
去除分段锁:Java 1.8中去除了分段锁,取而代之的是使用更细粒度的锁和CAS操作来提高并发性能。这意味着在Java 1.8中,
ConcurrentHashMap
的并发性能更好,可以处理更多的并发操作。 -
底层数据结构:Java 1.8中的
ConcurrentHashMap
底层数据结构变得更加复杂,包括了分段数组、链表和红黑树等。
综上所述,Java 1.8中的ConcurrentHashMap
相对于Java 1.7中的版本具有更好的性能和更先进的数据结构。如果你在使用Java 1.8或更高版本,建议使用Java 1.8中的ConcurrentHashMap
以获得更好的并发性能。但要注意,由于数据结构的复杂性,Java 1.8中的ConcurrentHashMap
在某些情况下可能会导致更高的内存消耗,因此在选择数据结构时应根据具体的需求进行权衡。
结语
结语:
在本文中,我们深入研究了ConcurrentHashMap
的特性和底层代码,了解了它如何实现线程安全的高性能并发操作。ConcurrentHashMap
是处理多线程并发访问的理想选择,它提供了高度的并发性能,同时保持了数据的一致性和准确性。
在你的多线程Java应用程序中,如果需要一个线程安全的映射数据结构,不妨考虑使用ConcurrentHashMap
。它可以满足大多数并发场景的需求,而无需手动添加同步机制。
在接下来的文章中,我们将继续深入探讨Java集合框架的其他部分,特别是HashSet
,它是一种非常常用的集合类型。如果你有任何问题、建议或反馈,请随时通过私信或留言与我们联系。我们将不断完善和更新内容,以便为你提供有价值的信息。