面试必问的CAS和ConcurrentHashMap,你搞懂了吗?

前言

各位码友们,作为一名后端开发,面试时是不是经常被问到并发编程的那些事儿?今天咱们就来聊聊那个让无数程序员又爱又恨的ConcurrentHashMap ,以及它的好基友CAS。准备好瓜子饮料,咱们开车啦!

一、什么是CAS?比相亲还谨慎的原子操作

1.1 CAS的自我介绍

CAS(Compare And Swap),翻译过来就是"比较并交换",是一种无锁编程的原子操作。它的行为模式像极了相亲:

java 复制代码
// 伪代码版相亲过程
public boolean 相亲(我, 心仪对象, 新对象) {
    if (当前状态 == 心仪对象) {
        当前状态 = 新对象;  // 牵手成功!
        return true;
    } else {
        return false;     // 抱歉,你来晚了
    }
}

在Java中,CAS是通过sun.misc.Unsafe类实现的,但咱们日常开发中更多使用的是它的包装类:

java 复制代码
// Java中的CAS实战
AtomicInteger atomicInt = new AtomicInteger(0);

// 如果当前值是0,就把它改成1
boolean success = atomicInt.compareAndSet(0, 1);
System.out.println("操作结果:" + success); // 输出:true

1.2 CAS的底层原理

CAS的底层其实是CPU的原子指令(cmpxchg),这个过程是不可中断的,就像:

"我看看现在是不是A,如果是的话我就换成B,这期间谁都不能打扰我!"

二、ABA问题:你的前女友改头换面回来了

2.1 什么是ABA问题?

想象一下这个悲伤的故事:

  1. 你看到女朋友在家(值是A)
  2. 你去买奶茶的功夫,她出门做了个发型又回来了(A→B→A)
  3. 你以为她一直在家,其实...
java 复制代码
// ABA问题示例
public class ABADemo {
    private static AtomicReference<String> gf = new AtomicReference<>("小美");
    
    public static void main(String[] args) {
        // 线程1:检查女朋友是否还是小美
        new Thread(() -> {
            String expect = "小美";
            // 中间可能发生很多事情...
            boolean success = gf.compareAndSet(expect, "新女友");
            System.out.println("线程1操作结果:" + success); // 可能成功,但...
        }).start();
        
        // 线程2:搞事情的第三者
        new Thread(() -> {
            gf.set("小丽");  // 暂时换人
            gf.set("小美");  // 又换回来了
        }).start();
    }
}

2.2 如何解决ABA问题?

Java提供了AtomicStampedReference,给值加上"版本号":

java 复制代码
// 使用版本号解决ABA问题
AtomicStampedReference<String> stampedGf = 
    new AtomicStampedReference<>("小美", 0);

// 要修改时,必须值和版本号都匹配
boolean success = stampedGf.compareAndSet(
    "小美", "新女友", 
    0,  // 期望的版本号
    1   // 新版本号
);

这样,即使值相同,版本号不同也会操作失败!

三、ConcurrentHashMap的进化史

3.1 JDK 1.7时代的"分段锁"策略

JDK 1.7的ConcurrentHashMap像个大型超市,把商品分区管理:

java 复制代码
// JDK 1.7的结构示意图
ConcurrentHashMap {
    Segment[] segments;  // 16个区域(默认)
    
    static class Segment {
        HashEntry[] table; // 每个区域有自己的哈希表
        ReentrantLock lock; // 每个区域有独立的锁
    }
}

工作原理:

  • 把数据分成16个Segment(可以理解为16个货架区)
  • 每个Segment有自己的锁
  • 操作不同Segment时不会互相阻塞

优点: 写操作只需要锁住对应的Segment,提高了并发度

缺点: 查询时需要遍历整个Segment,效率有待提升

3.2 JDK 8的华丽转身

JDK 8的ConcurrentHashMap进行了彻底的重构,变得更加优雅:

3.2.1 数据结构变革

java 复制代码
// JDK 8的ConcurrentHashMap
ConcurrentHashMap {
    Node[] table;        // 类似HashMap的数组+链表/红黑树
    volatile int sizeCtl; // 控制标识符
}

3.2.2 核心改进亮点

1. 抛弃分段锁,使用CAS+synchronized

