引言:Chrome的选择

打开Chrome浏览器的任务管理器(Shift + Esc),你会看到:

任务管理器
┌──────────────────────┬────────┬────────┐
│ 任务                 │ 内存   │ CPU    │
├──────────────────────┼────────┼────────┤
│ 浏览器进程           │ 80 MB  │ 1%     │
│ GPU进程              │ 120 MB │ 2%     │
│ 标签页: 百度         │ 65 MB  │ 0%     │
│ 标签页: GitHub       │ 95 MB  │ 3%     │
│ 标签页: YouTube      │ 150 MB │ 15%    │
│ 扩展程序: AdBlock    │ 25 MB  │ 0%     │
└──────────────────────┴────────┴────────┘

为什么Chrome选择多进程架构,而不是多线程?

  • 如果用多线程,YouTube标签页崩溃 → 整个浏览器崩溃
  • 如果用多进程,YouTube标签页崩溃 → 只影响一个标签页

这背后涉及进程与线程的本质区别


一、进程:资源分配的基本单位

1.1 什么是进程?

定义:进程(Process)是操作系统进行资源分配和调度的基本单位

更直白的解释:进程是一个正在运行的程序实例

// 在Java中启动一个新进程
public class ProcessExample {
    public static void main(String[] args) throws IOException {
        // 启动记事本程序(新进程)
        ProcessBuilder pb = new ProcessBuilder("notepad.exe");
        Process process = pb.start();

        System.out.println("新进程的PID: " + process.pid());
        System.out.println("新进程是否存活: " + process.isAlive());
    }
}

1.2 进程的组成

一个进程包含以下资源:

进程的内存布局(以32位系统为例)
┌─────────────────────┐ ← 0xFFFFFFFF (4GB)
│   内核空间          │   (操作系统使用,进程不可访问)
│   (1GB)             │
├─────────────────────┤ ← 0xC0000000 (3GB)
│                     │
│   栈 (Stack)        │ ← 向下增长
│   - 局部变量        │
│   - 函数调用栈      │
│                     │
│         ↓           │
│                     │
│   (未使用空间)      │
│                     │
│         ↑           │
│                     │
│   堆 (Heap)         │ ← 向上增长
│   - 动态分配内存    │
│   - new/malloc      │
│                     │
├─────────────────────┤
│   BSS段             │   (未初始化的全局变量)
├─────────────────────┤
│   Data段            │   (已初始化的全局变量)
├─────────────────────┤
│   Text段 (代码段)   │   (程序代码,只读)
└─────────────────────┘ ← 0x00000000

进程独有的资源

  1. 内存空间

    • 代码段(Text):程序代码
    • 数据段(Data/BSS):全局变量
    • 堆(Heap):动态分配的内存
    • 栈(Stack):函数调用和局部变量
  2. 文件描述符

    • 打开的文件
    • Socket连接
    • 管道等
  3. 进程ID(PID)

    • 系统唯一标识
  4. 环境变量

    • PATH、HOME等

1.3 进程的隔离性

关键特性:不同进程之间是相互隔离的。

// 进程A
public class ProcessA {
    private static int count = 0;

    public static void main(String[] args) {
        count = 100;
        System.out.println("进程A的count: " + count);
    }
}

// 进程B(同时运行)
public class ProcessB {
    private static int count = 0;

    public static void main(String[] args) {
        count = 200;
        System.out.println("进程B的count: " + count);
        // 无法访问进程A的count变量
    }
}

输出

进程A的count: 100
进程B的count: 200

进程A和进程B的count变量是完全独立的,互不影响。


二、线程:CPU调度的基本单位

2.1 什么是线程?

定义:线程(Thread)是操作系统进行CPU调度的基本单位

形象比喻

  • 进程 = 一个公司
  • 线程 = 公司里的员工

关键点:同一个进程内的线程共享进程的资源

public class ThreadExample {
    private static int sharedCount = 0;  // 共享变量

    public static void main(String[] args) {
        // 线程1
        Thread t1 = new Thread(() -> {
            sharedCount = 100;
            System.out.println("线程1修改后: " + sharedCount);
        });

        // 线程2
        Thread t2 = new Thread(() -> {
            System.out.println("线程2读取到: " + sharedCount);
        });

        t1.start();
        try { Thread.sleep(100); } catch (InterruptedException e) {}
        t2.start();
    }
}

