Java面试必备:HashMap、ConcurrentHashMap与Redis核心解析
1. 从HashMap到Redis的Java面试通关指南最近帮团队面试了几位Java工程师发现很多候选人在HashMap、ConcurrentHashMap和Redis这些基础知识点上频频翻车。这让我想起自己当年面试时闹过的笑话——把HashMap说成是线程安全的结果被面试官当场打脸。今天我就把这些年积累的Java面试干货整理出来特别是那些容易踩坑的知识点。2. HashMap底层原理深度解析2.1 数据结构与哈希算法HashMap的底层实现是数组链表红黑树JDK8。当我们执行put操作时首先会通过hash()方法计算key的哈希值static final int hash(Object key) { int h; return (key null) ? 0 : (h key.hashCode()) ^ (h 16); }这个算法通过将高16位与低16位进行异或运算既保证了哈希值的随机性又减少了哈希冲突的概率。但这里有个常见误区很多人以为hashCode()直接决定了元素在数组中的位置实际上还要经过取模运算index (n - 1) hash其中n是数组长度这个设计巧妙地用位运算替代了耗时的取模运算。2.2 扩容机制与线程安全问题HashMap的默认负载因子是0.75当元素数量超过容量×负载因子时就会触发扩容。扩容时会将数组大小翻倍并重新计算所有元素的位置。这个过程中最经典的线程安全问题就是死循环——当多线程同时触发扩容时链表可能形成环状结构。重要提示在JDK8中虽然修复了死循环问题但HashMap仍然是线程不安全的因为可能丢失更新。实际项目中一定要用ConcurrentHashMap替代。3. ConcurrentHashMap的并发控制艺术3.1 JDK7与JDK8实现对比JDK7采用分段锁机制将数据分成多个Segment每个Segment独立加锁。这种设计虽然提高了并发度但在极端情况下仍然会出现竞争。JDK8做了革命性改进抛弃分段锁改用CASsynchronized链表长度超过8时转为红黑树引入ForwardingNode辅助扩容final V putVal(K key, V value, boolean onlyIfAbsent) { if (key null || value null) throw new NullPointerException(); int hash spread(key.hashCode()); // ...省略部分代码... synchronized (f) { // 链表或红黑树操作 } }3.2 高频面试问题解析size()方法的实现原理JDK7尝试两次不加锁统计如果结果不一致则加锁统计JDK8基于CounterCell的分段计数为什么不用HashtableHashtable是全表锁并发性能差迭代器不是快速失败的4. Redis在Java面试中的核心考点4.1 数据类型与应用场景数据类型底层实现典型应用场景Java操作示例StringSDS计数器、分布式锁jedis.set(key, value)Hash哈希表对象存储jedis.hset(user:1, name, Jack)List双向链表消息队列jedis.lpush(queue, task1)Set哈希表标签系统jedis.sadd(tags, java)ZSet跳表哈希排行榜jedis.zadd(rank, 100, user1)4.2 持久化机制对比RDB持久化定时生成内存快照恢复速度快可能丢失最后一次快照后的数据AOF持久化记录每个写操作支持每秒同步/每个命令同步文件体积大但数据更安全生产环境建议同时开启两种方式用AOF保证数据安全用RDB加快重启恢复速度。5. 线程池的七大参数详解5.1 参数含义与配置建议ThreadPoolExecutor( int corePoolSize, // 核心线程数建议CPU核心数1 int maximumPoolSize, // 最大线程数建议核心线程数×2 long keepAliveTime, // 空闲线程存活时间建议60s TimeUnit unit, // 时间单位 BlockingQueueRunnable workQueue, // 任务队列建议有界队列 ThreadFactory threadFactory, // 线程工厂 RejectedExecutionHandler handler // 拒绝策略 )5.2 四种拒绝策略对比AbortPolicy默认直接抛出RejectedExecutionExceptionCallerRunsPolicy由调用线程执行该任务DiscardPolicy直接丢弃任务DiscardOldestPolicy丢弃队列中最老的任务实战建议自定义拒绝策略时一定要记录日志并告警方便及时发现系统过载。6. CompletableFuture与自定义线程池6.1 为什么要用自定义线程池// 错误示范 - 使用默认的ForkJoinPool CompletableFuture.supplyAsync(() - queryFromDB()); // 正确做法 - 指定自定义线程池 ExecutorService customPool Executors.newFixedThreadPool(10); CompletableFuture.supplyAsync(() - queryFromDB(), customPool);默认的ForkJoinPool是全局共享的如果所有异步任务都使用它可能导致关键业务被非关键业务阻塞无法针对不同业务设置不同的线程数难以监控和管理6.2 最佳实践方案根据业务类型创建多个线程池给线程池设置有意义的名称前缀监控线程池的运行状态使用Spring的ThreadPoolTaskExecutor简化配置Bean(dbQueryPool) public Executor dbQueryExecutor() { ThreadPoolTaskExecutor executor new ThreadPoolTaskExecutor(); executor.setCorePoolSize(5); executor.setMaxPoolSize(10); executor.setQueueCapacity(100); executor.setThreadNamePrefix(db-query-); executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy()); executor.initialize(); return executor; }7. 面试避坑指南与实战技巧7.1 HashMap常见误区误认为hashCode()决定存储位置实际还有哈希扰动和取模运算忽略负载因子的作用0.75是空间和时间效率的平衡点混淆JDK7和JDK8的实现差异JDK8引入了红黑树优化线程安全认知错误即使JDK8没有死循环仍然不是线程安全的7.2 Redis高频陷阱缓存穿透使用布隆过滤器或缓存空值缓存雪崩设置不同的过期时间热key问题本地缓存多级缓存大key问题拆分或使用SCAN迭代7.3 线程池使用禁忌避免使用无界队列可能导致OOM合理设置线程数IO密集型可多CPU密集型要少不要忽略拒绝策略至少记录日志记得关闭线程池shutdown或shutdownNow记得有一次线上事故就是因为使用了默认的ForkJoinPool导致订单服务被报表生成任务拖垮。后来我们为每个业务场景都配置了独立的线程池并通过监控及时发现并处理了多个潜在问题。