线程基础回顾:进程与线程的区别,线程的生命周期,Java/Python中的线程创建方式
各位同学好,我是你们的老朋友。今天咱们来聊聊多线程最基础的东西——进程与线程。
说实话,我见过不少工作了三四年的工程师,一提到线程生命周期就支支吾吾。其实这很正常,因为平时写业务代码很少直接操作线程。但做重构优化的时候,这些基础就成了你的底气。
进程 vs 线程:到底差在哪?
先问个问题:你打开一个浏览器,里面开了10个标签页。这是10个进程,还是10个线程?
答案是——看浏览器实现。Chrome 每个标签页是一个进程,老版 IE 所有标签页共享一个进程。这就是进程和线程最直观的区别。
| 维度 | 进程 | 线程 |
|---|---|---|
| 资源拥有 | 独立的内存空间、文件句柄 | 共享所属进程的资源 |
| 切换开销 | 大(需要切换页表、刷新TLB) | 小(只需保存寄存器) |
| 通信方式 | IPC(管道、消息队列、共享内存) | 直接读写共享变量 |
| 健壮性 | 一个进程挂了不影响其他 | 一个线程崩溃可能导致整个进程挂掉 |
我个人习惯这样理解:进程是资源分配的最小单位,线程是CPU调度的最小单位。你想想看,一个进程就像一家公司,有自己的办公室、资金、设备。线程就是公司里的员工,共用办公室,但每个人干自己的活。
核心要点:多线程的优势在于轻量、共享数据方便。但代价是——你得自己管好同步问题。我在项目中遇到过好几次,因为共享变量没加锁,线上数据全乱了。嗯,那感觉,比加班还难受。
线程的生命周期:从生到死
线程不是一创建就开始跑,也不是一跑就停不下来。它有几个明确的状态。Java 里定义得很清楚:
- NEW(新建)——new Thread() 之后,还没调用 start()
- RUNNABLE(可运行)——调了 start(),等待CPU调度
- BLOCKED(阻塞)——想拿锁,但锁被别人占着
- WAITING(等待)——调了 wait() 或 join(),等别人通知
- TIMED_WAITING(超时等待)——sleep() 或带超时的 wait()
- TERMINATED(终止)——run() 执行完,或者抛了未捕获异常
这里我画了一张状态流转图,帮你理清关系:
避坑指南:我曾经在重构一个消息推送系统时,发现线程大量卡在 BLOCKED 状态。查了半天,原来是某个工具类里用了 synchronized 方法,但方法体里调了外部 HTTP 接口。一个线程卡住,其他全等着。后来改成读写锁 + 超时机制,问题才解决。
Java 创建线程:三种姿势
Java 里创建线程,说白了就三种方式。我按推荐程度排个序:
方式一:继承 Thread 类
public class MyThread extends Thread {
@Override
public void run() {
System.out.println("线程跑起来了:" + Thread.currentThread().getName());
}
}
// 使用
MyThread t = new MyThread();
t.start();
这种方式最直观,但有个硬伤——Java 是单继承,你继承了 Thread 就不能继承别的了。我个人不太推荐,除非你只是想快速验证点什么。
方式二:实现 Runnable 接口
public class MyTask implements Runnable {
@Override
public void run() {
System.out.println("任务执行中...");
}
}
// 使用
Thread t = new Thread(new MyTask());
t.start();
这种方式更灵活,任务和线程分离。你想想看,同一个 MyTask 实例可以传给多个 Thread,也可以丢给线程池。我在实际项目中,90% 的场景都用这种方式。
方式三:实现 Callable 接口(带返回值)
public class MyCallable implements Callable<String> {
@Override
public String call() throws Exception {
return "任务结果:" + System.currentTimeMillis();
}
}
// 使用
ExecutorService pool = Executors.newFixedThreadPool(4);
Future<String> future = pool.submit(new MyCallable());
String result = future.get(); // 阻塞等待结果
Callable 的好处是能拿到返回值,还能抛异常。但注意,future.get() 是阻塞的,别在 UI 线程里直接调,否则界面卡死别怪我。
Python 创建线程:简单粗暴
Python 的 threading 模块用起来比 Java 轻量得多。但有个坑——GIL(全局解释器锁)。说白了,CPython 解释器同一时刻只能跑一个线程。所以 Python 多线程适合 I/O 密集型任务,不适合 CPU 密集型。
import threading
import time
def worker(name):
print(f"线程 {name} 开始工作")
time.sleep(2)
print(f"线程 {name} 结束工作")
# 创建线程
t1 = threading.Thread(target=worker, args=("A",))
t2 = threading.Thread(target=worker, args=("B",))
# 启动
t1.start()
t2.start()
# 等待结束
t1.join()
t2.join()
print("所有线程执行完毕")
Python 也支持继承 Thread 类的方式:
class MyThread(threading.Thread):
def __init__(self, name):
super().__init__()
self.name = name
def run(self):
print(f"自定义线程 {self.name} 运行中")
t = MyThread("Worker-1")
t.start()
注意:Python 的 GIL 不是绝对的枷锁。如果你的任务是网络请求、文件读写这类 I/O 操作,多线程依然能大幅提升吞吐量。但如果是纯计算,比如循环一亿次,那多线程反而可能比单线程慢——因为线程切换有开销。
小结一下
今天的内容其实就三块:
- 进程 vs 线程——资源隔离 vs 轻量共享,选哪个看场景
- 线程生命周期——六个状态,记住流转条件就行
- 创建方式——Java 推荐 Runnable/Callable,Python 用 threading.Thread
这些基础看着简单,但真到重构的时候,你会发现很多性能问题都出在这些细节上。比如线程状态不对导致死锁,或者创建线程的方式不对导致资源泄漏。嗯,后面几章我们会深入这些实战场景。
可可精品资料库