输出

线程1修改后: 100
线程2读取到: 100  ← 线程2能看到线程1的修改

2.2 线程的组成

一个线程包含以下私有资源:

线程的私有资源
┌─────────────────────┐
│ 程序计数器 (PC)     │ ← 指向下一条要执行的指令
├─────────────────────┤
│ 虚拟机栈            │ ← 存储局部变量和方法调用
│  ┌────────────┐     │
│  │ 栈帧 3     │     │
│  ├────────────┤     │
│  │ 栈帧 2     │     │
│  ├────────────┤     │
│  │ 栈帧 1     │     │
│  └────────────┘     │
├─────────────────────┤
│ 本地方法栈          │ ← native方法调用
├─────────────────────┤
│ 线程ID (TID)        │ ← 线程的唯一标识
└─────────────────────┘

线程私有的资源(不共享):

  1. 程序计数器(PC)

    • 记录当前线程执行到哪一行代码
    • 每个线程独立
  2. 虚拟机栈

    • 局部变量
    • 方法参数
    • 方法返回地址
  3. 线程ID

    • 线程的唯一标识

线程共享的资源(来自进程):

  1. 堆内存

    • new 出来的对象
    • 静态变量
  2. 方法区

    • 类信息
    • 常量池
  3. 文件描述符

    • 打开的文件
    • Socket连接

2.3 一个进程内的多个线程

进程的内存空间
┌──────────────────────────────────┐
│         进程 A                    │
│  ┌────────────────────────────┐  │
│  │    堆 (Heap)               │  │ ← 所有线程共享
│  │  ┌──────┐  ┌──────┐       │  │
│  │  │Object│  │Object│       │  │
│  │  └──────┘  └──────┘       │  │
│  └────────────────────────────┘  │
│                                   │
│  ┌────────────────────────────┐  │
│  │    方法区 (Method Area)    │  │ ← 所有线程共享
│  │  - 类信息                  │  │
│  │  - 静态变量                │  │
│  └────────────────────────────┘  │
│                                   │
│  线程1        线程2      线程3    │
│  ┌─────┐    ┌─────┐    ┌─────┐  │
│  │ PC  │    │ PC  │    │ PC  │  │ ← 每个线程私有
│  ├─────┤    ├─────┤    ├─────┤  │
│  │Stack│    │Stack│    │Stack│  │ ← 每个线程私有
│  └─────┘    └─────┘    └─────┘  │
└──────────────────────────────────┘

三、进程 vs 线程:核心对比

对比维度进程 (Process)线程 (Thread)
定义资源分配的基本单位CPU调度的基本单位
资源占用重量级(MB级)轻量级(KB级)
内存空间独立的地址空间共享进程的地址空间
通信方式IPC(管道、消息队列、共享内存)直接读写共享变量
通信复杂度复杂,需要系统调用简单,但需要同步控制
创建开销大(需要分配内存、复制资源)小(只需创建栈和PC)
创建速度慢(毫秒级)快(微秒级)
切换开销大(需要切换地址空间、刷新TLB)小(只需切换栈和PC)
隔离性强(一个进程崩溃不影响其他进程)弱(一个线程崩溃导致整个进程崩溃)
数据安全性高(进程间相互隔离)低(需要加锁保护共享数据)
典型应用浏览器的多标签页、微服务架构Web服务器处理并发请求

3.1 资源占用对比实验

public class ProcessVsThreadTest {
    public static void main(String[] args) throws Exception {
        // 测试1:创建进程的开销
        long start1 = System.nanoTime();
        ProcessBuilder pb = new ProcessBuilder("java", "-version");
        Process process = pb.start();
        process.waitFor();
        long time1 = System.nanoTime() - start1;

        // 测试2:创建线程的开销
        long start2 = System.nanoTime();
        Thread thread = new Thread(() -> {
            // 空任务
        });
        thread.start();
        thread.join();
        long time2 = System.nanoTime() - start2;

        System.out.println("创建进程耗时: " + time1 / 1_000_000.0 + "ms");
        System.out.println("创建线程耗时: " + time2 / 1_000_000.0 + "ms");
        System.out.println("进程/线程耗时比: " + (double)time1 / time2);
    }
}

典型输出

创建进程耗时: 45.2ms
创建线程耗时: 0.8ms
进程/线程耗时比: 56.5x

