Java 进程与线程:从概念到实战
1. 引言
在 Java 开发中,进程与线程是两个最基础也最容易混淆的概念。很多初学者在接触多线程编程时,往往只记住了 Thread 类和 Runnable 接口,却对进程与线程的本质区别、Java 中如何创建线程、线程的生命周期等问题缺乏系统认识。本文将从操作系统层面出发,结合 Java 代码实例,帮你彻底理清进程与线程的关系,并掌握 Java 多线程编程的核心要点。
2. 进程与线程的基本概念
2.1 什么是进程
进程是操作系统进行资源分配和调度的基本单位,是程序的一次动态执行过程。每个进程拥有独立的地址空间、内存、文件描述符等系统资源,进程之间相互隔离,互不干扰。
例如,你同时打开了一个浏览器和一个 IDE,操作系统会为它们分别创建独立的进程,各自拥有独立的内存空间。一个进程崩溃通常不会直接导致另一个进程崩溃。
2.2 什么是线程
线程是进程内部的一条执行路径,是 CPU 调度和执行的最小单位。一个进程可以包含多个线程,这些线程共享进程的地址空间和系统资源,但各自拥有独立的栈和程序计数器。
以浏览器为例,一个浏览器进程内部可能包含多个线程:一个线程负责渲染页面,一个线程负责处理网络请求,一个线程负责响应用户点击事件。这些线程共享浏览器的内存数据,但各自执行不同的任务。
2.3 进程与线程的区别
| 对比维度 | 进程 | 线程 |
|---|---|---|
| 资源拥有 | 独立地址空间、独立资源 | 共享所属进程的地址空间和资源 |
| 系统开销 | 创建、切换开销大 | 创建、切换开销小 |
| 通信方式 | 进程间通信(IPC)复杂,如管道、消息队列 | 线程间通过共享内存直接通信,简单高效 |
| 健壮性 | 一个进程崩溃不影响其他进程 | 一个线程崩溃可能导致整个进程崩溃 |
| 独立性 | 相互独立 | 依附于进程,不能独立存在 |
3. Java 中进程的创建
在 Java 中,我们通常不直接创建操作系统进程,而是通过 ProcessBuilder 或 Runtime.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:线程刚创建,尚未调用
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 中创建线程推荐优先使用
Runnable或Callable接口。 - 多线程访问共享数据时,必须考虑线程安全问题。
- 实际开发中优先使用线程池管理线程,避免频繁创建和销毁线程。
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐
所有评论(0)