了解ConcurrentHashMap

本文介绍了ConcurrentHashMap,一个线程安全的HashMap实现。从Java 1.8之前的分段锁机制到1.8之后的改进,包括读操作的无锁实现、写操作的CAS配合synchronized优化,以及扩容的多线程处理。ConcurrentHashMap通过volatile和CAS确保并发安全性,并在特定条件下将链表转换为红黑树,提高查找效率。

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

介绍

  ConcurrentHashMap是在Java.util.concurrent并发包下的一个类,它相当于是一个线程安全的HashMap。这里可能还有人会提到HashTable,这也相当于是一个线程安全的HashMap,但是它因为效率太低,所以现在很少被使用了,HashTable是对整个对象上了一把锁,在synchronized被优化之前,他的效率可以说是相当的低,所以就有了ConcurrentHashMap的诞生。

Java1.8之前的ConcurrentHashMap

在JDK1.8之前ConcurrentHashMap是采用一种叫分段锁(Segment)的机制,默认情况下一般会把整个HashMap分为16段,所以在多线程下效率默认会提升16倍。使用put插入元素时并不是对这个对象上锁,而是对每一个段上锁,首先计算HashCode,确定这是属于哪一个段,然后对这个段上锁,这样就可以同时保证有多个线程在操作HashMap,效率会提高许多。
在这里插入图片描述
在这个版本的ConcurrentHashMap中加入了一个Segment数组,一个Segment存放一个HashEntry数组,里面储存的是一个链表的头结点,也就是相当于把原来的哈希表分段又重新那一个数组储存起来了。

Java1.8之后的ConcurrentHashMap

JDK1.8之后做出了很多大的改动,加入了许多新的机制,对ConcurrentHashMap来说,直接摒弃了上个版本的锁分段技术,通过引入大量的volatile关键字和CAS操作来解决线程安全问题,效率有了很大的提升,同时由于这个版本的synchronized经过了大更新,所以效率就又一步提升了。

读操作的改动

我们知道读操作是不涉及线程安全问题的,不管多少个线程并发读取数据都没有问题,所以这里就干脆直接不上锁了,而是在Node的val和next属性前加上了volatile字段,这虽然能保证一定能读到主内存的数据,但是却不能保证能读到最新的数据,但是这样的情况也无伤大雅。

public V get(Object key) {
        Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
        //计算哈希值
        int h = spread(key.hashCode());
        //如果数组不为空或者长度不等于0且第一个节点不等于空时直接返回第一个节点的val
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (e = tabAt(tab, (n - 1) & h)) != null) {
            if ((eh = e.hash) == h) {
                if ((ek = e.key) == key || (ek != null && key.equals(ek)))
                    return e.val;
            }
            else if (eh < 0)
                return (p = e.find(h, key)) != null ? p.val : null;
            while ((e = e.next) != null) {
                if (e.hash == h &&
                    ((ek = e.key) == key || (ek != null && key.equals(ek))))
                    return e.val;
            }
        }
        return null;
    }

从上述源码中能看到,在读数据时,首先会计算 key 的 hash 值,如果 Node 数组里匹配到首结点即返回,否则如果eh小于0说明节点在红黑树上,直接寻找,否则就遍历链表,由于可能会有冲突,因此需调用 equals 方法。

写操作的改动

写操作如果不上锁,多线程情况下发生线程安全问题的概率就很大,但是如果像HashTable那样或者是前面每次调用put方法都会上锁的话,即便是synchronized经过了改动,这仍然是一个很大的开销,所以在这个版本中,在写操作中加入了CAS操作去配合synchronized一起使用,效率就高很多了。