java 复制代码
// JDK 8的putVal方法核心逻辑
final V putVal(K key, V value, boolean onlyIfAbsent) {
    // ...
    for (Node<K,V>[] tab = table;;) {
        Node<K,V> f; int n, i, fh;
        if (tab == null || (n = tab.length) == 0)
            tab = initTable();  // 初始化表,使用CAS
        else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
            // 如果位置为空,使用CAS添加新节点
            if (casTabAt(tab, i, null, new Node<K,V>(hash, key, value)))
                break;
        }
        else if ((fh = f.hash) == MOVED)
            tab = helpTransfer(tab, f);  // 协助扩容
        else {
            synchronized (f) {  // 对头节点加锁
                // 链表或树的操作
            }
        }
    }
    // ...
}

2. 引入红黑树解决哈希冲突

当链表长度超过8时,转换为红黑树:

less 复制代码
链表: A → B → C → D → E → F → G → H
超过8个节点时:
红黑树:
    D
   / \
  B   F
 / \ / \
A C E G H

3. 扩容机制的优化

  • 多线程协同扩容:线程在put时如果发现正在扩容,会协助转移数据
  • 更精细的锁粒度:只锁住当前操作的桶(链表头节点)

3.2.3 JDK 8 ConcurrentHashMap的亮点总结

特性 说明 优势
CAS操作 初始化、空桶插入使用CAS 无锁化,提高性能
synchronized 对头节点加锁 锁粒度更细
红黑树 链表过长时转换 查询效率从O(n)提升到O(log n)
协助扩容 多线程共同完成数据迁移 避免单线程扩容的瓶颈

四、实战技巧与面试宝典

4.1 使用示例

java 复制代码
// ConcurrentHashMap的基本使用
ConcurrentHashMap<String, Integer> userScores = new ConcurrentHashMap<>();

// 线程安全的put
userScores.put("张三", 100);

// 使用computeIfAbsent原子操作
userScores.computeIfAbsent("李四", k -> {
    // 复杂的初始化逻辑
    return calculateInitialScore(k);
});

// 统计所有分数
int total = userScores.reduceValues(2, Integer::sum);

4.2 面试常见问题

  1. ConcurrentHashMap在JDK7和JDK8的区别?

    • 数据结构:分段锁 vs 数组+链表/红黑树
    • 锁机制:ReentrantLock vs synchronized+CAS
    • 并发度:16个Segment vs 桶级别的锁
  2. ConcurrentHashMap的size方法如何实现?

    • JDK7:尝试2次不锁统计,如果变化就加锁统计
    • JDK8:使用baseCount和CounterCell[]来分片计数
  3. 为什么JDK8用synchronized代替ReentrantLock?

    • synchronized在JDK6后做了大量优化,性能接近ReentrantLock
    • 减少内存开销,简化实现

总结

从JDK7到JDK8,ConcurrentHashMap完成了一次华丽的转身:

  • JDK7像是分工明确的工厂,每个车间有自己的门锁
  • JDK8则升级成了现代化智能仓库,用更精细的管控和智能算法提升效率

CAS作为无锁编程的基石,虽然要小心ABA这样的"陷阱",但确实是构建高性能并发容器的利器。

希望这篇博客能让你对CAS和ConcurrentHashMap有更深入的理解。下次面试再被问到这些问题,你就可以自信地说:"这得从CAS的原理说起..."

记住:好的并发设计,就像好的团队协作,既要各司其职,也要默契配合!


PS: 如果在生产环境中使用ConcurrentHashMap,记得结合具体业务场景选择合适的并发级别和初始容量哦!

相关推荐
SirLancelot13 小时前
MinIO-基本介绍(一)基本概念、特点、适用场景
后端·云原生·中间件·容器·aws·对象存储·minio
golang学习记3 小时前
Go 1.25 新特性:正式支持 Git 仓库子目录作为 Go 模块
后端
Penge6663 小时前
一文读懂 ucrypto.Md5
后端
sophie旭5 小时前
一道面试题,开始性能优化之旅(3)-- DNS查询+TCP(三)
前端·面试·性能优化
代码充电宝5 小时前
LeetCode 算法题【简单】49. 字母异位词分组
java·算法·leetcode·面试·哈希算法
Terio_my6 小时前
Spring Boot 缓存集成实践
spring boot·后端·缓存
karry_k6 小时前
JMM与Volatitle
后端
数字化顾问6 小时前
“AMQP协议深度解析:消息队列背后的通信魔法”之核心概念与SpringBoot落地实战
开发语言·后端·ruby
程序员的奶茶馆7 小时前
Python 字典速查:键值对操作与高频函数
python·面试