深入理解Java中的ConcurrentHashMap:原理与实践

本文详细解析了Java中线程安全的HashMap实现------ConcurrentHashMap的工作原理。通过深入分析其内部源码,我们阐述了ConcurrentHashMap如何利用分段锁、CAS操作、扩容机制、近似计数等技术实现高并发和线程安全。同时,我们还提供了一些实际的使用示例,帮助读者更好地理解和掌握ConcurrentHashMap的使用方法。

1. ConcurrentHashMap简介

ConcurrentHashMap是Java中提供的一个线程安全的HashMap实现,它采用分段锁和CAS(Compare and Swap)操作等技术来实现高并发和线程安全。下面我们结合ConcurrentHashMap的内部源码解释分段锁、CAS操作、扩容机制、近似计数等技术如何实现的。

2. 分段锁原理

ConcurrentHashMap的内部结构是由多个Segment组成的数组。每个Segment独立维护一个HashEntry数组,并拥有一个独立的锁(ReentrantLock)。这样,在进行操作时,只需要锁定对应的Segment,而不需要锁定整个Map。这种分段锁的机制有效地减小了锁的粒度,提高了并发性能。

源码中的Segment定义如下:

java 复制代码
static final class Segment<K,V> extends ReentrantLock implements Serializable {
    private static final long serialVersionUID = 2249069246763182397L;

    // HashEntry 数组
    transient volatile HashEntry<K,V>[] table;

    // ...
}

3. CAS操作原理

ConcurrentHashMap使用CAS(Compare and Swap)操作来实现无锁的并发更新。在进行插入、删除和替换操作时,ConcurrentHashMap会尝试使用CAS操作来更新HashEntry数组,从而避免锁的开销。

源码中的HashEntry定义如下:

java 复制代码
static final class HashEntry<K,V> {
    final K key;
    final int hash;
    volatile V value;
    final HashEntry<K,V> next;
}

在ConcurrentHashMap的源码中,更新操作使用了Unsafe类提供的CAS方法,例如compareAndSwapObject()和compareAndSwapInt()等。这些方法可以实现无锁的原子更新,提高并发性能。

例如,在put操作中,ConcurrentHashMap使用CAS更新HashEntry的value:

java 复制代码
if (!onlyIfAbsent || oldValue == null) {
    V v = value;
    if (c.value != v) { // 保证原子性
        if (chm.casValue(hash, key, e, v, oldValue))
            return oldValue;
    }
}

4. 扩容机制原理

当ConcurrentHashMap的某个Segment的填充程度超过阈值时,为了保持性能,ConcurrentHashMap会对该Segment进行扩容。扩容操作涉及创建一个新的、更大的HashEntry数组,并将旧数组中的所有键值对重新插入到新数组中。这个过程称为"rehashing"。

源码中的扩容操作如下:

java 复制代码
void rehash() {
    HashEntry<K,V>[] oldTable = table;
    int oldCapacity = oldTable.length;
    if (oldCapacity >= MAXIMUM_CAPACITY)
        return;

    HashEntry<K,V>[] newTable = (HashEntry<K,V>[]) new HashEntry[oldCapacity << 1];
    threshold = (int)(newTable.length * loadFactor);
    int sizeMask = newTable.length - 1;
    for (int i = 0; i < oldCapacity ; i++) {
        // rehash
    }
    table = newTable;
}

5. 近似计数原理

ConcurrentHashMap提供了一些用于统计的方法,如size()、isEmpty()等。这些方法在ConcurrentHashMap的实现中,采用了一种近似计算的策略。由于ConcurrentHashMap是高并发的,要精确地计算元素个数会带来很大的性能开销。因此,ConcurrentHashMap允许这些统计方法返回一个近似值,从而在保持性能的同时,还能提供一定程度的准确性。

源码中的近似计数方法如下:

