ConcurrentHashMap让你的代码飞起来【Java多线程必备】

目录

一、介绍

二、特性

三、原理

四、使用场景

五、注意事项

六、实际应用


一、介绍

ConcurrentHashMap 是 JDK 提供的线程安全的哈希表实现,它可以支持高并发的读写操作,并且具有很好的扩展性。它是在 Java 1.5 中引入的,并且在 Java 1.8 中进行了优化。ConcurrentHashMap 底层采用分段锁技术来实现线程安全,其内部结构包含多个 Segment,每个 Segment 本质上就是一个线程安全的哈希表。

二、特性

1. 线程安全

ConcurrentHashMap 的 put、get、remove 等操作都是线程安全的,不需要额外的同步机制,因此可以在多线程环境下安全地使用。

2. 高效性

ConcurrentHashMap 在并发环境下可以提供较高的吞吐量,它通过使用分段锁技术来实现线程安全,每个 Segment 都拥有一把锁,不同的线程可以同时访问不同的 Segment,从而提高并发性。

3. 支持高并发读写操作

ConcurrentHashMap 支持多个线程同时进行读操作,而且不会阻塞写操作,因此可以在高并发环境下保持较好的性能。

4. 支持更多的操作

ConcurrentHashMap 还提供了一些其他的操作,如 replace、putIfAbsent 等,方便开发人员实现更多的业务需求。

5. ConcurrentHashMap 的性能优化

为了进一步提高 ConcurrentHashMap 的性能,Java 8 中对其进行了一些优化。其中一个重要的优化是利用 CAS 操作替换了之前的 Synchronized 关键字,从而减少了锁的争用,提高了并发度。

三、原理

ConcurrentHashMap 采用分段锁技术来实现线程安全,其内部结构包含多个 Segment,每个 Segment 本质上就是一个线程安全的哈希表。每个 Segment 都拥有一把锁,不同的线程可以同时访问不同的 Segment,从而提高并发性。在读操作时,可以同时支持多个线程进行读操作,而在写操作时,只需要锁住对应的 Segment,不会对其他的 Segment 进行影响。

ConcurrentHashMap 中的哈希表采用数组 + 链表 + 红黑树的数据结构实现,其中数组是用来存储哈希桶的,链表和红黑树则是用来解决哈希冲突的。当链表中的元素达到一定的数量时,链表会被转换成红黑树,以提高查找和删除操作的性能。

四、使用场景

1. 高并发环境

ConcurrentHashMap最适合在高并发环境下使用,特别是读写操作频繁的场景。在这种场景下,使用普通的HashMap 可能会出现线程安全问题,使用Hashtable的话效率相对较低。

2. 大规模数据存储

由于 ConcurrentHashMap 内部采用分段锁技术来实现线程安全,因此它的并发性能非常好,并且支持动态调整 Segment 的数量,因此可以很好地支持大规模数据的存储和处理。

3. 缓存

ConcurrentHashMap 也非常适合作为缓存的存储结构,因为它支持高并发读写操作,并且具有很好的扩展性。在缓存中,读操作比写操作要频繁得多,因此可以在保证线程安全的同时提高缓存的性能。

五、注意事项

1. ConcurrentHashMap 的迭代器是弱一致性的

由于 ConcurrentHashMap 是一个并发容器,因此在遍历容器时,可能会出现一些数据的不一致性问题。虽然 ConcurrentHashMap 提供了多种遍历方式,但是无论是使用迭代器、forEach 还是 Spliterator,都需要注意它们是弱一致性的,可能会看到过时的数据。

2. ConcurrentHashMap 的 putIfAbsent() 方法

ConcurrentHashMap 中提供了一个 putIfAbsent(K key, V value) 方法,可以将一个 key-value 对添加到 ConcurrentHashMap 中,但是只有在该 key 还不存在的情况下才会添加成功。这个方法常常用于缓存中的添加操作,可以避免重复添加相同的数据。

3. ConcurrentHashMap 的 size() 方法

ConcurrentHashMap 中的 size() 方法并不总是返回准确的大小,因为在多线程环境下,可能会存在一些数据的不一致性。虽然 ConcurrentHashMap 提供了一些获取近似大小的方法,但是需要注意它们的精度可能会受到一些限制。