结论:创建进程的开销是创建线程的50-100倍。

3.2 通信方式对比

进程间通信(IPC)- 复杂

// 使用管道进行进程间通信
public class ProcessCommunication {
    public static void main(String[] args) throws Exception {
        // 父进程创建管道
        PipedOutputStream out = new PipedOutputStream();
        PipedInputStream in = new PipedInputStream(out);

        // 启动子进程(实际中需要通过ProcessBuilder)
        // 这里简化演示原理
        ProcessBuilder pb = new ProcessBuilder("子进程命令");
        // ... 复杂的IPC配置 ...
    }
}

线程间通信 - 简单

// 线程间共享变量
public class ThreadCommunication {
    private static String message = null;  // 共享变量

    public static void main(String[] args) throws Exception {
        // 线程1:写入
        Thread writer = new Thread(() -> {
            message = "Hello from Thread 1";
        });

        // 线程2:读取
        Thread reader = new Thread(() -> {
            while (message == null) {
                // 等待消息
            }
            System.out.println("收到消息: " + message);
        });

        writer.start();
        reader.start();

        writer.join();
        reader.join();
    }
}

四、上下文切换:性能的隐形杀手

4.1 什么是上下文切换?

定义:CPU从一个线程切换到另一个线程时,需要保存当前线程的状态,恢复新线程的状态,这个过程叫上下文切换

上下文切换过程
时刻 T1:线程A正在执行
┌─────────────┐
│   线程A     │
│  ┌───────┐  │
│  │ PC=100│  │ ← 程序计数器指向第100行
│  │ 寄存器 │  │ ← 各种寄存器的值
│  │ Stack │  │ ← 栈指针
│  └───────┘  │
└─────────────┘

发生中断(时间片到期/等待IO)

时刻 T2:保存线程A的状态
┌─────────────┐
│  保存到内存  │
│  PC=100     │
│  寄存器值   │
│  栈指针     │
└─────────────┘

时刻 T3:恢复线程B的状态
┌─────────────┐
│  从内存加载  │
│  PC=250     │
│  寄存器值   │
│  栈指针     │
└─────────────┘

时刻 T4:线程B开始执行
┌─────────────┐
│   线程B     │
│  ┌───────┐  │
│  │ PC=250│  │ ← 从第250行继续执行
│  │ 寄存器 │  │
│  │ Stack │  │
│  └───────┘  │
└─────────────┘

4.2 上下文切换的成本

进程切换的成本(高)

进程切换需要:
1. 保存进程A的状态
   - 所有寄存器
   - 程序计数器
   - 栈指针
   - ...

2. 切换地址空间
   - 更新页表
   - 刷新TLB(Translation Lookaside Buffer)
   - 刷新CPU缓存(L1/L2/L3)← 最大的开销!

3. 恢复进程B的状态
   - 加载所有寄存器
   - 加载程序计数器
   - 加载栈指针
   - ...

总耗时:5-10微秒

线程切换的成本(低)

线程切换需要:
1. 保存线程A的状态
   - 所有寄存器
   - 程序计数器
   - 栈指针

2. 不需要切换地址空间
   - 页表不变
   - TLB不需要刷新
   - CPU缓存大部分可以保留 ← 省了最大的开销!

3. 恢复线程B的状态
   - 加载所有寄存器
   - 加载程序计数器
   - 加载栈指针

总耗时:1-2微秒

结论:进程切换的成本是线程切换的3-5倍。

4.3 上下文切换的性能影响

public class ContextSwitchCost {
    private static final int COUNT = 10_000_000;