java 复制代码
public int size() {
    final Segment<K,V>[] segments = this.segments;
    long sum = 0L; // 使用 long 类型避免溢出
    long check = 0;
    int[] mc = new int[segments.length];
    // 重试
    for (int k = 0; k < 2; k++) {
        check = 0;
        sum = 0;
        int mcsum = 0;
        for (int i = 0; i < segments.length; ++i) {
            sum += segments[i].count;
            mcsum += (mc[i] = segments[i].modCount);
        }
        // 检查是否有正在进行的写操作
        for (int i = 0; i < segments.length; ++i) {
            check += segments[i].count;
            if (mc[i] != segments[i].modCount) {
                check = -1; // 发现写操作,需要重新计算
                break;
            }
        }
        if (check == sum)
            break;
    }
    if (check != sum) { // 如果检查失败,尝试使用锁进行精确计数
        sum = 0;
        for (Segment<K,V> segment : segments) {
            segment.lock();
            try {
                sum += segment.count;
            } finally {
                segment.unlock();
            }
        }
    }
    // 防止溢出
    if (sum > Integer.MAX_VALUE)
        return Integer.MAX_VALUE;
    else
        return (int)sum;
}

在这个方法中,ConcurrentHashMap会尝试两次计算元素个数。如果两次计算的结果一致,那么返回这个结果;如果不一致,说明有写操作正在进行,此时会使用锁进行精确计数。这种策略在保持性能的同时,还能提供一定程度的准确性。

6. 并发操作方法

ConcurrentHashMap提供了一些用于并发操作的方法,如putIfAbsent()、replace()、remove()等。这些方法可以在一个原子操作中完成检查和更新,从而避免多线程环境下的竞争条件。

例如,下面的代码展示了使用putIfAbsent()方法来实现一个线程安全的缓存:

java 复制代码
import java.util.concurrent.ConcurrentHashMap;

public class Cache {
    private ConcurrentHashMap<String, Object> cache = new ConcurrentHashMap<>();

    public Object get(String key) {
        return cache.get(key);
    }

    public void putIfAbsent(String key, Object value) {
        cache.putIfAbsent(key, value);
    }
}

在这个示例中,我们创建了一个Cache类,它使用ConcurrentHashMap来存储缓存数据。当我们需要添加一个键值对时,可以使用putIfAbsent()方法,这个方法会在键不存在时才添加键值对,从而避免覆盖已存在的值。

7. 遍历ConcurrentHashMap

ConcurrentHashMap的遍历操作也是线程安全的。它提供了keySet、values和entrySet等方法,可以返回Map的键集、值集或键值对集。这些方法返回的集合是ConcurrentHashMap的视图,它们会反映ConcurrentHashMap的实时状态。也就是说,你在遍历这些集合的过程中,其他线程对ConcurrentHashMap的修改操作是可见的。

例如,下面的代码展示了如何遍历ConcurrentHashMap:

java 复制代码
import java.util.concurrent.ConcurrentHashMap;

public class ConcurrentHashMapExample {
    public static void main(String[] args) {
        ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();

        // 添加键值对
        map.put("one", 1);
        map.put("two", 2);
        map.put("three", 3);

        // 遍历ConcurrentHashMap
        for (String key : map.keySet()) {
            System.out.println("Key: " + key + ", Value: " + map.get(key));
        }
    }
}

在这个示例中,我们创建了一个ConcurrentHashMap实例,然后使用put方法添加了一些键值对,最后使用for-each循环遍历了整个ConcurrentHashMap。这个遍历操作是线程安全的,即使在遍历过程中有其他线程修改ConcurrentHashMap,也不会抛出ConcurrentModificationException。

8. 扩展方法介绍

ConcurrentHashMap还提供了一些并发编程中常用的扩展方法,如compute、merge等。这些方法可以在一个原子操作中完成复杂的更新逻辑,从而避免多线程环境下的竞争条件。

例如,下面的代码展示了使用compute方法来实现一个线程安全的计数器:

java 复制代码
import java.util.concurrent.ConcurrentHashMap;

