1. 多线程基础回顾:进程与线程的区别、线程的生命周期、Java/Python中的线程创建方式

1.1 进程与线程:别再傻傻分不清

说实话,我面试过不少人,问起进程和线程的区别,很多人张口就来「进程是资源分配的最小单位,线程是CPU调度的最小单位」。背得挺熟,但一问「那你项目里怎么用的?」就卡壳了。

咱们用大白话捋一捋。进程就像一家独立的公司——有自己的办公场地(内存空间)、财务(文件句柄)、员工(资源)。线程呢,就是公司里的一个员工。公司之间互不干扰,但公司内部的员工可以共享会议室、打印机这些资源。

我在重构一个支付网关项目时遇到过这么个事:原本每个用户请求都开一个新进程,结果服务器内存直接爆了。后来改成线程池,同样的硬件配置,吞吐量翻了3倍。你想想看,进程切换的开销有多大?光是切换页表、刷新TLB就够CPU忙活一阵子了。

核心区别总结:

  • 资源开销:进程拥有独立地址空间,线程共享进程地址空间。创建线程的开销比进程小得多(大约快10-100倍)。
  • 通信方式:进程间通信(IPC)需要管道、消息队列、共享内存等机制;线程间通信直接读写共享变量即可。
  • 健壮性:一个进程挂了不影响其他进程;一个线程挂了,整个进程都可能崩溃——嗯,这里要注意,这也是为什么我在关键服务里会做线程隔离。

1.2 线程的生命周期:从出生到消亡

线程的一生,说白了就是「新建→就绪→运行→阻塞→死亡」这五个状态来回切换。我习惯用一张图来理解它:

线程生命周期状态流转图 新建 (New) 就绪 (Runnable) 运行 (Running) 阻塞 (Blocked) 死亡 (Terminated) start() CPU调度 yield() / 时间片用完 sleep()/wait()/I/O notify()/I/O完成 run()结束/异常

这张图我画了好多年了,每次培训新人都会拿出来讲。有几个关键点我得提醒你:

  • 新建→就绪:调用 start() 后线程进入就绪队列,等着CPU临幸。注意,不是调用 run()!run() 只是普通方法调用,不会启动新线程。我曾经见过一个同事直接调 run() 调试了半天——嗯,谁还没犯过这种错呢。
  • 就绪→运行:由操作系统调度器决定。你没法控制具体哪个线程先跑,只能通过优先级给个「建议」。
  • 运行→阻塞:调用 sleep()、wait() 或者发起I/O操作时,线程会主动让出CPU。这里有个坑:sleep() 不会释放锁,wait() 会释放锁。我在做分布式锁时吃过这个亏,后面会细讲。
  • 阻塞→就绪:阻塞条件解除后,线程回到就绪状态,重新参与调度。
  • 死亡:run() 方法执行完毕或者抛出未捕获异常。线程一旦死亡,不能重新 start(),否则会抛 IllegalThreadStateException。

避坑指南:我曾经在日志收集系统里用 Thread.stop() 强制终止线程,结果导致共享数据结构损坏,整个进程都挂了。记住:stop()、suspend()、resume() 这些方法已经被标记为废弃,千万别用!正确的做法是通过标志位或 interrupt() 来优雅终止。

1.3 Java中的线程创建:不止两种方式

很多人说Java创建线程有两种方式:继承 Thread 和实现 Runnable。其实还有第三种——实现 Callable 接口,配合 FutureTask 使用。我个人更推荐后两种,因为Java是单继承,实现接口更灵活。

咱们直接看代码:

// 方式一:继承Thread(不推荐)
class MyThread extends Thread {
    @Override
    public void run() {
        System.out.println("线程运行中: " + Thread.currentThread().getName());
    }
}
new MyThread().start();

// 方式二:实现Runnable(推荐)
class MyRunnable implements Runnable {
    @Override
    public void run() {
        System.out.println("线程运行中: " + Thread.currentThread().getName());
    }
}
new Thread(new MyRunnable()).start();

