一、操作系统底层前置知识

1. 进程 vs 线程

进程

程序运行实例,操作系统资源分配最小单位(内存、文件、网络),进程之间完全隔离。进程间通信(IPC)成本高:管道、信号、消息队列、共享内存、信号量、Socket。

线程

进程内指令流,CPU 调度最小单位,又叫轻量级进程 LWP;同进程内所有线程共享进程堆、全局变量,仅私有栈、寄存器;线程切换开销远小于进程。

核心区别
  1. 资源:进程独立内存,线程共享进程资源

  2. 切换成本:进程切换极高,线程切换轻量

  3. 通信:进程 IPC 复杂;线程直接读写共享变量

  4. 调度:操作系统调度线程

2. 上下文切换 Context Switch

定义

CPU 暂停当前线程,保存寄存器 / 程序计数器,加载下一线程上下文,恢复运行。只能发生内核态,是系统高开销操作,频繁切换会严重降低性能。

上下文切换示意图

两种切换
  1. 主动切换 cswch:线程主动放弃 CPU(sleep、wait、park、yield)

  2. 被动切换 nvcswch:时间片耗尽、更高优先级线程抢占

Linux 查看命令
# 全局每秒切换统计
vmstat 1
# 单个进程上下文切换
pidstat -w -p 进程PID 1
# 进程总切换次数
cat /proc/PID/status

3. OS 五态线程模型

操作系统原生线程 5 种状态:初始→就绪→运行→休眠→终止

操作系统线程五态

  • 初始:创建完毕,未申请内核线程

  • 就绪:内核线程创建完成,等待 CPU 时间片

  • 运行:分配 CPU 执行指令

  • 休眠:阻塞 / 等待事件,释放 CPU

  • 终止:执行完毕,资源回收

4. Linux 线程实现:NPTL

Java 线程基于 Linux NPTL(原生 POSIX 线程),一对一模型:1 个 Java 线程映射 1 个 OS 轻量级进程。

查看系统线程库:getconf GNU_LIBPTHREAD_VERSION

5. 协程 Coroutine(对比 Java 线程)

协程与线程对比

  1. 调度:线程由 OS 内核调度;协程用户态自主调度,无内核上下文切换

  2. 内存:线程栈默认 1M;协程栈 KB 级,可大量创建

  3. 锁:单协程无并发竞争,无需锁;多线程共享变量必须同步

  4. 适用:协程适合 IO 密集;线程适合 CPU 密集

  5. Java 原生无协程,第三方框架:Quasar、Kilim

二、Java 线程基础核心

1. Java 创建线程 4 种写法(本质只有 1 种)

底层全部依赖new Thread().start()创建内核线程,Runnable/Callable 只是任务载体。

方式 1:继承 Thread 类
Thread t = new Thread(){
    @Override
    public void run() {
        System.out.println("子线程执行");
    }
};
t.start();

方式 2:实现 Runnable(解耦线程与任务,推荐)
Runnable task = () -> System.out.println("任务执行");
Thread t = new Thread(task);
t.start();

方式 3:Callable+Future(带返回值,配合线程池)
class MyTask implements Callable<Integer> {
    @Override
    public Integer call() {
        return 100;
    }
}
ExecutorService pool = Executors.newSingleThreadExecutor();
Future<Integer> future = pool.submit(new MyTask());
Integer res = future.get();

方式 4:Lambda 简写
new Thread(() -> System.out.println("lambda线程")).start();

关键面试题:为什么不能直接调用 run ()?

run()只是普通方法,不会创建 OS 线程,仅当前主线程串行执行;

start()调用 native 方法,向操作系统申请内核线程,就绪后由 CPU 调度执行 run。

Java一对一线程模型

2. Java 线程调度机制:抢占式

  1. 协同调度:线程主动让出 CPU,缺陷:单个死循环线程卡死整个程序

  2. 抢占调度(Java 采用):OS 分配时间片,时间片耗尽强制切换;高优先级线程可抢占

  3. 优先级:1~10,默认 5;优先级仅给 OS 参考,不保证绝对执行顺序,受底层系统限制。

3. Java 线程 6 种状态(jstack 标准)

Java线程状态流转图

状态 含义 触发场景
NEW 新建 new Thread (),未调用 start ()
RUNNABLE 就绪 + 运行合并 start () 后;OS 就绪 / 运行都归为此状态
BLOCKED 阻塞 竞争 synchronized 锁失败,等待 monitor
WAITING 无限等待 wait()、join()、LockSupport.park()
TIMED_WAITING 限时等待 sleep(n)、wait(n)、parkNanos
TERMINATED 终止 run 执行完毕 / 异常退出

