如何线程安全的使用HashMap_map 线程安全-CSDN博客

前言

Map一直是面试中经常被问到的问题。博主在找工作的过程中,就被问到了这样一个问题:

  1. Map是线程安全的吗?
  2. 我不考虑使用线程安全的Map(eg:ConcurrentHashMap) 。如何在多线程/高并发下安全使用 HashMap

当时博主只知道 HashMap是线程不安全的,但是没了解过要如何在多线程下保证 HashMap的安全。于是在面试结束后,我查阅了相关的资料,也进行了对应的 Code。这篇文章,我们就来深入浅出聊一聊以上的两个面试常问题。

    • *

1. Map 是线程安全的吗?

首先,我们常见的 Map的实现类有:HashtableHashMapConcurrentHashMap这三种。

先说结论:得看 **Map**接口的实现类,**Hashtable****ConcurrentHashMap**是线程安全的,最常用的**HashMap**是非线程安全的。

那么,Hashtable 和 ConcurrentHashMap 的区别是什么?

ConcurrentHashMapHashtable 的区别主要体现在实现线程安全的方式不同。

  • 底层数据结构: JDK1.7 的 ConcurrentHashMap 底层采用 分段的数组+链表 实现,JDK1.8 采用的数据结构跟 HashMap1.8 的结构一样:数组+链表/红黑二叉树Hashtable 和 JDK1.8 之前的 HashMap 的底层数据结构类似都是采用 数组+链表 的形式,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的;
  • 实现线程安全的方式(重要)
  • 到了 JDK1.8 的时候,ConcurrentHashMap 已经摒弃了 Segment 的概念,而是直接用 Node 数组+链表+红黑树的数据结构来实现,并发控制使用 **synchronized** 和 CAS 来操作。(JDK1.6 以后 synchronized 锁做了很多优化) 整个看起来就像是优化过且线程安全的 HashMap,虽然在 JDK1.8 中还能看到 Segment 的数据结构,但是已经简化了属性,只是为了兼容旧版本;
  • **Hashtable**(同一把锁) :使用 synchronized 来保证线程安全,效率非常低下。当一个线程访问同步方法时,其他线程也访问同步方法,可能会进入阻塞或轮询状态,如使用 put 添加元素,另一个线程不能使用 put 添加元素,也不能使用 get,竞争会越来越激烈效率越低。

Java 8 中,ConcurrentHashMap 的锁粒度更细,synchronized 只锁定当前链表或红黑二叉树的首节点,这样只要 hash 不冲突,就不会产生并发,就不会影响其他 Node 的读写,效率大幅提升。

    • *

2. 如何在多线程/高并发下安全使用 HashMap

博主提供三种方法来保证线程安全的使用 HashMap

2.1. 配置数据:初始化写,后续只提供读

系统启动之后,我们可以将配置数据加载到本地缓存 HashMap 里 ,这些配置信息初始化之后,就不需要写入了,后续只提供读操作。

上图中显示一个非常简单的配置类SimpleConfig,内部有一个HashMap对象configMap

构造函数调用初始化方法,初始化方法内部的逻辑是:将配置数据存储到HashMap中。

SimpleConfig类对外暴露了getConfig方法 ,当main线程初始化SimpleConfig对象之后,当其他线程调用 getConfig方法时,因为只有读,没有写操作,所以是线程安全的。

2.2. 读写锁:写时阻塞,并行读,读多写少场景

读写锁是一把锁分为两部分:读锁和写锁,其中读锁允许多个线程同时获得,而写锁则是互斥锁。

它的规则是:读读不互斥,读写互斥,写写互斥,适用于读多写少的业务场景。

