1. 引言

在 Java 开发中,进程与线程是两个最基础也最容易混淆的概念。很多初学者在接触多线程编程时,往往只记住了 Thread 类和 Runnable 接口,却对进程与线程的本质区别、Java 中如何创建线程、线程的生命周期等问题缺乏系统认识。本文将从操作系统层面出发,结合 Java 代码实例,帮你彻底理清进程与线程的关系,并掌握 Java 多线程编程的核心要点。

2. 进程与线程的基本概念

2.1 什么是进程

进程是操作系统进行资源分配和调度的基本单位,是程序的一次动态执行过程。每个进程拥有独立的地址空间、内存、文件描述符等系统资源,进程之间相互隔离,互不干扰。

例如,你同时打开了一个浏览器和一个 IDE,操作系统会为它们分别创建独立的进程,各自拥有独立的内存空间。一个进程崩溃通常不会直接导致另一个进程崩溃。

2.2 什么是线程

线程是进程内部的一条执行路径,是 CPU 调度和执行的最小单位。一个进程可以包含多个线程,这些线程共享进程的地址空间和系统资源,但各自拥有独立的栈和程序计数器。

以浏览器为例,一个浏览器进程内部可能包含多个线程:一个线程负责渲染页面,一个线程负责处理网络请求,一个线程负责响应用户点击事件。这些线程共享浏览器的内存数据,但各自执行不同的任务。

2.3 进程与线程的区别

对比维度进程线程
资源拥有独立地址空间、独立资源共享所属进程的地址空间和资源
系统开销创建、切换开销大创建、切换开销小
通信方式进程间通信(IPC)复杂,如管道、消息队列线程间通过共享内存直接通信,简单高效
健壮性一个进程崩溃不影响其他进程一个线程崩溃可能导致整个进程崩溃
独立性相互独立依附于进程,不能独立存在

3. Java 中进程的创建

在 Java 中,我们通常不直接创建操作系统进程,而是通过 ProcessBuilderRuntime.exec() 来启动外部程序。

import java.io.BufferedReader;
import java.io.InputStreamReader;

public class ProcessDemo {
    public static void main(String[] args) throws Exception {
        // 使用 ProcessBuilder 启动一个外部进程(以 Windows 的 ping 命令为例)
        ProcessBuilder builder = new ProcessBuilder("ping", "-c", "3", "www.baidu.com");
        Process process = builder.start();

        // 读取子进程的输出
        BufferedReader reader = new BufferedReader(
                new InputStreamReader(process.getInputStream()));
        String line;
        while ((line = reader.readLine()) != null) {
            System.out.println(line);
        }

        // 等待子进程结束
        int exitCode = process.waitFor();
        System.out.println("子进程退出码:" + exitCode);
    }
}

注意:ProcessBuilder 启动的是独立的操作系统进程,与当前 JVM 进程相互隔离,无法直接共享内存数据。

4. Java 中线程的创建方式

Java 中创建线程主要有三种方式:继承 Thread 类、实现 Runnable 接口、实现 Callable 接口配合 FutureTask

4.1 继承 Thread 类

class MyThread extends Thread {
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + " 正在执行");
    }
}

public class ThreadDemo1 {
    public static void main(String[] args) {
        MyThread t1 = new MyThread();
        t1.start(); // 启动线程,注意是 start() 而不是 run()
    }
}

4.2 实现 Runnable 接口

class MyRunnable implements Runnable {
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + " 正在执行");
    }
}

public class ThreadDemo2 {
    public static void main(String[] args) {
        Thread t1 = new Thread(new MyRunnable());
        t1.start();
    }
}

4.3 实现 Callable 接口(带返回值)

import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;

class MyCallable implements Callable<Integer> {
    @Override
    public Integer call() throws Exception {
        int sum = 0;
        for (int i = 1; i <= 100; i++) {
            sum += i;
        }
        return sum;
    }
}

public class ThreadDemo3 {
    public static void main(String[] args) throws Exception {
        FutureTask<Integer> task = new FutureTask<>(new MyCallable());
        Thread t1 = new Thread(task);
        t1.start();

        // 获取线程执行结果(会阻塞直到线程执行完毕)
        Integer result = task.get();
        System.out.println("计算结果:" + result);
    }
}

4.4 三种方式的对比

创建方式优点缺点
继承 Thread代码简单,可直接使用 Thread 类的方法Java 单继承,继承后无法再继承其他类
实现 Runnable避免单继承限制,多个线程可共享同一个 Runnable 实例无法直接获取执行结果
实现 Callable可以获取执行结果,支持抛出异常代码相对复杂,需要配合 FutureTask