区分:OS 休眠状态 = Java BLOCKED+WAITING+TIMED_WAITING,这三类都无 CPU 时间片。

三、Thread 核心 API 详解

1. sleep(long ms)

  1. 进入 TIMED_WAITING,不释放对象锁

  2. 可被 interrupt 打断,抛出InterruptedException,同时清空中断标记

  3. sleep (0) 等价 yield ()

2. yield()

  1. 让出 CPU,回到 RUNNABLE 就绪状态,不释放锁

  2. 仅提示调度器,不保证一定切换线程

3. join ():线程等待合并

主线程阻塞,等待目标线程执行完毕再继续;底层基于 wait/notify 实现。

Thread t = new Thread(() -> Thread.sleep(5000));
t.start();
t.join(); // 主线程阻塞5秒
System.out.println("子线程执行完");

4. stop () 废弃方法(禁止使用)

暴力终止线程,强行释放锁,导致共享数据半写、数据不一致,并发安全灾难。

四、线程中断机制(优雅终止线程核心)

核心思想:协作式中断

没有强制杀死线程 API,仅设置中断标记,由线程自身判断标记、自主退出。

三个核心方法
  1. t.interrupt():设置线程中断标记 = true,不会停止线程

  2. t.isInterrupted():获取中断标记,不清空标记

  3. Thread.interrupted():静态方法,返回标记并重置为 false

标准优雅停止模板
public class StopTask implements Runnable {
    @Override
    public void run() {
        // 循环判断中断标记
        while (!Thread.currentThread().isInterrupted()) {
            // 执行业务
        }
        System.out.println("线程正常退出");
    }
}

阻塞方法的中断特性

sleep() / wait() / join() / park() 阻塞时收到中断:

  1. 抛出InterruptedException

  2. 自动清除中断标记

避坑写法(捕获异常后恢复中断)
try {
    Thread.sleep(1000);
} catch (InterruptedException e) {
    // 重新打上中断标记,上层可感知
    Thread.currentThread().interrupt();
    break;
}

五、Java 线程间通信 4 种方式

1. volatile 共享变量(可见性通信)

多线程感知变量修改,依托 JMM 可见性,适合简单开关控制:

private static volatile boolean flag = true;

2. wait/notify 等待唤醒(synchronized 配套)

  1. 必须在同步块内调用,持有对象 monitor 锁

  2. wait:释放锁,进入 WAITING;notify/notifyAll 唤醒等待队列线程

  3. 标准生产者消费者基础方案

3. LockSupport park/unpark(无锁阻塞,AQS 底层依赖)

优势:无需同步块,可指定线程唤醒、不受调用顺序限制;多次 unpark 等价一次许可。

Thread parkT = new Thread(() -> {
    System.out.println("等待许可");
    LockSupport.park();
    System.out.println("被唤醒");
});
parkT.start();
LockSupport.unpark(parkT);

4. Thread.join 线程等待通信

通过等待其他线程完成,保证执行顺序。

5. 管道流 PipedInputStream/PipedWriter

内存管道,线程间直接传输字节 / 字符,适合数据传递。

六、高频面试简答题汇总

  1. 创建线程有几种方式?本质是什么?答:4 种写法,底层全部依靠 Thread.start () 创建操作系统内核线程。

  2. start () 和 run () 区别?答:run 是普通方法;start 申请 OS 线程,调度后执行 run。

  3. 线程 6 种状态,BLOCKED 和 WAITING 区别?答:BLOCKED 是等 synchronized 锁;WAITING 是 wait/park 无时限等待。

  4. 为什么不用 stop () 停止线程?答:强制释放锁,造成共享数据损坏,并发数据不一致。

  5. interrupt 中断原理,sleep 被中断会发生什么?答:仅打标记;sleep 中断抛异常并清除标记,需要手动恢复中断位。

  6. yield、sleep、join 区别?

  • yield:让出 CPU,不释放锁,RUNNABLE;

  • sleep:限时阻塞,不释放锁;

  • join:等待其他线程完成,底层 wait。

  1. 进程和线程区别?上下文切换代价谁更大?答:进程资源隔离,线程共享进程资源;进程切换开销远大于线程。

  2. Java 线程是用户级还是内核级?答:一对一内核级线程,每条 Java 线程对应一个 OS 轻量级进程。

  3. LockSupport 相比 wait/notify 优势?无需同步锁、可指定唤醒线程、park/unpark 不分先后顺序。


Logo

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

更多推荐