public class Counter {
    private ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();

    public void increment(String key) {
        map.compute(key, (k, v) -> (v == null) ? 1 : v + 1);
    }

    public int getCount(String key) {
        return map.getOrDefault(key, 0);
    }
}

在这个示例中,我们创建了一个Counter类,它使用ConcurrentHashMap来存储计数数据。当我们需要增加一个键的计数时,可以使用compute方法,这个方法会在键存在时增加计数,否则初始化计数为1。

9. 并发性能分析

由于ConcurrentHashMap采用了分段锁和CAS操作等技术,它在高并发环境下具有很好的性能。相比于同步的HashMap(如Hashtable或使用Collections.synchronizedMap包装的HashMap),ConcurrentHashMap在读操作上几乎没有锁的开销,在写操作上也只需要锁定部分段,因此并发性能更高。

然而,ConcurrentHashMap并不是万能的。在数据量较小或并发访问较低的情况下,简单的HashMap可能会更快。此外,ConcurrentHashMap也不能保证所有操作的全局有序性。如果需要全局有序性,可以考虑使用同步的Map实现,或者使用锁和其他同步工具来协调并发操作。

10. 局限性与适用场景

虽然ConcurrentHashMap在并发环境下提供了很好的性能,但它也有一些局限性。首先,ConcurrentHashMap的所有操作都是线程安全的,但如果你需要执行复合操作(例如,先检查一个键是否存在,然后根据结果进行更新操作),那么就需要额外的同步措施来保证这些操作的原子性。因为在两个操作之间,可能有其他线程修改了ConcurrentHashMap的状态。

其次,ConcurrentHashMap的size方法和isEmpty方法返回的结果是近似的,它们可能不会立即反映其他线程的修改操作。这是因为为了提高性能,ConcurrentHashMap没有使用全局锁来同步这些方法。

最后,虽然ConcurrentHashMap的并发性能很好,但如果你的应用场景中读操作远多于写操作,那么使用Read-Write Locks可能会获得更好的性能。Read-Write Locks允许多个线程同时读,但只允许一个线程写,这对于读多写少的场景是非常有效的。

11. 总结

ConcurrentHashMap是Java中提供的一个高性能、线程安全的HashMap实现。它采用了分段锁、CAS操作、扩容机制、近似计数等技术,实现了高并发和线程安全。在需要处理并发访问的场景中,ConcurrentHashMap是一个非常实用的工具。

在实际应用中,我们需要根据具体的场景和需求来选择合适的数据结构。

  • 如果需要高并发访问和更新,那么ConcurrentHashMap是一个很好的选择。
  • 如果数据量较小或并发访问较低,简单的HashMap可能会更快。
  • 如果需要全局有序性,可以考虑使用同步的Map实现,或者使用锁和其他同步工具来协调并发操作。

推荐阅读

图解ConcurrentHashMap

相关推荐
Synaric13 分钟前
Android与Java后端联调RSA加密的注意事项
android·java·开发语言
程序员老刘·1 小时前
如何评价Flutter?
android·flutter·ios
JoyceMill3 小时前
Android 图像效果的奥秘
android
想要打 Acm 的小周同学呀4 小时前
ThreadLocal学习
android·java·学习
天下是个小趴菜4 小时前
蚁剑编码器编写——中篇
android
命运之手4 小时前
【Android】自定义换肤框架05之Skinner框架集成
android·skinner·换肤框架·不重启换肤·无侵入换肤
DS小龙哥4 小时前
QT+OpenCV在Android上实现人脸实时检测与目标检测
android·人工智能·qt·opencv·目标检测
SwBack5 小时前
【pearcmd】通过pearcmd.php 进行GetShell
android·开发语言·php
miao_zz5 小时前
react native中依赖@react-native-clipboard/clipboard库实现文本复制以及在app中获取复制的文本内容
android·react native·react.js
小羊子说5 小时前
Android 开发中 C++ 和Java 日志调试
android·java·c++