5. 线程的生命周期

Java 线程的生命周期包含以下六个状态:

new Thread()

start()

等待锁

wait()/join()

sleep()/wait(timeout)

run() 执行完毕

获取到锁

notify()/notifyAll()

时间到/notify()

NEW

RUNNABLE

BLOCKED

WAITING

TIMED_WAITING

TERMINATED

  • NEW:线程刚创建,尚未调用 start()
  • RUNNABLE:线程已调用 start(),处于可运行状态,等待 CPU 调度。
  • BLOCKED:线程等待获取锁进入同步代码块/方法。
  • WAITING:线程调用了 wait()join() 等方法,等待其他线程唤醒。
  • TIMED_WAITING:线程调用了 sleep(timeout)wait(timeout) 等方法,等待指定时间。
  • TERMINATED:线程执行完毕或异常退出。

6. 线程安全与同步

多个线程共享进程资源时,容易出现数据竞争问题。Java 提供了多种同步机制来保证线程安全。

6.1 synchronized 关键字

class Counter {
    private int count = 0;

    public synchronized void increment() {
        count++;
    }

    public int getCount() {
        return count;
    }
}

public class SyncDemo {
    public static void main(String[] args) throws InterruptedException {
        Counter counter = new Counter();

        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 10000; i++) {
                counter.increment();
            }
        });

        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 10000; i++) {
                counter.increment();
            }
        });

        t1.start();
        t2.start();
        t1.join();
        t2.join();

        System.out.println("最终计数:" + counter.getCount()); // 输出 20000
    }
}

6.2 volatile 关键字

volatile 保证变量的可见性,但不保证原子性,适用于多线程读写共享标志位的场景。

public class VolatileDemo {
    private static volatile boolean flag = false;

    public static void main(String[] args) throws InterruptedException {
        Thread reader = new Thread(() -> {
            while (!flag) {
                // 自旋等待 flag 变为 true
            }
            System.out.println("flag 已变为 true,退出循环");
        });

        reader.start();
        Thread.sleep(1000);

        flag = true; // 主线程修改 flag
    }
}

6.3 Lock 接口

java.util.concurrent.locks.Lock 提供了比 synchronized 更灵活的锁操作。

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

class CounterWithLock {
    private int count = 0;
    private final Lock lock = new ReentrantLock();

    public void increment() {
        lock.lock();
        try {
            count++;
        } finally {
            lock.unlock();
        }
    }

    public int getCount() {
        return count;
    }
}

7. 线程池

频繁创建和销毁线程会带来较大的系统开销,因此实际开发中推荐使用线程池来复用线程。

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class ThreadPoolDemo {
    public static void main(String[] args) {
        // 创建固定大小为 4 的线程池
        ExecutorService executor = Executors.newFixedThreadPool(4);

        for (int i = 0; i < 10; i++) {
            final int taskId = i;
            executor.submit(() -> {
                System.out.println(Thread.currentThread().getName() + " 执行任务 " + taskId);
            });
        }

        // 关闭线程池
        executor.shutdown();
    }
}

建议:生产环境推荐使用 ThreadPoolExecutor 手动配置线程池参数,避免使用 Executors 提供的快捷方法可能带来的资源耗尽风险。

8. 进程与线程的实战选择

在实际开发中,如何选择使用进程还是线程?

  • 需要强隔离、高稳定性:选择多进程。例如微服务架构中,每个服务独立部署为独立进程,一个服务崩溃不影响其他服务。
  • 需要高并发、频繁通信:选择多线程。例如 Web 服务器处理大量并发请求,线程间共享缓存数据,通信开销小。
  • 需要跨语言协作:选择多进程。进程间可以通过网络协议或消息队列通信,不受语言限制。
  • 需要充分利用多核 CPU:两者都可以,但线程切换开销更小,更适合计算密集型任务的并行处理。

9. 总结

本文从操作系统层面介绍了进程与线程的基本概念和区别,并详细讲解了 Java 中创建线程的三种方式、线程的生命周期、线程安全与同步机制以及线程池的使用。核心要点如下:

  • 进程是资源分配的基本单位,线程是 CPU 调度的基本单位。
  • Java 中创建线程推荐优先使用 RunnableCallable 接口。
  • 多线程访问共享数据时,必须考虑线程安全问题。
  • 实际开发中优先使用线程池管理线程,避免频繁创建和销毁线程。
Logo

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

更多推荐