• 线程安全使用 HashMap 的四种技巧


    这篇文章,我们聊聊线程安全使用 HashMap 的四种技巧。

    1方法内部:每个线程使用单独的 HashMap

    如下图,tomcat 接收到到请求后,依次调用控制器 Controller、服务层 Service 、数据库访问层的相关方法。

    每次访问服务层方法 serviceMethod 时,都会在方法体内部创建一个单独的 HashMap , 将相关请求参数拷贝到 HashMap 里,然后调用 DAO 方法进行数据库操作。

    每个 HTTP 处理线程在服务层方法体内部都有自己的 HashMap 实例,在多线程环境下,不需要对 HashMap 进行任何同步操作。

    这也是我们使用最普遍也最安全的的方式,是 CRUD 最基本的操作。

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

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

    上图中显示一个非常简单的配置类 SimpleConfig ,内部有一个 HashMap 对象 configMap 。构造函数调用初始化方法,初始化方法内部的逻辑是:将配置数据存储到 HashMap 中。

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

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

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

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

    我们一般都使用 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 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 是一个非常经典的技巧,消息中间件 RockeMQ NameServer (名字服务)保存和查询路由信息都是通过这种技巧实现的。

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

    4 Collections.synchronizedMap : 读写均加锁

    如下代码,当我们多线程使用 userMap 时,

    static Map<Long, User> userMap = Collections.synchronizedMap(new HashMap<Long, User>());
    

    进入 synchronizedMap 方法:

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

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

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

    5 总结

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

    1、方法内部:每个线程使用单独的 HashMap

    这是我们使用最普遍,也是非常可靠的方式。每个线程在方法体内部创建HashMap 实例,在多线程环境下,不需要对 HashMap 进行任何同步操作。

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

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

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

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

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

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

    4、Collections.synchronizedMap : 读写均加锁

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

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


    如果我的文章对你有所帮助,还请帮忙点赞、在看、转发一下,你的支持会激励我输出更高质量的文章,非常感谢!

  • 相关阅读:
    conda安装pyhanlp遇到的问题及解决方法
    我们能做出来数据库吗?
    100道JVM面试题大全最新版2023版
    P23~33第7章 一阶电路和二阶电路的时域分析 详情可以看看书
    Microbiome 跨组学功能网络预测神器
    C++核心编程:P18->STL----常用算法(上)
    武汉星起航:瑞典波兰首次参与亚马逊prime会员日内藏市场潜力
    ubuntu下Anaconda安装与使用教程
    k8s图形界面登录报错Failure
    电场刺激响应性和AIE水凝胶/调控发射波长及亮度AIE微球/AIE糖肽聚合物的研究
  • 原文地址:https://www.cnblogs.com/makemylife/p/18200927