// 方式三:实现Callable + FutureTask(带返回值)
class MyCallable implements Callable<String> {
    @Override
    public String call() throws Exception {
        return "线程返回结果: " + Thread.currentThread().getName();
    }
}
FutureTask<String> futureTask = new FutureTask<>(new MyCallable());
new Thread(futureTask).start();
String result = futureTask.get(); // 阻塞等待结果

我在重构一个批量数据处理系统时,大量使用了 Callable + 线程池的方式。每个任务处理一批数据,返回处理结果,主线程统一汇总。这样既利用了多核CPU,又避免了手动管理线程的麻烦。

小技巧:Java 8以后,推荐用Lambda表达式简化线程创建:

new Thread(() -> System.out.println("Lambda线程")).start();

ExecutorService executor = Executors.newFixedThreadPool(10);
executor.submit(() -> {
    // 业务逻辑
    return "done";
});

1.4 Python中的线程创建:GIL是个绕不开的坎

Python的线程创建比Java简单,但有个大坑——全局解释器锁(GIL)。说白了,GIL保证同一时刻只有一个线程在执行Python字节码。所以Python多线程在CPU密集型任务上基本是废的,但在I/O密集型任务上依然好用。

我做过一个爬虫项目,用Python多线程并发抓取100个网页,比单线程快了将近10倍。为什么?因为网络I/O等待时,线程会释放GIL,其他线程就能趁机执行。

import threading
import time

# 方式一:继承Thread
class MyThread(threading.Thread):
    def run(self):
        print(f"线程 {self.name} 运行中")
        
t = MyThread()
t.start()

# 方式二:直接传入target函数(推荐)
def worker(name):
    print(f"线程 {name} 运行中")
    time.sleep(1)
    
threads = []
for i in range(5):
    t = threading.Thread(target=worker, args=(f"worker-{i}",))
    threads.append(t)
    t.start()

# 等待所有线程完成
for t in threads:
    t.join()
    
print("所有线程执行完毕")

Python 3.2以后,GIL的实现做了优化,不再是简单的「拿到GIL才能执行」,而是引入了「超时机制」——一个线程如果连续执行一段时间(默认5毫秒)还没释放GIL,其他线程就有机会抢过来。但说实话,对于纯计算任务,还是建议用 multiprocessing 模块绕过GIL。

Python线程 vs 进程选择指南:

场景 推荐方案 原因
I/O密集型(网络请求、文件读写) 多线程 I/O等待时释放GIL,线程切换开销小
CPU密集型(计算、图像处理) 多进程 每个进程有独立GIL,真正并行
混合型 多进程+协程 进程间用队列通信,协程处理I/O

1.5 线程创建的最佳实践

说了这么多,总结几条我踩坑踩出来的经验:

  • 永远不要直接创建线程:用线程池代替。Java用 Executors 或 ThreadPoolExecutor,Python用 concurrent.futures.ThreadPoolExecutor。手动创建线程就像用手工拧螺丝——费时费力还不均匀。
  • 给线程起个有意义的名字:排查问题时,看到「pool-1-thread-3」和「order-process-thread-3」完全是两种心情。
  • 处理好异常:线程内抛出的异常不会传播到主线程。Java里可以用 UncaughtExceptionHandler,Python里在 run() 方法里加 try-except。
  • 注意线程安全:共享变量加锁,或者用线程安全的数据结构。我见过太多因为 HashMap 并发put导致死循环的案例了——换成 ConcurrentHashMap 能省很多事。

我的习惯:在项目启动时,我会打印一份线程池的配置信息,包括核心线程数、最大线程数、队列类型、拒绝策略。这样线上出问题时,第一眼就能确认线程池配置是否合理。你也不妨试试。

好了,多线程的基础就回顾到这里。这些概念虽然基础,但就像盖房子的地基——地基不稳,后面再花哨的设计都是空中楼阁。下一节我们开始真正动手,看看怎么用这些知识去重构一个真实项目。

可可精品资料库