    public static void main(String[] args) throws Exception {
        // 测试1:单线程(无上下文切换)
        long start1 = System.nanoTime();
        int sum1 = 0;
        for (int i = 0; i < COUNT; i++) {
            sum1 += i;
        }
        long time1 = System.nanoTime() - start1;

        // 测试2:两个线程互相切换(大量上下文切换)
        Object lock = new Object();
        int[] sum2 = {0};
        CountDownLatch latch = new CountDownLatch(2);

        long start2 = System.nanoTime();

        Thread t1 = new Thread(() -> {
            for (int i = 0; i < COUNT / 2; i++) {
                synchronized (lock) {
                    sum2[0] += i;
                    lock.notify();
                    try { lock.wait(); } catch (InterruptedException e) {}
                }
            }
            latch.countDown();
        });

        Thread t2 = new Thread(() -> {
            for (int i = COUNT / 2; i < COUNT; i++) {
                synchronized (lock) {
                    sum2[0] += i;
                    lock.notify();
                    try { lock.wait(); } catch (InterruptedException e) {}
                }
            }
            latch.countDown();
        });

        t1.start();
        t2.start();
        latch.await();
        long time2 = System.nanoTime() - start1;

        System.out.println("单线程耗时: " + time1 / 1_000_000.0 + "ms");
        System.out.println("多线程(频繁切换)耗时: " + time2 / 1_000_000.0 + "ms");
        System.out.println("性能下降: " + (double)time2 / time1 + "x");
    }
}

典型输出

单线程耗时: 25ms
多线程(频繁切换)耗时: 8500ms
性能下降: 340x

关键结论过多的上下文切换会严重降低性能


五、Java中的线程

5.1 Java线程模型

重要事实:Java线程是对操作系统线程的封装

Java线程与操作系统线程的映射关系

Java应用层
┌─────────────────────────────────┐
│  Thread t1 = new Thread(...);   │
│  t1.start();                    │
└────────────┬────────────────────┘
             │
             │ JNI调用
             ↓
JVM层
┌─────────────────────────────────┐
│  HotSpot VM                      │
│  - 线程管理                      │
│  - 线程调度                      │
└────────────┬────────────────────┘
             │
             │ 系统调用
             ↓
操作系统层
┌─────────────────────────────────┐
│  Linux: pthread_create()         │
│  Windows: CreateThread()         │
│  macOS: pthread_create()         │
└─────────────────────────────────┘
             │
             ↓
内核线程 (Kernel Thread)

5.2 1:1 线程模型

现代JVM采用1:1线程模型:一个Java线程对应一个操作系统内核线程。

public class JavaThreadMapping {
    public static void main(String[] args) {
        Thread thread = new Thread(() -> {
            System.out.println("Java线程ID: " + Thread.currentThread().getId());
            System.out.println("Java线程名称: " + Thread.currentThread().getName());

            // 获取操作系统线程ID(Java 9+)
            long nativeThreadId = Thread.currentThread().threadId();
            System.out.println("操作系统线程ID: " + nativeThreadId);
        }, "MyThread");

        thread.start();
    }
}

输出

Java线程ID: 12
Java线程名称: MyThread
操作系统线程ID: 23456  ← 操作系统分配的真实线程ID

5.3 Java线程的生命周期管理

public class ThreadLifecycle {
    public static void main(String[] args) throws Exception {
        Thread thread = new Thread(() -> {
            System.out.println("线程开始执行");
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                System.out.println("线程被中断");
            }
            System.out.println("线程执行结束");
        });

        // 1. NEW状态:线程对象创建,但未启动
        System.out.println("状态1: " + thread.getState());  // NEW

        // 2. 调用start()启动线程
        thread.start();
        Thread.sleep(100);
        System.out.println("状态2: " + thread.getState());  // RUNNABLE

        // 3. 线程sleep时
        Thread.sleep(500);
        System.out.println("状态3: " + thread.getState());  // TIMED_WAITING

        // 4. 等待线程结束
        thread.join();
        System.out.println("状态4: " + thread.getState());  // TERMINATED
    }
}

六、协程:用户态线程

6.1 什么是协程?

协程(Coroutine):也叫用户态线程,由程序自己调度,不需要操作系统参与。

线程 vs 协程

操作系统线程(内核态)
┌─────────────────┐
│  操作系统负责调度 │ ← 需要系统调用,开销大
│  线程1          │
│  线程2          │
│  线程3          │
└─────────────────┘

协程(用户态)
┌─────────────────┐
│  线程1          │
│  ├─ 协程1.1     │ ← 程序自己调度,开销小
│  ├─ 协程1.2     │
│  └─ 协程1.3     │
│                 │
│  线程2          │
│  ├─ 协程2.1     │
│  └─ 协程2.2     │
└─────────────────┘

6.2 协程的优势

  1. 创建开销极小

    • 线程:几KB栈空间
    • 协程:几百字节
  2. 切换开销极小

    • 线程切换:需要系统调用,1-2微秒
    • 协程切换:用户态切换,几十纳秒
  3. 可以创建海量协程

    • 线程:受限于系统资源,通常几千个
    • 协程:可以创建上百万个

