深入理解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

相关推荐
-优势在我5 小时前
Android TabLayout 实现随意控制item之间的间距
android·java·ui
hedalei5 小时前
android13修改系统Launcher不跟随重力感应旋转
android·launcher
Indoraptor7 小时前
Android Fence 同步框架
android
峥嵘life7 小时前
DeepSeek本地搭建 和 Android
android
叶羽西7 小时前
Android14 Camera框架中Jpeg流buffer大小的计算
android·安卓
jiasting7 小时前
Android 中 如何监控 某个磁盘有哪些进程或线程在持续的读写
android
AnalogElectronic10 小时前
问题记录,在使用android studio 构建项目时遇到的问题
android·ide·android studio
我爱松子鱼10 小时前
mysql之InnoDB Buffer Pool 深度解析与性能优化
android·mysql·性能优化
江上清风山间明月13 小时前
Flutter开发的应用页面非常多时如何高效管理路由
android·flutter·路由·页面管理·routes·ongenerateroute
子非衣17 小时前
MySQL修改JSON格式数据示例
android·mysql·json