ConcurrentHashMap是Java中的一种线程安全的哈希表完成,它供给了与Hashtable和HashMap相似的API,但经过运用分段锁技术(Segment),使得多个线程能够一起读取和写入不同的数据块,然后提高了并发性能。一起,ConcurrentHashMap也支撑弱一致性,即在某些状况下,读取操作可能会回来稍早的值,但这关于许多应用场景来说是能够承受的。该类还供给了一些有用的办法,如putIfAbsent()、replace()、compute()等,便利开发者进行依据哈希表的数据处理。总归,ConcurrentHashMap是一个高效且可靠的多线程环境下的哈希表完成,十分适合在并发场景中运用。
一、ConcurrentHashMap 的数据结构
ConcurrentHashMap 是 Java 中的一种线程安全的哈希表完成,其数据结构是由多个 Node 节点组成的数组和链表或红黑树。
在 ConcurrentHashMap 中,Node 节点是一个键值对,其间键为 K 类型,值为 V 类型。每个节点包含了一个哈希值、一个键和一个值,以及指向下一个节点的引证。详细而言,ConcurrentHashMap 的内部数据结构如下:
ConcurrentHashMap 中的每个 Segment 是一个独立的哈希表,而每个 Segment 又由多个 Bucket 组成,每个 Bucket 再保护一个链表或红黑树(红黑树出现的条件是 Bucket 中的元素数量大于等于 8)。
ConcurrentHashMap 的 put 操作能够分红两个过程,首要依据 Key 的哈希值找到对应的 Segment 和 Bucket,然后在 Bucket 中刺进新的 Node 节点。详细来说,它的处理流程如下:
- 对 Key 进行哈希操作,得到其哈希值。
- 依据哈希值和 Segment 数组的长度,计算出 Key 应该被放在哪个 Segment 中。
- 在 Segment 中获取 Key 应该放在哪个 Bucket 中。
- 假如该 Bucket 是空的,则直接在它的头部刺进新的 Node 节点;不然,将新的 Node 节点刺进到链表的尾部或红黑树上,并依据状况进行扩容或红黑树转换为链表。
- 假如刺进新的节点后 Bucket 中的元素数量超过了一个阈值,则需求进行扩容操作。
- 假如刺进新的节点后 Bucket 中的元素数量大于等于 8 个并且 Bucket 不是红黑树,则需求将链表转换为红黑树。
- 假如旧的红黑树中的节点数量少于 6 个,则需求将红黑树转换为链表。
ConcurrentHashMap 的 get 操作也很简单,直接依据 Key 的哈希值找到对应的 Segment 和 Bucket,然后在 Bucket 中查找对应的 Node 节点即可。
二、ConcurrentHashMap 的分段锁机制
ConcurrentHashMap 将整个哈希表分为多个 Segment,每个 Segment 又是一个独立的哈希表,能够单独进行加锁和扩容操作。在操作数据时,只需求获取对应 Segment 的锁,不需求锁住整个哈希表,这样能够防止多个线程之间的等候和竞争,一起提高吞吐量和并发性能。
简单完成示例:
class ConcurrentHashMap<K, V> {
final Segment[] segments;
class Segment extends ReentrantLock implements Serializable{
// 每个 Segment 自己独立的哈希表
private final Map<K,V> map = new HashMap<>();
// Segment 内部加锁机制,确保线程安全
public synchronized V put(K key, V value) {
return map.put(key, value);
}
}
// 获取 key 所属的 Segment 的索引
private int getSegmentIndex(K key) {
int hash = hash(key.hashCode()); // 对 hashcode() 进行哈希
int segmentMask = segments.length - 1; // mask 值
return hash & segmentMask; // 按位与,定位详细的 Segment
}
public V put(K key, V value) {
Segment s = segments[getSegmentIndex(key)];
s.lock(); // 获取 s 对应的 Segment 的锁
try {
return s.put(key, value); // 在 s 上进行 put 操作
} finally {
s.unlock(); // 释放 s 对应的 Segment 的锁
}
}
}
在实践的 ConcurrentHashMap 中,每个 Segment 会运用一个独立的哈希表来保护其内部的数据,一起也具备自己的锁机制,然后完成对其内部状况的并发安全拜访。这样,不同线程拜访不同的 Segment 时能够经过分段锁机制来完成并发拜访,然后提高了 ConcurrentHashMap 的并发性能和吞吐量。
三、ConcurrentHashMap 的完成过程
在 JDK 1.8 以前,ConcurrentHashMap 的完成采用了与 Hashtable 相似的分段锁机制,每个 Segment 都对应一个 ReentrantLock 锁,用于并发拜访。
而在 JDK 1.8 中,ConcurrentHashMap 引入了 CAS(Compare and Swap)技术,用于完成一个更加高效的并发操控机制。CAS 是一种无锁机制,能够防止线程争抢锁的状况。
咱们以 put 操作为例,来看一下 ConcurrentHashMap 的完成过程:
- 首要计算 key 的哈希值;
- 依据哈希值找到对应的 Segment;
- 获取 Segment 对应的锁;
- 假如还没有元素,就直接刺进到 Segment 中;
- 假如现已存在元素,就循环比较 key 是否持平;
- 假如 key 现已存在,就依据要求更新 value;
- 假如 key 不存在,就刺进新的元素(链表或者红黑树)。
上述操作中,过程 2 到 3 相当于加了一个失望锁,在整个哈希表上加锁,假如只有一个 Segment,作用与 Hashtable 相似;假如存在多个 Segment,作用就相当于运用了分段锁机制,提高了并发拜访性能。
四、运用场景案例
1. 高并发的计数器
ConcurrentHashMap 能够用来完成高并发的计数器,例如记载网站拜访量、接口调用次数等。详细地,咱们能够运用 ConcurrentHashMap 的 compute 办法来完成计数操作,如下所示:
import java.util.concurrent.ConcurrentHashMap;
public class Counter {
private final ConcurrentHashMap<String, Integer> map;
public Counter() {
this.map = new ConcurrentHashMap<>();
}
public void increase(String key) {
map.compute(key, (k, v) -> (v == null) ? 1 : v + 1);
}
public int get(String key) {
return map.getOrDefault(key, 0);
}
}
在上述代码中,咱们创建了一个 Counter 类,运用 ConcurrentHashMap 存储计数器数据。详细地,咱们运用 compute 办法完成对计数器的增加操作,假如 key 不存在则新建一个值为 1 的计数器;不然将其递加 1。经过 get 办法能够获取指定 key 对应的计数器值。
2. 线程池使命办理
ConcurrentHashMap 还能够用来完成线程池使命的办理,例如记载每个使命的履行状况、结果等信息。详细地,咱们能够将一个 ConcurrentHashMap 实例作为使命办理器,在使命履行前将使命信息增加到该办理器中,然后再在使命完成后更新对应的信息,如下所示:
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
public class TaskManager {
private final ConcurrentHashMap<String, TaskInfo> tasks;
public TaskManager() {
this.tasks = new ConcurrentHashMap<>();
}
public void addTask(String taskId, Runnable task) {
// 增加使命信息
tasks.put(taskId, new TaskInfo());
// 提交使命到线程池
ExecutorService executor = Executors.newCachedThreadPool();
executor.submit(() -> {
try {
// 履行使命
task.run();
// 更新使命状况和结果
TaskInfo info = tasks.get(taskId);
info.setStatus(TaskStatus.COMPLETED);
info.setResult("Task completed successfully!");
} catch (Exception ex) {
// 更新使命状况和结果
TaskInfo info = tasks.get(taskId);
info.setStatus(TaskStatus.FAILED);
info.setResult(ex.getMessage());
}
});
// 封闭线程池
executor.shutdown();
}
public TaskInfo getTaskInfo(String taskId) {
return tasks.getOrDefault(taskId, new TaskInfo());
}
}
enum TaskStatus {
NEW,
RUNNING,
COMPLETED,
FAILED;
}
class TaskInfo {
private TaskStatus status;
private String result;
public TaskInfo() {
this.status = TaskStatus.NEW;
this.result = "";
}
public TaskStatus getStatus() {
return status;
}
public void setStatus(TaskStatus status) {
this.status = status;
}
public String getResult() {
return result;
}
public void setResult(String result) {
this.result = result;
}
}
在上述代码中,咱们创建了一个 TaskManager 类,运用 ConcurrentHashMap 存储使命信息。详细地,咱们界说了一个 TaskInfo 类来表明使命信息,其间包含使命状况和结果两个特点。在增加使命时,咱们新建一个 TaskInfo 实例并增加到 ConcurrentHashMap 中,然后在异步履行使命的线程中更新其状况和结果;一起咱们运用 ExecutorService 来办理并发履行的使命。经过 getTaskInfo 办法能够获取指定 taskId 对应的使命信息。
3. 缓存办理器
ConcurrentHashMap 还能够用来完成缓存办理器,例如存储经常运用的业务数据、系统配置等信息,然后防止频频的数据库查询或网络请求。详细地,咱们能够运用 ConcurrentHashMap 存储缓存数据,并设定缓存过期时刻,如下所示:
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
public class CacheManager<K, V> {
private final Map<K, CacheEntry<V>> cache;
public CacheManager() {
this.cache = new ConcurrentHashMap<>();
}
public void put(K key, V value, long ttl) {
// 增加缓存项,一起记载当前时刻戳和缓存生计时刻
CacheEntry<V> entry = new CacheEntry<>(value, System.currentTimeMillis(), ttl);
cache.put(key, entry);
}
public V get(K key) {
// 获取缓存项和其缓存生计时刻
CacheEntry<V> entry = cache.get(key);
// 查看缓存项是否过期,假如过期则删去缓存项并回来 null
if (entry != null && !entry.isExpired()) {
return entry.getValue();
} else {
cache.remove(key);
return null;
}
}
static class CacheEntry<V> {
private final V value;
private final long timestamp;
private final long ttl;
public CacheEntry(V value, long timestamp, long ttl) {
this.value = value;
this.timestamp = timestamp;
this.ttl = ttl;
}
public V getValue() {
return value;
}
public boolean isExpired() {
return System.currentTimeMillis() - timestamp > ttl;
}
}
}
在上述代码中,咱们创建了一个 CacheManager 类,运用 ConcurrentHashMap 存储缓存数据。详细地,咱们界说了一个 CacheEntry 类来表明缓存项,其间包含值、时刻戳和缓存生计时刻三个特点。在增加缓存项时,咱们新建一个 CacheEntry 实例并增加到 ConcurrentHashMap 中,然后在获取缓存项时查看其是否过期;假如未过期则回来其值,不然删去缓存项并回来 null。