聊聊ConcurrentHashMap

本文深入探讨了ConcurrentHashMap的设计理念,包括其如何解决HashMap线程不安全的问题,并通过分段锁技术提高并发性能。此外,还介绍了Java 1.8中ConcurrentHashMap的新实现方式。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

聊聊ConcurrentHashMap

一、为什么需要ConcurrentHashMap

1、HashMap 线程不安全

在多线程环境下,使用HashMap进行 put 操作的时候可能造成死循环,导致 CPU 使用率太高

为什么HashMap线程不安全?

在 put 的时候,插入元素超过了容量,就会进行rehash,这个会重新将原数组的内容重新hash到新的扩容数组中,在多线程的环境下,存在同时其他的元素也在进行 put 操作,如果 hash 值相同,可能出现同时在同一数组下用链表表示,造成闭环,导致在 get 时会出现死循环,所以不安全

2、HashTable 安全但是效率太低了

因为 HashTable 是利用 synchronized 来保证线程安全的,在线程竞争激烈的情况下效率将会非常低。因为在一个线程访问同步方法的时候,其他线程只能阻塞等待。

二、ConcurrentHashMap 的 好处

实现了前面的两个问题 :线程安全了 并且也解决了HashTable 效率低下的问题

三、怎么解决效率低下的问题

这个主要是运用了分段锁(segment)的思想

HashTable 为什么效率低,就是因为它是多个线程竞争同一把锁,那么如果容器里面有很多把锁,这个问题是不是就可以解决了,这个就是ConcurrentHashMap所使用的分段锁技术。

首先将数据分为一段一段的进程存储,然后给每一段分别加上锁,当一个线程占用锁访问其中一个段的数据的时候,其他段的数据也可以被其他线程访问。

接下来我们就重点讲讲分段锁吧

ConcurrentHashMap 是由Segment 数组结构和HashEntry数组结构组成的。Segment是一种可重入锁ReentrantLock,在 ConcurrentHashMap 里面扮演锁的角色,HashEntry 则是用来村塾键值对数据。一个 ConcurrentHashMap 里面包含一个 Segment数组,Segment的结构和HashMap类似,是一种数组+链表结构,一个Segment里面包含一个 HashEntry数组,每个HashEntry是一个链表节点构成的元素,每个Segment守护一个HashEntry数组里面的元素,当对HashEntry数组的元素进行修改时,必须首先获得它对应的Segment锁。可以说,ConcurrentHashMap是一个二级的哈希表。在一个总的哈希表下面还有若干个子哈希表。

在这里插入图片描述

四、采用分段锁技术的好处:并发的读写

case1:不同Segment的并发写入:

在这里插入图片描述

不同的Segment的写入是可以并发执行的

case 2:同一个Segment 的写
在这里插入图片描述

Segment的写入是需要上锁的,因此对同一个Segment的并发写会被阻塞

case 3:同一个Segment 的写-读

同一个Segment的写-读是可以并发执行的

五、详细看看 读-写 的过程
1、读:Get()
  • 为输入的Key做 Hash 运算,得到 hash 值
  • 通过 hash 值,定位到对应的Segment 对象
  • 再次通过 hash 值,定位到 Segment 当中数组的具体位置

读操作其实是没有锁的,第一次通过 hash 定位到 Segment 上,第二次通过 hash 定位到 具体元素上。因为 hashEntry 中的 value 属性是用 volatile 修饰的,保证了可见性,所以每次获取的都死最新值。

2、写:Put()
  • 为输入的 Key 做 Hash 运算,得到 hash 值
  • 通过 hash 值,定位到对应的 Segment 对象
  • 获取可重入锁
  • 再次通过 hash 值,定位到 Segment 当中数组的具体位置
  • 插入或覆盖 HashEntry 对象
  • 释放锁

总结:可以看出ConcurrentHashMap在读写的时候都需要两次定位。首先是定位到 Segment ,然后再定位到 Segment 下的具体的数组下标

六、size() 怎么 解决一致性问题

size()目的是统计ConcurrentHashMap的总元素数量,自然需要把各个Segment 内部的元素都加起来。但是在统计数量的时候,有可能 已经统计过的Segment顺佳插入了新的元素,这个时候应该怎么办?下面我们来看看ConcurrentHashMap的size(),他是一个嵌套循环,大致逻辑如下:

  1. 遍历所有的 Segment
  2. 把 Segment 的元素数量累加起来
  3. 把 Segment 的修改次数累加起来
  4. 判断所有 Segment 的总修改次数是否大于上一次的总修改次数。如果大于,说明统计过程中有修改,重新统计,尝试次数+1;如果不是,说明没有修改,统计结束
  5. 如果尝试次数超过阈值,则对每一个 Segment 加锁,在重新统计
  6. 再次判断所有 Segment 的总修改次数是否大于上一次的总修改次数。由于已经加锁,次数一定和上一次相同
  7. 释放锁,统计结束