4. ConcurrentHashMap 的初始化容量

在创建 ConcurrentHashMap 对象时,需要指定其初始化容量和负载因子。为了提高性能,建议将初始化容量设置为预期存储的 key-value 对数量的两倍左右。如果初始化容量设置得太小,可能会导致扩容过程比较频繁,影响性能。

六、实际应用

1. 案例一

(1) 场景

使用ConcurrentHashMap的一些方法的案例。

(2) 代码

import java.util.concurrent.ConcurrentHashMap;

/**
 * ConcurrentHashMapCase1
 * 简单使用ConcurrentHashMap的一些方法
 *
 * @author wxy
 * @since 2023-04-26
 */
public class ConcurrentHashMapCase1 {
    public static void main(String[] args) {
        ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();
        map.put("one", 1);
        map.put("two", 2);
        map.put("three", 3);

        // 输出 1
        System.out.println(map.get("one"));

        map.putIfAbsent("four", 4);
        map.putIfAbsent("four", 5);
        map.putIfAbsent("four", 6);
        // 输出 4
        System.out.println(map.get("four"));

        map.replace("two", 22);
        // 输出 22
        System.out.println(map.get("two"));

        map.remove("three");
        // 输出 null
        System.out.println(map.get("three"));
    }
}

在这个示例代码中,我们创建了一个 ConcurrentHashMap 对象,并向其中添加了三个 key-value 对。然后我们使用 get() 、putIfAbsent()、replace() 、remove() 方法进行演示。

2. 案例二

(1) 场景

ConcurrentHashMap和HashMap在多线程情况下, 缓存数据准确程度。

(2) 代码

import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

/**
 * ConcurrentHashMapCase2: ConcurrentHashMap相对于HashMap有什么根本区别呢?
 * CopyOnWriteArrayList: 线程安全。
 * ArrayList: 线程不安全。
 * ConcurrentHashMap和HashMap在多线程情况下, 缓存数据准确程度。
 *
 * @author wxy
 * @since 2023-04-26
 */
public class ConcurrentHashMapCase2 {
    public static void main(String[] args) throws InterruptedException {
        Map<String, String> concurrentHashMap = new ConcurrentHashMap<>(500);
        Map<String, String> hashMap = new HashMap<>(500);

        // 创建两个线程并启动它们
        Thread thread1 = new Thread(() -> {
            for (int index1 = 0; index1 < 1000; index1++) {
                concurrentHashMap.put(Integer.toString(index1), Integer.toBinaryString(index1));

                hashMap.put(Integer.toString(index1), Integer.toBinaryString(index1));
            }
        });

        Thread thread2 = new Thread(() -> {
            for (int index2 = 1000; index2 < 2000; index2++) {
                concurrentHashMap.put(Integer.toString(index2), Integer.toBinaryString(index2));

                hashMap.put(Integer.toString(index2), Integer.toBinaryString(index2));
            }
        });

        thread1.start();
        thread2.start();

        // 等待两个线程结束
        thread1.join();
        thread2.join();

        System.out.println("ConcurrentHashMap size: " + concurrentHashMap.size());
        System.out.println("HashMap size: " + hashMap.size());
    }
}

运行结果如下:

为什么在上述代码中,HashMap没有相同的键值对,却导致一部分键值被覆盖了呢?实际上,即使没有相同的键值对,也可能会出现这种情况。

这是因为HashMap不是线程安全的,它的内部结构由一个数组和一个单向链表组成。在多线程环境下,如果两个线程同时调用put方法,可能会出现以下情况:

  1. 线程1正在往数组位置i处添加一个键值对,但还没有将链表上的其他元素复制到新的链表中。

  2. 线程2也要往数组位置i处添加一个键值对,发现此时链表上还只有旧的元素,于是也将它们复制到新的链表中。

  3. 线程1完成了它的操作,将新的键值对插入到新的链表的头部。

  4. 线程2完成了它的操作,将新的键值对插入到新的链表的头部。

由于HashMap的put方法并没有同步,所以这种情况下会出现并发修改异常。其中一个键值对会被覆盖,最终导致HashMap大小不到预期值。如果使用ConcurrentHashMap就不会出现上述情况。

  • 5
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

懒阳快跑

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值