Map
一直是面试中经常被问到的问题。博主在找工作的过程中,就被问到了这样一个问题:
Map
是线程安全的吗?Map(eg:ConcurrentHashMap)
。如何在多线程/高并发下安全使用 HashMap
?当时博主只知道 HashMap
是线程不安全的,但是没了解过要如何在多线程下保证 HashMap
的安全。于是在面试结束后,我查阅了相关的资料,也进行了对应的 Code。这篇文章,我们就来深入浅出聊一聊以上的两个面试常问题。
首先,我们常见的 Map
的实现类有:Hashtable
、HashMap
、ConcurrentHashMap
这三种。
先说结论:得看 **Map**
接口的实现类,**Hashtable**
和**ConcurrentHashMap**
是线程安全的,最常用的**HashMap**
是非线程安全的。
那么,Hashtable 和 ConcurrentHashMap 的区别是什么?
ConcurrentHashMap
和 Hashtable
的区别主要体现在实现线程安全的方式不同。
ConcurrentHashMap
底层采用 分段的数组+链表 实现,JDK1.8 采用的数据结构跟 HashMap1.8
的结构一样:数组+链表/红黑二叉树。Hashtable
和 JDK1.8 之前的 HashMap
的底层数据结构类似都是采用 数组+链表 的形式,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的;ConcurrentHashMap
已经摒弃了 Segment
的概念,而是直接用 Node
数组+链表+红黑树的数据结构来实现,并发控制使用 **synchronized**
和 CAS 来操作。(JDK1.6 以后 synchronized
锁做了很多优化) 整个看起来就像是优化过且线程安全的 HashMap
,虽然在 JDK1.8 中还能看到 Segment
的数据结构,但是已经简化了属性,只是为了兼容旧版本;**Hashtable**
(同一把锁) :使用 synchronized
来保证线程安全,效率非常低下。当一个线程访问同步方法时,其他线程也访问同步方法,可能会进入阻塞或轮询状态,如使用 put 添加元素,另一个线程不能使用 put 添加元素,也不能使用 get,竞争会越来越激烈效率越低。Java 8 中,ConcurrentHashMap 的锁粒度更细,synchronized
只锁定当前链表或红黑二叉树的首节点,这样只要 hash 不冲突,就不会产生并发,就不会影响其他 Node 的读写,效率大幅提升。
HashMap
?博主提供三种方法来保证线程安全的使用 HashMap
系统启动之后,我们可以将配置数据加载到本地缓存 HashMap 里 ,这些配置信息初始化之后,就不需要写入了,后续只提供读操作。
上图中显示一个非常简单的配置类SimpleConfig
,内部有一个HashMap
对象configMap
。
构造函数调用初始化方法,初始化方法内部的逻辑是:将配置数据存储到HashMap
中。
SimpleConfig
类对外暴露了getConfig
方法 ,当main
线程初始化SimpleConfig
对象之后,当其他线程调用 getConfig
方法时,因为只有读,没有写操作,所以是线程安全的。
读写锁是一把锁分为两部分:读锁和写锁,其中读锁允许多个线程同时获得,而写锁则是互斥锁。
它的规则是:读读不互斥,读写互斥,写写互斥,适用于读多写少的业务场景。
我们一般都使用 ReentrantReadWriteLock ,该类实现了 ReadWriteLock 。ReadWriteLock 接口也很简单,其内部主要提供了两个方法,分别返回读锁和写锁 。
public interface ReadWriteLock { //获取读锁 Lock readLock(); //获取写锁 Lock writeLock();}
读写锁的使用方式如下所示:
private ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
// 获取读锁Lock readLock = readWriteLock.readLock();readLock.lock();try { // TODO 查询共享数据} finally { readLock.unlock();}
// 获取写锁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**
可以控制缓存对象的颗粒度,具备更大的灵活性。
我们可以使用 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**
这种技巧使用起来非常简单,但是我们需要理解它的每次读写都会加锁,性能并不会特别好。
这篇文章,笔者总结了三种线程安全的使用 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
标签: 无
未标明原创文章均为采集,版权归作者所有,转载无需和我联系,请注明原出处,南摩阿彌陀佛,知识,不只知道,要得到
java windows火焰图_mob64ca12ec8020的技术博客_51CTO博客 - 在windows下不可行,不知道作者是怎样搞的 监听SpringBoot 服务启动成功事件并打印信息_监听springboot启动完毕-CSDN博客 SpringBoot中就绪探针和存活探针_management.endpoint.health.probes.enabled-CSDN博客 u2u转换板 - 嘉立创EDA开源硬件平台 Spring Boot 项目的轻量级 HTTP 客户端 retrofit 框架,快来试试它!_Java精选-CSDN博客 手把手教你打造一套最牛的知识笔记管理系统! - 知乎 - 想法有重合-理论可参考 安宇雨 闲鱼 机械键盘 客制化 开贴记录 文本 linux 使用find命令查找包含某字符串的文件_beijihukk的博客-CSDN博客_find 查找字符串 ---- mac 也适用 安宇雨 打字音 记录集合 B站 bilibili 自行搭建 开坑 真正的客制化 安宇雨 黑苹果开坑 查找工具包maven pom 引用地 工具网站 Dantelis 介绍的玩轴入坑攻略 --- 关于轴的一些说法 --- 非官方 ---- 心得而已 --- 长期开坑更新 [本人问题][新开坑位]关于自动化测试的工具与平台应用 机械键盘 开团 网站记录 -- 能做一个收集的程序就好了 不过现在没时间 -- 信息大多是在群里发的 - 你要让垃圾佬 都去一个地方看难度也是很大的 精神支柱 [超级前台]sprinbboot maven superdesk-app 记录 [信息有用] [环境准备] [基本完成] [sebp/elk] 给已创建的Docker容器增加新的端口映射 - qq_30599553的博客 - CSDN博客 [正在研究] Elasticsearch, Logstash, Kibana (ELK) Docker image documentation elasticsearch centos 安装记录 及 启动手记 正式服务器 39 elasticsearch 问题合集 不断更新 6.1.1 | 6.5.1 两个版本 博客程序 - 测试 - bug记录 等等问题 laravel的启动过程解析 - lpfuture - 博客园 OAuth2 Server PHP 用 Laravel 搭建带 OAuth2 验证的 RESTful 服务 | Laravel China 社区 - 高品质的 Laravel 和 PHP 开发者社区 利用Laravel 搭建oauth2 API接口 附 Unauthenticated 解决办法 - 煮茶的博客 - SegmentFault 思否 使用 OAuth2-Server-php 搭建 OAuth2 Server - 午时的海 - 博客园 基于PHP构建OAuth 2.0 服务端 认证平台 - Endv - 博客园 Laravel 的 Artisan 命令行工具 Laravel 的文件系统和云存储功能集成 浅谈Chromium中的设计模式--终--Observer模式 浅谈Chromium中的设计模式--二--pre/post和Delegate模式 浅谈Chromium中的设计模式--一--Chromium中模块分层和进程模型 DeepMind 4 Hacking Yourself README.md update 20211011
Laravel China 简书 知乎 博客园 CSDN博客 开源中国 Go Further Ryan是菜鸟 | LNMP技术栈笔记 云栖社区-阿里云 Netflix技术博客 Techie Delight Linkedin技术博客 Dropbox技术博客 Facebook技术博客 淘宝中间件团队 美团技术博客 360技术博客 古巷博客 - 一个专注于分享的不正常博客 软件测试知识传播 - 测试窝 有赞技术团队 阮一峰 语雀 静觅丨崔庆才的个人博客 软件测试从业者综合能力提升 - isTester IBM Java 开发 使用开放 Java 生态系统开发现代应用程序 pengdai 一个强大的博主 HTML5资源教程 | 分享HTML5开发资源和开发教程 蘑菇博客 - 专注于技术分享的博客平台 个人博客-leapMie 流星007 CSDN博客 - 舍其小伙伴 稀土掘金 Go 技术论坛 | Golang / Go 语言中国知识社区
最新评论