6.3 Java中的虚拟线程(Project Loom)

Java 21+ 引入了虚拟线程(Virtual Threads),本质上就是协程。

// Java 21+ 的虚拟线程
public class VirtualThreadExample {
    public static void main(String[] args) throws Exception {
        // 传统平台线程
        Thread platformThread = Thread.ofPlatform()
            .name("platform-thread")
            .start(() -> {
                System.out.println("平台线程: " + Thread.currentThread());
            });

        // 虚拟线程(协程)
        Thread virtualThread = Thread.ofVirtual()
            .name("virtual-thread")
            .start(() -> {
                System.out.println("虚拟线程: " + Thread.currentThread());
            });

        platformThread.join();
        virtualThread.join();

        // 创建100万个虚拟线程(秒级完成)
        try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
            for (int i = 0; i < 1_000_000; i++) {
                executor.submit(() -> {
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {}
                });
            }
        }
        System.out.println("100万个虚拟线程创建完成");
    }
}

对比

特性平台线程(OS Thread)虚拟线程(Virtual Thread)
创建开销几KB几百字节
切换开销1-2微秒几十纳秒
最大数量几千个百万级
阻塞成本高(占用OS线程)低(不占用OS线程)
适用场景CPU密集型IO密集型

七、实战:验证线程与进程的特性

7.1 验证进程隔离性

// ProcessA.java
public class ProcessA {
    private static int value = 100;

    public static void main(String[] args) throws Exception {
        System.out.println("进程A的PID: " + ProcessHandle.current().pid());
        System.out.println("进程A的value: " + value);

        // 修改value
        value = 200;
        System.out.println("进程A修改后的value: " + value);

        Thread.sleep(10000);  // 保持进程运行
    }
}

// ProcessB.java
public class ProcessB {
    private static int value = 100;

    public static void main(String[] args) {
        System.out.println("进程B的PID: " + ProcessHandle.current().pid());
        System.out.println("进程B的value: " + value);  // 仍然是100,不受进程A影响
    }
}

7.2 验证线程共享内存

public class ThreadSharing {
    private static int sharedValue = 0;

    public static void main(String[] args) throws Exception {
        // 线程1:写入
        Thread writer = new Thread(() -> {
            sharedValue = 100;
            System.out.println("线程1写入: " + sharedValue);
        }, "Writer");

        // 线程2:读取
        Thread reader = new Thread(() -> {
            try {
                Thread.sleep(100);  // 等待线程1写入
                System.out.println("线程2读取: " + sharedValue);  // 能读到100
            } catch (InterruptedException e) {}
        }, "Reader");

        writer.start();
        reader.start();

        writer.join();
        reader.join();
    }
}

八、总结

8.1 核心要点

  1. 进程是资源分配的基本单位

    • 拥有独立的地址空间
    • 进程间相互隔离
    • 创建和切换开销大
  2. 线程是CPU调度的基本单位

    • 共享进程的地址空间
    • 线程间可以直接通信
    • 创建和切换开销小
  3. 进程 vs 线程选择

    • 需要隔离性 → 选择进程(如Chrome浏览器)
    • 需要高性能通信 → 选择线程(如Web服务器)
  4. 上下文切换是性能杀手

    • 过多的上下文切换会严重降低性能
    • 线程切换比进程切换开销小
    • 协程切换比线程切换开销更小
  5. Java线程模型

    • 1:1映射到操作系统线程
    • Java 21+ 引入虚拟线程(协程)

8.2 类比总结

概念现实世界类比
进程一个公司
线程公司里的员工
进程隔离不同公司的员工互不认识
线程共享同一公司的员工共享办公室
上下文切换员工换岗(需要交接工作)
协程一个员工同时处理多个任务

8.3 思考题

  1. 为什么浏览器用多进程而不是多线程?
  2. 什么场景下应该使用多进程而不是多线程?
  3. 上下文切换的成本主要来自哪里?
  4. 虚拟线程(协程)相比平台线程有什么优势?

8.4 下一篇预告

《Java线程的生命周期:6种状态详解》

  • 线程的6种状态是什么?
  • 状态之间如何转换?
  • 如何使用jstack分析线程状态?
  • BLOCKED和WAITING的区别?

扩展阅读