ConcurrentHashMap是Java并发包中的一个线程安全的HashMap实现。它通过使用分段锁(segmentation locks)和CAS(Compare And Swap)操作来支持高并发下的键值对存储和检索。下面是一个简化的源码分析,帮助你理解ConcurrentHashMap的工作原理:
import java.util.concurrent.ConcurrentHashMap;
public class ConcurrentHashMapExample {
public static void main(String[] args) {
ConcurrentHashMap<Integer, String> map = new ConcurrentHashMap<>();
for (int i = 0; i < 100; i++) {
map.put(i, "Value " + i);
}
System.out.println(map.get(50));
}
}
1. 类结构
ConcurrentHashMap类包含一个Segment数组,每个Segment维护一个HashEntry的数组,HashEntry表示实际的键值对。Segment继承自ReentrantLock,因此每个Segment都是一个可重入锁。
1.1 ReentrantLock
Java并发包中的一种同步工具,它提供了一个可重入的互斥锁实现。
- 互斥性:ReentrantLock确保任何时候只有一个线程可以访问共享资源。当一个线程获取了锁之后,其他线程试图获取锁时会被阻塞,直到该线程释放锁。
public void method() {
lock.lock(); // 获取锁
try {
// 临界区代码
} finally {
lock.unlock(); // 释放锁
}
- 可重入性:ReentrantLock允许同一个线程在已经获取了锁的情况下,再次获取相同的锁。这种特性称为“可重入性”或“递归锁”。也就是说,同一个线程可以多次进入同一个代码块,而不会被其他线程干扰。
### 通过内部计数器来实现的。每次线程请求锁时,计数器会递增;每次线程释放锁时,计数器会递减。只有当计数器为零时,其他线程才能够获取该锁。
import java.util.concurrent.locks.ReentrantLock;
public class SimpleReentrantLock {
private int lockCount = 0; // 计数器
private boolean isLocked = false; // 表示锁是否已经被获取
public void lock() {
// 自旋锁,如果当前线程已经持有锁,则直接返回
while (isLocked) {
// 等待,直到锁被释放
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 当前线程获取锁
isLocked = true;
lockCount++;
}
public void unlock() {
// 只有当计数器不为零时,才能释放锁
if (lockCount > 0) {
lockCount--;
// 当计数器为零时,表示锁可以释放
if (lockCount == 0) {
isLocked = false;
}
}
}
public boolean isLocked() {
return isLocked;
}
}
- 公平性:ReentrantLock提供了公平和非公平两种获取锁的方式。公平锁意味着等待时间最长的线程将首先获得锁。非公平锁则不保证等待顺序,它可能让新的线程获得锁,即使有其他线程等待的时间更长。
### 公平锁: ReentrantLock 的实现中,有一个内部类 Sync 来维护一个线程等待队列,这个队列是用来管理正在等待获取锁的线程的。会消耗一定的性能
// 创建一个公平锁
ReentrantLock fairLock = new ReentrantLock(true);
// 创建一个非公平锁
ReentrantLock unfairLock = new ReentrantLock(false);
=================================
public class ReentrantLock {
private static class Sync {
// 等待队列
private final Node waitQueue = new Node();
// 获取锁的入口方法
void lock() {
// 获取当前线程
Thread current = Thread.currentThread();
Node node = current.getNode();
if (node == null) {
// 创建一个新节点并将其添加到队列的尾部
node = new Node(current);
enq(node);
// 自旋获取锁
while (!tryAcquire(node)) {
// 线程阻塞,直到被唤醒
parkAndCheckInterrupt();
}
} else {
// 当前线程已经有一个节点,尝试获取锁
if (!tryAcquire(node)) {
// 如果获取锁失败,将当前线程挂起
throw new IllegalMonitorStateException();
}
}
}
// 释放锁
void unlock() {
// 获取当前线程的节点
Node node = Thread.currentThread().getNode();
if (node == null) {
throw new IllegalMonitorStateException();
}
// 释放锁
if (!node.waitQueue.isEmpty()) {
// 如果队列中还有等待的线程,尝试唤醒一个
Node next = node.waitQueue.next;
if (next != null) {
next.thread.interrupt();
}
}
}
// 尝试获取锁
private boolean tryAcquire(Node node) {
// 尝试获取锁的逻辑
return false;
}
// 将节点添加到队列的尾部
private Node enq(Node node) {
// 添加节点的逻辑
return null;
}
}
// 内部类 Node 用来表示线程等待队列的节点
private static class Node {
private Thread thread;
private Node next;
private Node prev;
private Node(Thread thread) {
this.thread = thread;
}
// 其他方法...
}
}
- 条件变量:ReentrantLock支持条件变量,允许线程在某个条件发生之前挂起,直到其他线程通知这个条件已经满足。这通过await和signal方法来实现。
### 在这个例子中,Thread A 通过调用 condition.await() 方法挂起,直到 Thread B 通过调用 condition.signal() 方法通知。Thread B 在调用 condition.signal() 之前,会先持有锁,以确保只有持有锁的线程才能唤醒等待线程。
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
public class ConditionExample {
public static void main(String[] args) {
ReentrantLock lock = new ReentrantLock();
Condition condition = lock.newCondition();
new Thread(() -> {
lock.lock();
try {
System.out.println("Thread A: waiting for condition");
condition.await();
System.out.println("Thread A: condition met, proceeding");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}).start();
new Thread(() -> {
try {
Thread.sleep(1000); // Simulate some work
lock.lock();
try {
System.out.println("Thread B: notifying condition");
condition.signal();
} finally {
lock.unlock();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
}
}
-
非阻塞式通知:ReentrantLock的实现使用了非阻塞式通知(non-blocking notification),这意味着当一个线程释放锁时,它会将等待队列中的第一个线程唤醒,而不是等待线程主动调用await方法。
-
内置超时机制:ReentrantLock提供了带超时时间的获取锁方法,如果线程在规定时间内没有获取到锁,则返回一个值表示是否成功获取锁。
在实际使用中,你通常会使用lock()和unlock()方法来获取和释放锁,或者使用tryLock()方法尝试获取锁,并在超时或被中断时返回。ReentrantLock也支持构建函数,允许你指定公平或不公平的获取锁方式。
2. 构造函数
ConcurrentHashMap的构造函数初始化Segment数组,并设置适当的初始容量和负载因子。
public ConcurrentHashMap(int initialCapacity, float loadFactor) {
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " + initialCapacity);
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " + loadFactor);
// Find a power of 2 >= initialCapacity
int c = initialCapacity - 1;
int sshift = 0;
int ssize = 1;
while (ssize < c) {
sshift++;
ssize <<= 1;
}
// Ensure capacity is a power of 2, and length-2 is a power of 2 as well
this.length = ssize;
this.loadFactor = loadFactor;
this.segments = new Segment[sshift];
for (int i = 0; i < sshift; i++)
this.segments[i] = new Segment(ssize, loadFactor);
}
3. 存储数据
put方法用于存储键值对。它首先计算键的哈希码,然后找到对应的Segment,最后使用CAS操作将键值对插入到HashEntry数组中。如果插入成功,则返回旧值;否则,它会自旋尝试直到成功或者超时。
public V put(K key, V value) {
Segment<K, V> seg = getSegment(key);
HashEntry<K, V>[] tab = seg.table;
int hash = hash(key);
int index = (hash & 0x60000000) == 0 ? hash & 0x7FFFFFFF : tab.length;
HashEntry<K, V> entry = segtreePut(seg, tab, hash, key, value, true, index);
return entry == null ? null : entry.value;
}
4. 获取数据
get方法用于检索键的值。它首先计算键的哈希码,然后找到对应的Segment和HashEntry,最后返回对应的值。
public V get(Object key) {
Segment<K, V> seg = getSegment(key);
HashEntry<K, V> entry = seg.getEntry(key, false);
return entry == null ? null : entry.value;
}
5. 扩容机制
ConcurrentHashMap使用一种叫做“渐进式扩容”(biased locking)的机制来避免在扩容时完全锁定整个映射。渐进式扩容是通过使用Node<K,V>和Segment<K,V>这两个内部类来实现的。每个Segment代表一个独立的哈希桶数组,它维护了一个子映射。当一个Segment达到其容量阈值时,它会被单独扩容,这个过程是线程安全的,因为它使用了ReentrantLock来保护扩容操作。
6. 总结
ConcurrentHashMap通过分段锁和CAS操作实现了高并发的键值对存储和检索。它的设计使得在多个线程同时访问时,只要访问不同的Segment,就可以避免竞争。此外,它的扩容机制能够避免在扩容时完全锁定整个映射,从而提高了并发性能。