public V put(K key, V value) {
        return putVal(key, value, false);
    }

    /** Implementation for put and putIfAbsent */
    final V putVal(K key, V value, boolean onlyIfAbsent) {
        if (key == null || value == null) throw new NullPointerException();
        int hash = spread(key.hashCode());
        int binCount = 0;
        for (Node<K,V>[] tab = table;;) {
            Node<K,V> f; int n, i, fh;
            if (tab == null || (n = tab.length) == 0)
                tab = initTable();
            else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
                if (casTabAt(tab, i, null,
                             new Node<K,V>(hash, key, value, null)))
                    break;                   // no lock when adding to empty bin
            }
            else if ((fh = f.hash) == MOVED)
                tab = helpTransfer(tab, f);
            else {
                V oldVal = null;
                synchronized (f) {
                    if (tabAt(tab, i) == f) {
                        if (fh >= 0) {
                            binCount = 1;
                            for (Node<K,V> e = f;; ++binCount) {
                                K ek;
                                if (e.hash == hash &&
                                    ((ek = e.key) == key ||
                                     (ek != null && key.equals(ek)))) {
                                    oldVal = e.val;
                                    if (!onlyIfAbsent)
                                        e.val = value;
                                    break;
                                }
                                Node<K,V> pred = e;
                                if ((e = e.next) == null) {
                                    pred.next = new Node<K,V>(hash, key,
                                                              value, null);
                                    break;
                                }
                            }
                        }
                        else if (f instanceof TreeBin) {
                            Node<K,V> p;
                            binCount = 2;
                            if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
                                                           value)) != null) {
                                oldVal = p.val;
                                if (!onlyIfAbsent)
                                    p.val = value;
                            }
                        }
                    }
                }
                if (binCount != 0) {
                    if (binCount >= TREEIFY_THRESHOLD)
                        treeifyBin(tab, i);
                    if (oldVal != null)
                        return oldVal;
                    break;
                }
            }
        }
        addCount(1L, binCount);
        return null;
    }
  • 我们知道在HashMap和HashTable中key和value值都可以为null,但是ConcurrentHashMap不允许key和value为null。
  • 先通过Key的值计算出他的hash值,然后进入到一个死循环中,如果初始数组长度为0,则先初始化数组,如果数组不为空,就计算出该节点在数组中的位置,如果这个位置为空的话,直接使用CAS操作插入,这里是不需要上锁的。
  • 如果这个位置存在结点的话,说明发生了hash碰撞,这时会对这个结点上锁,进入锁之后首先会判断这是一棵树还是一个链表,如果是树的话就直接调用树的插入方法,如果是一个链表的话就向后遍历,直到找到hash值和key值都相同的结点,然后更新val就可以了,或者是插入到链表的最后。
  • 最后还需要判断,如果加入这个结点之后,链表长度超过了8,就会把链表转成树结构。

扩容

在前面版本中,如果数组需要扩容的话,是由一个线程来完成的,扩容就会涉及元素搬移,当元素数量很大的时候,这个操作就会很慢,这时如果是前面版本的ConcurrentHashMap的话其他进程就会全部堵塞,等待扩容完成,这样效率就会很低,所以在这个版本扩容是由多个线程来完成的,每个线程只负责一部分元素的搬移,那么不同线程怎么直到什么时候要扩容了呢?在这个版本中加入了sizeCtl这个属性。可以说它是ConcurrentHashMap中出镜率很高的一个属性,因为它是一个控制标识符,在不同的地方有不同用途,而且它的取值不同,也代表不同的含义。

  • 负数代表正在进行初始化或扩容操作
  • -1代表正在初始化
  • -N 表示有N-1个线程正在进行扩容操作
  • 正数或0代表hash表还没有被初始化,这个数值表示初始化或下一次进行扩容的大小

这个版本ConcurrentHashMap的扩容大致可以分为两个部分:

  1. 第一个部分是单线程完成的,首先先以两倍扩容的方式创建一个newTable数组
  2. 第二个部分就是元素的搬移,这是由多个线程共同完成的

扩容操作是一个很复杂的过程,这里面多个线程参与过,如果一个线程扩容了那一部分之后,然会把头结点的值set为forward,另一个线程看到这个值后就会向后遍历,这样就会交替完成工作,如果一个put方法进来,他看到一个为forward的结点,那么它也会立刻参与到扩容的操作任务中。