总结:一开始对 Segment 不加锁,而是直接尝试将所有的 Segment 元素中的count相加,这样执行两次,然后将两次的结果进行对比,如果两次结果相同则直接返回;如果不相同,则将所有的Segment加锁,然后再次执行统计得到对应的 size 值。

七、再看看1.8是怎么实现的

在这里插入图片描述

1.8 抛弃了分段锁(Segment),而是采取的是 CAS + Synchronized 的方式

将1.7里面的 HashEntry 改为 Node ,但是作用是一样的。

先看看一个概念:乐观锁和悲观锁

悲观锁:认为对于同一个数据的并发操作,一定是为发生修改的

乐观锁:对于同一个数据的并发操作是不会发生修改的,在更新的时候会采取尝试更新不断尝试的方式更新数据

CAS(compare and swap,比较交换)原理:

CAS有三个操作数,内存值V、预期值A、要修改的值B,当且仅当A和V相等时才会将V修改为B,否则什么也不会做。

CAS的缺点:

存在 ABA 问题,解决办法,添加版本号

循环时间长,开销大

只能保证一个共享变量的原子操作

### 如何使用 WebSocket 实现一对一聊天功能 要通过 WebSocket 实现一对一聊天功能,可以按照以下方式构建项目并完成相关配置。 #### 1. 添加依赖项 为了在 Spring Boot 中启用 WebSocket 功能,需要引入 `spring-boot-starter-websocket` 的依赖。这可以通过 Maven 或 Gradle 完成[^1]: 对于 Maven 用户: ```xml <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-websocket</artifactId> </dependency> ``` 对于 Gradle 用户: ```gradle implementation 'org.springframework.boot:spring-boot-starter-websocket' ``` #### 2. 配置 WebSocket 支持 创建一个名为 `WebSocketStompConfig` 的 Java 类来开启 WebSocket 支持。此配置类会注册 `ServerEndpointExporter` Bean 来启动 WebSocket 连接服务[^2]: ```java package com.example.websocket.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.socket.server.standard.ServerEndpointExporter; @Configuration public class WebSocketStompConfig { @Bean public ServerEndpointExporter serverEndpointExporter() { return new ServerEndpointExporter(); } } ``` #### 3. 创建 WebSocket 处理器 定义一个处理器用于处理客户端的消息发送和接收逻辑。该处理器通常继承自 `TextWebSocketHandler` 并重写其方法以满足需求[^3]: ```java package com.example.websocket.handler; import org.springframework.stereotype.Component; import org.springframework.web.socket.TextMessage; import org.springframework.web.socket.WebSocketSession; import org.springframework.web.socket.handler.TextWebSocketHandler; @Component public class ChatWebSocketHandler extends TextWebSocketHandler { private final Map<String, WebSocketSession> userSessions = new ConcurrentHashMap<>(); @Override public void afterConnectionEstablished(WebSocketSession session) throws Exception { String username = (String) session.getAttributes().get("username"); userSessions.put(username, session); } @Override protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception { String payload = message.getPayload(); JSONObject jsonObject = new JSONObject(payload); String sender = jsonObject.getString("sender"); String receiver = jsonObject.getString("receiver"); String content = jsonObject.getString("content"); WebSocketSession receiverSession = userSessions.get(receiver); if (receiverSession != null && receiverSession.isOpen()) { receiverSession.sendMessage(new TextMessage(content)); } } @Override public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception { String username = (String) session.getAttributes().get("username"); userSessions.remove(username); } } ``` 在此代码片段中,消息被解析为 JSON 格式,并根据发件人 (`sender`) 和收件人 (`receiver`) 将消息定向到目标用户的连接会话。 #### 4. 前端实现 前端部分可利用 JavaScript 提供的 WebSocket API 发起连接并与服务器通信。以下是简单的 HTML 页面示例: ```html <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>WebSocket Single Chat</title> </head> <body> <div id="messages"></div> <input type="text" id="messageInput"/> <button onclick="sendMessage()">Send Message</button> <script> const socket = new WebSocket('ws://localhost:8080/chat'); function sendMessage() { const inputElement = document.getElementById('messageInput'); const messageContent = inputElement.value; socket.send(JSON.stringify({ sender: 'Alice', receiver: 'Bob', content: messageContent, })); inputElement.value = ''; } socket.onmessage = function(event) { const messagesDiv = document.getElementById('messages'); const receivedMessage = event.data; messagesDiv.innerHTML += `<p>${receivedMessage}</p>`; }; </script> </body> </html> ``` 上述页面展示了如何向指定用户发送消息以及显示收到的消息。 --- ###
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值