一、基础概念:并发 & 并行 & 线程调度

1. 并发与并行区别

  • 并发:多线程争抢资源,单核 CPU 通过时间片切换执行,同一时刻只有一个线程运行;
  • 并行:多核 CPU,多线程不抢占资源,同时执行。

2. 线程完整生命周期

text

新建 → 就绪 → 运行 → 阻塞 → 完毕
  1. 新建:线程对象初始化;
  2. 就绪:调用 start() 后,进入操作系统就绪队列,等待 CPU 调度
    • 就绪队列分两种:
      • 公平队列:先进先出,性能损耗大;
      • 非公平队列:短任务优先、人机交互频繁线程优先级更高;
    • 核心结论:任何线程创建后不会立刻执行,必须进入就绪态等待 CPU 选中
  3. 运行:CPU 分配时间片,线程开始执行;
  4. 阻塞:竞争资源失败、休眠、等待时进入阻塞队列,不再消耗 CPU
  5. 完毕:线程执行结束。

3. 上下文切换

操作系统随机调度线程,切换时会产生上下文切换: CPU 执行完线程后,保存当前执行信息,下次继续执行。

  • 时间损耗:毫秒级,单次切换约 10ms至12ms
  • 适用场景:
    • IO 密集型(网络、磁盘读写):适合多线程;
    • CPU 密集型:单核下单线程最快;
  • 优化方向:减少上下文切换,采用无锁并发、CAS 算法、使用最少线程

4. 线程执行特性

  • 单线程:代码从上往下顺序执行;
  • 多线程:线程执行顺序不确定,由操作系统调度决定;
  • join():调用线程执行完毕后,后续代码才会执行,控制线程执行顺序。

二、Java 锁核心机制

1. synchronized 加锁规则

  1. 只能对引用类型加锁,基本数据类型无法加锁;
  2. 锁释放时机:只有synchronized代码块 / 方法执行完毕,才会释放锁;
  3. 核心特性:线程让出 CPU,锁不会释放

2. sleep () 和 wait () 核心区别

表格

方法 是否让出 CPU 是否释放锁 归属类
sleep() 让出 CPU ❌ 不释放锁 Thread
wait() 让出 CPU ✅ 释放锁 Object
  • sleep(2000):线程休眠 2s,进入阻塞队列,休眠期间持有锁,其他线程无法访问;
  • 死锁成因:两个线程互相持有对方需要的锁,同时进入阻塞队列,无限等待。

3. volatile 轻量级锁

  • 作用:只保证读安全、保证可见性,线程能读取最新变量值;
  • 缺陷:不保证原子性,写操作会相互覆盖,计算结果不准确;
  • 对比:synchronized是重量级锁,读写都加锁,保证原子性 + 可见性,安全性更高。

三、硬件底层原理:并发问题的根源

1. 总线竞争问题

计算机总线同一时刻只能传输一个指令,单核时代内存同一时刻只能被一个指令操作。 多核 CPU 优化:增加高速缓存,每个核心独立缓存,提升利用率,但带来缓存一致性的并发问题。

2. 高速缓存 & 内存屏障

  1. 缓存行:大小为 64 字节一个缓存行不能同时读写
  2. 内存屏障:标记内存区域,控制线程读写权限,解决缓存数据不一致;
  3. 缓存读取规则:CPU 先查高速缓存,无数据再查内存
    • 写命中:数据写入高速缓存;
    • 写缺失:数据直接写入内存。

3. 原子操作

一系列操作要么全部成功,要么全部失败,中间出错则全部回滚,是并发安全的底层基础。

四、重点补充:死锁

1. 死锁定义

多个线程互相持有对方需要的锁,且都不主动释放锁,导致所有线程陷入无限等待,无法继续执行的状态,就是死锁。

核心特点:循环等待、持有并等待、不可剥夺、互斥(四大必要条件)。

2. 死锁四大必要条件

只要破坏其中任意一个条件,就能避免死锁:

  1. 互斥条件:锁资源只能被一个线程持有(无法破坏,锁的核心特性);

  2. 持有并等待条件:线程持有一个锁后,又去等待另一个锁(可破坏:一次性获取所有需要的锁);

  3. 不可剥夺条件:线程持有锁后,不能被其他线程强制剥夺(可破坏:超时自动释放锁);

  4. 循环等待条件:多个线程形成“你等我锁,我等你锁”的循环(可破坏:按固定顺序获取锁)。

3. 线上死锁排查

死锁排查核心还是依赖 jstack 工具,步骤如下(和线程排查联动):

  1. 第一步:查看Java进程号(找到目标进程) ps -ef | grep java

  2. 第二步:导出线程dump文件 sudo -u admin jstack 进程号 > deadlock_dump

  3. 第三步:查看dump文件,筛选死锁信息grep -A 20 "deadlock" deadlock_dump

核心结论:jstack会自动识别死锁,在dump文件中明确标注“Found one Java-level deadlock”,并列出死锁线程、持有锁和等待锁的信息,直接定位问题。

4. 死锁避免方案

  • 方案1:按固定顺序获取锁(破坏循环等待);

  • 方案2:使用 tryLock(long timeout, TimeUnit unit)(超时自动释放锁,破坏不可剥夺);

  • 方案3:一次性获取所有需要的锁(破坏持有并等待);

  • 方案4:减少锁的持有时间,避免锁嵌套(降低死锁概率)。

五、线上实战:Linux 线程排查命令

线上排查死锁、线程阻塞、线程状态,核心使用 jstack 工具,抓取线程快照:

1. 导出线程 dump 文件

bash

运行

sudo -u admin jstack 进程号 > dump17

dump:抓取某一瞬间线程内存快照,用于分析线程状态。

2. 筛选线程状态

bash

运行

grep java.lang.Thread.State dump17 | awk '{print $2$3$4$5}'
  • grep:筛选关键字;
  • awk:统计输出信息。

3. 系统监控工具

  • vmstat:系统资源监控;
  • Lmbench3:性能基准测试。

六、总结

Java 并发编程,上层是线程、锁、关键字的使用,底层是 CPU 缓存、操作系统调度、硬件总线的限制

  1. 搞懂线程生命周期、上下文切换,理解多线程调度逻辑;
  2. 分清sleep/wait/volatile/synchronized区别,避开锁使用误区;
  3. 理解 CPU 缓存、内存屏障,明白并发安全问题的硬件根源;
  4. 掌握jstack排查命令,具备线上多线程问题定位能力。
Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