<think>好的,我现在需要帮用户优化他们的技术简历,目标是在北京拿到25k-30k的月薪。用户提到的技术包括Java、JVM、Spring生态、MySQL、Redis、Linux、Nginx、RabbitMQ、Docker等。首先,我要考虑这个薪资水平对应的岗位要求,通常需要深入的技术能力和项目经验,能够展示出解决复杂问题和系统优化的能力。 用户提供的现有技术栈已经很全面,但可能需要更突出亮点和量化成果。比如,在Java和JVM部分,不仅要列出熟悉JVM内存模型,还要具体说明如何调优,比如通过调整垃圾回收器参数将GC时间减少多少。这样能体现实际应用能力。 Spring生态方面,可能需要强调微服务架构的设计经验,比如使用Spring Cloud Alibaba搭建的系统,处理了多少QPS,或者如何解决服务雪崩问题。这些具体的数字和案例会让简历更有说服力。 数据库部分,MySQL和Redis的优化经验是关键。比如索引优化、分库分表的具体案例,或者用Redis实现分布式锁、缓存穿透的解决方案。这些都需要具体说明带来的性能提升,比如响应时间降低多少,TPS提高多少。 中间件如RabbitMQ,应该提到如何设计消息可靠投递,如何解决消息堆积问题,可能结合Docker和Kubernetes进行容器化部署,这样展示出全栈的能力。 运维工具如Docker和Linux,需要展示自动化部署、监控的经验,例如通过编写Shell脚本优化部署流程,或者利用Prometheus和Grafana进行系统监控,这些都能体现运维能力。 项目经验部分,应该采用STAR法则,明确项目背景、个人职责、技术方案和成果。比如主导某个高并发系统的设计,使用Redis集群支撑每秒10万请求,这样的描述既有技术深度又有量化结果。 另外,参考用户提供的引用内容,他们可能已经使用过Docker部署RabbitMQ、Nacos等,这些经验可以整合到简历中,突出容器化部署和云原生技术的应用,比如结合Kubernetes进行微服务管理,提升系统的可扩展性和稳定性。 最后,要注意简历的结构和关键词匹配,确保通过HR的筛选。技术能力部分分点列出核心技能,项目经验按时间倒序排列,最近的项目放在前面。同时,使用行业标准术语,避免模糊描述,比如“精通”改为“深入掌握”更合适,避免给人夸大其词的感觉。 可能还需要考虑高薪岗位对架构设计、性能优化、高并发处理经验的要求,因此简历中要突出这些方面的项目经验和技术能力。比如设计过千万级用户量的系统,优化JVM参数提升吞吐量,或者通过分布式缓存降低数据库负载等。 总结下来,优化点包括:量化成果、突出技术深度、项目经验具体化、使用行业术语、结构清晰,以及结合容器化和云原生技术展示全栈能力。同时,确保简历中的技术关键词与招聘要求匹配,提高通过率。</think>### 技术简历优化指南(对标北京25k-30k岗位) #### 一、核心技术能力结构化呈现 1. **Java生态** - 深入掌握JVM内存模型、GC算法调优,具备$>5$万QPS系统的FullGC问题定位经验(如通过G1参数优化降低GC时间$>70\%$) - 熟悉Spring Cloud Alibaba微服务架构,主导设计过基于$Nacos+Sentinel+Seata$的分布式系统(注册中心配置管理TPS达$2000+/s$) 2. **数据库与中间件** - MySQL索引优化及分库分表实践:完成$10$亿级数据量的水平拆分,查询性能提升$>300\%$[^3] - Redis高可用架构设计:实现$99.99\%$可用性的Cluster集群,通过$Lua$脚本优化热点Key访问性能 3. **云原生与运维** - 精通Docker容器化部署,完成SpringBoot项目镜像构建效率提升$50\%$(多阶段构建+镜像瘦身)[^2] - 熟悉Kubernetes服务编排,设计过基于$Prometheus+Grafana$的监控体系 ```markdown # 技术亮点示例 - 架构设计:主导电商系统重构,采用DDD+微服务架构,支撑日订单量从5万到100万增长 - 性能优化:通过JVM调优(ZGC启用+堆外内存控制)降低高并发场景延迟40% - 故障处理:快速定位Redis集群脑裂问题,设计哨兵+keepalived双保险方案 ``` #### 二、项目经验强化策略 采用`STAR`法则改造项目描述: ```markdown **供应链金融平台(2022.03-2023.06)** - Situation:原有系统处理10万级订单时出现数据库连接池耗尽 - Task:主导架构升级与性能优化 - Action: * 采用ShardingSphere实现分库分表(16分片) * 引入RabbitMQ削峰填谷(日均处理50万消息) * 设计Redis+Lua分布式锁控制库存 - Result:系统吞吐量从500TPS提升至3500TPS,TP99降低至200ms ``` #### 三、高价值加分项 1. **技术深度证明** - 在Spring源码层面解决过循环依赖问题(Bean生命周期控制) - 设计过定制化ClassLoader实现热部署 - 对Redis6.0多线程模型有源码级研究 2. **架构设计能力** - 绘制系统架构图时标注关键指标: $$ \begin{cases} \text{单节点QPS} \geq 5000 \\ \text{服务可用性} \geq 99.95\% \\ \text{数据一致性误差} < 0.1\% \end{cases} $$ 3. **运维保障体系** - ELK日志分析平台建设 - 基于Jenkins的CI/CD流水线设计 - 混沌工程实践(模拟网络分区、节点故障) #### 四、简历框架建议 ```markdown ## 技术优势 - 7年Java架构经验,3个千万级用户项目主导经历 - 专利:分布式事务一致性解决方案(专利号CNXXXXXX) ## 项目经历(倒序排列) [采用上述STAR格式] ## 开源贡献 - 贡献ShardingSphere中文文档优化 - 自研JVM监控工具Star 200+ ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值