我们一般都使用 ReentrantReadWriteLock ,该类实现了 ReadWriteLock 。ReadWriteLock 接口也很简单,其内部主要提供了两个方法,分别返回读锁和写锁 。

 public interface ReadWriteLock {    //获取读锁    Lock readLock();    //获取写锁    Lock writeLock();}

读写锁的使用方式如下所示:

  1. 创建 ReentrantReadWriteLock 对象 , 当使用 ReadWriteLock 的时候,并不是直接使用,而是获得其内部的读锁和写锁,然后分别调用 lock / unlock 方法 ;
private ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
  1. 读取共享数据 ;
// 获取读锁Lock readLock = readWriteLock.readLock();readLock.lock();try {    // TODO 查询共享数据} finally {    readLock.unlock();}
  1. 写入共享数据;
// 获取写锁Lock writeLock = readWriteLock.writeLock();writeLock.lock();try {    // TODO 修改共享数据} finally {    writeLock.unlock();}

下面的代码展示如何使用 ReadWriteLock 线程安全的使用 HashMap :

import java.util.HashMap;import java.util.Map;import java.util.concurrent.locks.ReadWriteLock;import java.util.concurrent.locks.ReentrantReadWriteLock; public class ReadWriteLockCache {     // 创建一个 HashMap 来存储缓存的数据    private Map<String, String> map = new HashMap<>();     // 创建读写锁对象    private ReadWriteLock rw = new ReentrantReadWriteLock();     // 放对象方法:向缓存中添加一个键值对    public void put(String key, String value) {        // 获取写锁,以确保当前操作是独占的        rw.writeLock().lock();        try {            // 执行写操作,将键值对放入 map            map.put(key, value);        } finally {            // 释放写锁            rw.writeLock().unlock();        }    }     // 取对象方法:从缓存中获取一个值    public String get(String key) {        // 获取读锁,允许并发读操作        rw.readLock().lock();        try {            // 执行读操作,从 map 中获取值            return map.get(key);        } finally {            // 释放读锁            rw.readLock().unlock();        }    }}

使用读写锁操作HashMap是一个非常经典的技巧,消息中间件 `RocketMQ NameServer (名字服务)保存和查询路由信息都是通过这种技巧实现的。

另外,读写锁可以操作多个**HashMap**,相比**ConcurrentHashMap**而言,**ReadWriteLock**可以控制缓存对象的颗粒度,具备更大的灵活性

2.3. Collections.synchronizedMap : 读写均加锁

我们可以使用 Collections.synchronizedMap()方法来对我们的 HashMap进行加锁保护。

如下所示:

public static <K,V> Map<K,V> synchronizedMap(Map<K,V> m) {       return new SynchronizedMap<>(m);}

我们点进 SynchronizedMap方法看看源码是如何实现加锁保护的:

SynchronizedMap 内部包含一个对象锁 Object mutex ,它本质上是一个包装类,将HashMap的读写操作重新实现了一次,我们看到每次读写时,都会用synchronized关键字来保证操作的线程安全。

虽然**Collections.synchronizedMap**这种技巧使用起来非常简单,但是我们需要理解它的每次读写都会加锁,性能并不会特别好。

3. 总结

这篇文章,笔者总结了三种线程安全的使用 HashMap 的技巧。

1、配置数据:初始化写,后续只提供读

中间件在启动时,会读取配置文件,将配置数据写入到 HashMap 中,主线程写完之后,以后不会再有写入操作,其他的线程可以读取,不会产生线程安全问题。

2、读写锁:写时阻塞,并行读,读多写少场景

读写锁是一把锁分为两部分:读锁和写锁,其中读锁允许多个线程同时获得,而写锁则是互斥锁。

它的规则是:读读不互斥,读写互斥,写写互斥,适用于读多写少的业务场景。

使用读写锁操作 HashMap 是一个非常经典的技巧,消息中间件 RockeMQ NameServer (名字服务)保存和查询路由信息都是通过这种技巧实现的。

3、Collections.synchronizedMap : 读写均加锁

Collections.synchronizedMap 方法使用了装饰器模式为线程不安全的 HashMap 提供了一个线程安全的装饰器类 SynchronizedMap。

通过SynchronizedMap来间接的保证对 HashMap 的操作是线程安全,而 SynchronizedMap 底层也是通过 synchronized 关键字来保证操作的线程安全。


原网址: 访问
创建于: 2025-01-14 11:20:00
目录: default
标签: 无

请先后发表评论
  • 最新评论
  • 总共0条评论