JAVA语言程序开发集合框架底层、并发编程高级特性、IO 模型优化以及 JVM 初步调优
1. 🔍 集合框架:从“会用”到“看穿”底层
掌握集合框架不仅仅是会使用 ArrayList 和 HashMap,更要理解其数据结构和扩容机制。
1.1 HashMap 的树化与退化
· 核心结构: 数组 + 链表 + 红黑树。
· 树化条件: 当链表长度 > 8 且数组长度 ≥ 64 时,链表转换为红黑树(TreeNode),提升查询效率从 O(n) 到 O(\log n)。
· 退化条件: 在 resize() 或 remove() 时,若桶内节点数 < 6,红黑树会退化为链表。
· 为什么是 8? 源码注释指出,理想情况下随机哈希码分布符合泊松分布,链表长度达到 8 的概率极小(小于千万分之一)。这是空间和时间的一种权衡。
1.2 ConcurrentHashMap 的并发技巧
· JDK 1.7: 采用 Segment 分段锁,一次锁住一个子数组,默认 16 个并发级别。
· JDK 1.8+: 取消分段锁,改用 synchronized + CAS 操作:
· 写操作:仅锁住当前桶的头节点,细粒度锁提升并发。
· 读操作:使用 volatile 保证可见性,无需加锁。
· 计数:使用 LongAdder 思想的 CounterCell,分散计数热点。
2. 🧵 多线程与并发:深入 JVM 模型
理解 Java 内存模型是解决并发问题的关键。
2.1 Java 内存模型
· 主内存 vs 工作内存: 线程不能直接操作主内存,必须将变量拷贝到本地内存操作后再写回。
· 三大特性:
· 原子性: synchronized、Lock 或原子类 (AtomicInteger) 保证。
· 可见性: volatile 强制刷新到主内存,使其他缓存失效。
· 有序性: volatile 禁止指令重排序(通过内存屏障实现)。
2.2 synchronized 的锁升级之路
无锁 → 偏向锁 → 轻量级锁 → 重量级锁
· 偏向锁: 只有一个线程访问时,记录线程 ID,无需 CAS。
· 轻量级锁: 少量线程竞争,通过自旋(CAS)尝试获取锁,不挂起线程,避免内核态切换开销。
· 重量级锁: 长时间自旋失败,线程阻塞,由内核调度。
· 锁优化技巧: 减少锁粒度(如 ConcurrentHashMap)、锁粗化(合并频繁加锁)、锁消除(JVM 逃逸分析优化明显不会竞争的锁)。
3. 📂 I/O 模型演进:从 BIO 到 NIO
在高并发场景下,传统的 BIO(同步阻塞 I/O)会因为线程数暴增导致 CPU 频繁上下文切换,性能骤降。
3.1 对比分析
模型 线程模型 适用场景 痛点
BIO 一连接一线程 连接数少,固定架构 线程资源浪费严重,大量阻塞
NIO 一请求一线程 (Selector) 连接数多,连接时间短 (聊天室) 编程复杂,不易管理
AIO 有效 I/O 时回调 连接数多,连接时间长 (相册) 底层依赖 OS 支持,Linux 支持一般
3.2 操作系统层面的 I/O 模型
· 用户态与内核态: I/O 分为两个阶段:
1. 等待数据准备到内核缓冲区。
2. 将数据从内核拷贝到用户内存。
· NIO (Non-blocking I/O) 本质: Java 的 NIO 对应操作系统的多路复用 I/O(select/poll/epoll),通过一个线程监控多个通道的“准备就绪”状态。
4. ⚙️ JVM 内存结构与垃圾回收
4.1 运行时数据区
· 程序计数器: 当前线程执行的字节码行号。
· 虚拟机栈: 存储局部变量、操作数栈、方法出口,是线程私有的。
· 堆: 几乎所有对象实例的存储区域,GC 管理的主要区域。
· 方法区: 存储类信息、常量、静态变量(JDK1.8 中改为 元空间,使用本地内存,避免 OOM 问题)。
4.2 GC 分析
· 可达性分析: 从 GC Roots(栈引用、静态属性、常量引用等)出发,未被引用的对象将可能被回收。
· 引用级别(决定了回收时机):
1. 强引用:new Object(),永不回收(除非 OOM)。
2. 软引用:内存不足时回收(适合缓存)。
3. 弱引用:下次 GC 必回收(WeakHashMap)。
· 常用参数实践:
· -Xmx / -Xms:最大堆 / 初始堆(设置相等防止扩容抖动)。
· -Xss:设置每个线程的栈大小(递归过深会 StackOverflowError)。
5. 🛠️ 代码优化实战
5.1 痛点1:SimpleDateFormat 线程不安全
现象: 多线程下抛出 NumberFormatException 或时间混乱。
原因: Calendar 对象在并发执行 format() 和 parse() 时共享了状态。
解决:
```java
// 方案1:局部变量(推荐,线程封闭)
public String format(Date date) {
return new SimpleDateFormat("yyyy-MM-dd").format(date);
}
// 方案2:ThreadLocal 封装
private static final ThreadLocal<SimpleDateFormat> SDF =
ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd"));
```
5.2 痛点2:集合的快速失败机制
现象: 遍历 ArrayList 时尝试 remove() 抛出 ConcurrentModificationException。
原理: 迭代器内部维护 expectedModCount,若集合实际 modCount 发生变化,立即抛异常。
正确删除姿势:
```java
// 使用 Iterator 的 remove 方法
Iterator<String> it = list.iterator();
while(it.hasNext()) {
if("b".equals(it.next())) {
it.remove(); // 正确,会同步修改 modCount
}
}
// Java 8+ 推荐
list.removeIf(s -> s.equals("b"));
```
6. 📌 中等难度面试高频题
1. HashMap 在 JDK 1.7 和 1.8 中的区别是什么?
1.7 头插法 + 数组+链表,1.8 尾插法 + 数组+链表+红黑树。
区别在于扩容时的 rehash 方式。1.7 头插法在多线程扩容时会导致死循环(环形链表),1.8 改用尾插法避免了此问题。
2. volatile 能保证原子性吗?举例说明。
不能保证原子性。例如 volatile int count = 0; count++,这行代码分解为“读-改-写”三步,volatile 只保证了读和写的可见性,但在修改过程中,其他线程可能已经修改了 count,导致值变小。需使用 AtomicInteger。
3. CPU 飙升 100%,如何排查?
1. top 找到高 CPU 的 Java 进程 PID。
2. top -Hp PID 找到进程内高 CPU 的线程 ID (TID)。
3. 将 TID 转为十六进制 (printf '%x\n' TID)。
4. jstack PID | grep -A 20 十六进制TID 查看线程堆栈,定位到具体代码行。
更多推荐
所有评论(0)