Java多线程入门

在 Java 的世界里,多线程(Multi-threading)是每个进阶开发者的必经之路。如果你觉得这个词听起来很高级,别担心,今天我们用最直白的方式拆解它。

1. 概念热身:什么是进程和线程?

别去背书本上的定义,看这个类比:

你的电脑就像一个大型商场,每个正在运行的应用程序(比如微信、浏览器、你的 Java 程序)就是一个进程。进程就像商场里一家独立的店铺,有自己的场地、资源和员工。

线程,就是店铺里真正干活的员工。

  • 一家店(进程)至少要有一个员工(线程)才能营业,这个员工叫主线程

  • 一家店可以同时雇佣多个员工(多线程)一起干活,共享店里的冰箱、灶台等资源(内存)。

  • 员工之间可以并行工作:你切菜,我炒菜,他打包,效率瞬间拉满。

2. 为什么要用多线程?

核心目的:压榨 CPU 性能,提高效率。

想象一个快餐店:

店里只有一个厨师(单线程)。客人来了要点餐,厨师先去记菜单,然后去炒菜,炒完菜出来打包,再叫客人取餐。这个过程里,他炒菜的时候就没法接待下一位客人,所有人都得排队等着。

现在我们雇了3个厨师(多线程):

  • 厨师A专门负责点餐接单

  • 厨师B专门负责炒菜

  • 厨师C专门负责打包出餐

三个人同时干活,客人流动速度直接翻三倍,CPU这个“灶台”被压榨得明明白白。

多线程做的就是这样的事:把一个大任务拆成多个小任务,让 CPU 的多个核心(多个灶眼)同时运转,不让硬件闲着。

3. 核心术语大白话

新手最容易晕这几个词,我们挨个“翻译”:

术语 大白话 快餐店比喻
线程 (Thread) 正在干活的员工,一个执行流程的独立单元 厨师本人
任务 (Runnable/Callable) 要做什么事,具体的操作步骤 菜谱:先放油,再放鸡块,炸5分钟
执行者 (Executor) 负责分配任务给线程的“排班经理” 厨师长站在出菜口喊:“3号单给小王,5号单给小李”
锁 (Lock / synchronized) 抢控制权:同一时刻只允许一个人碰的数据或资源 只有一个盐罐,谁拿到谁才能放盐,防止两个人同时往里倒盐洒一地
死锁 (Deadlock) 互相等对方手里的东西,结果谁也不能动 厨师A拿着盐等油,厨师B拿着油等盐,俩人互不相让,厨房瘫痪
线程池 (Thread Pool) 固定数量的员工,有活了就干,没活就待命 一个3人厨师小组,用完不辞退,下个任务直接给他们
上下文切换 CPU 在不同线程之间快速“换人”干活 厨师切菜切到一半,被喊去收银,回来再接着切——切换本身也消耗时间,所以线程不是越多越好

掌握了这些词,你基本就拿到了多线程世界的“驾照”。

4. 实战:怎么在 Java 里开线程?

在 Java 的进化史上,雇佣“厨师”主要有三种方式。搞清楚它们的区别,你才算真入门。

① 继承 Thread 类(老派写法)

这是最原始的方法,直接定义一个具体的厨师类。

java

// 定义一个"独行侠厨师",他就是一个线程
class MyChef extends Thread {
    public void run() { 
        System.out.println("独行侠厨师在炒菜..."); 
    }
}

public class Demo {
    public static void main(String[] args) {
        // 启动线程——注意是 start(),不是 run()!
        new MyChef().start(); 
    }
}

⚠️ 致命细节:一定要调 start() 而不是 run()。调 run() 就相当于你直接跑去炒菜,根本没请新厨师,程序还是单线程。

② 实现 Runnable 接口(主流做法)

这是目前最通用的方式:实现了“任务(菜谱)”与“执行者(厨师)”的分离。

java

// 1. 定义任务:我要干什么(菜谱)?
Runnable task = () -> {
    System.out.println(Thread.currentThread().getName() + " 正在炸鸡...");
};

// 2. 雇佣员工并开始:让谁干?
new Thread(task, "厨师A").start();

这样做的好处是:菜谱(任务)可以被多个厨师复用,更灵活,也符合面向对象“组合优于继承”的原则。

③ 线程池 (Thread Pool) —— 职业选手的选择

资深警告:在真实项目中,严禁频繁手动创建和销毁线程!

java

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

// 创建一个包含3个名额的"员工池"
ExecutorService pool = Executors.newFixedThreadPool(3);

// 往池子里不停扔任务,厨师们自动接活
pool.execute(() -> System.out.println("池子里的老师傅正在处理点餐..."));
pool.execute(() -> System.out.println("另一位师傅在处理外卖订单..."));

// 告知不再接收新任务,已有任务执行完后关闭
pool.shutdown(); 

线程池就是预先养好一批“厨师”,任务来了直接分配,任务干完了他们不回家,等着下一批任务。避免反复请人、辞退的巨大开销,还能防止任务太多把系统挤爆。

5. 资深对比:我该怎么选?

方式 角色比喻 优点 缺点 生产建议
继承 Thread 孤儿厨师 写法最直观 限制继承,耦合度过高 了解即可,实际勿用
实现 Runnable 通用菜谱 灵活、支持资源共享 仍需手动 new Thread,未享受池化能力 适合配合线程池使用
线程池 餐饮集团 性能最高,控人数,防崩溃 参数配置需要了解 企业级首选

简单总结一句:永远把任务写成 Runnable(或 Callable),然后用线程池去执行它。

6. 给新手的避坑指南

看完上面的内容你可能已经跃跃欲试,但请先读完这几条“血泪教训”,能帮你少熬几个夜:

  • 坑1:把 run() 当成 start()
    调 run() 只是普通方法调用,不会创建新线程,你的“新厨师”其实根本不存在。一定要用 start() 才能真正启动一个新线程。

  • 坑2:一堆厨师抢一勺盐 —— 线程安全问题
    当多个线程同时修改同一个变量(比如剩余食材数量),不加锁就会发生“两个厨师同时拿走最后一份鸡块”的惨剧,数据错乱。这时候必须用 synchronized 或 Lock,保证同一时刻只有一个厨师能碰关键数据。

  • 坑3:请来的厨师忘了解雇 —— 线程泄漏
    创建了大量线程却从不关闭,就像请了100个临时工干完活不让他们走,系统资源被吃光,最终程序崩溃。线程池用完后一定记得 shutdown()

  • 坑4:锁加多了,厨房彻底瘫痪 —— 死锁
    厨师A拿着盐等油,厨师B拿着油等盐,谁也不肯先放手。避免死锁的关键是:规定好所有厨师永远按相同的顺序拿资源(比如先拿盐,再拿油),并且尽量缩小锁的范围。

  • 坑5:以为线程越多越快 —— 上下文切换成本
    100个厨师挤在一个灶台前,光互相撞来撞去、让路、交换工具的时间就比真正炒菜的时间还长。线程数最好设置为 CPU 核心数的 2 倍左右(视任务类型而定),线程池正好帮你控住数量。


总结一下:

多线程不是玄学,它只是让程序在同一时间内“分身”去处理不同的事。记住三个核心:

  • 任务:你要干什么(菜谱)

  • 执行者:让谁来干(厨师 / 线程池)

  • :什么时候只能一个人干(防抢盐)

掌握了这三样,你就已经跨过了多线程的大门!剩下的就是在实战中慢慢积累经验,希望各位能从这个学到新知识,祝我们无限进步!

更多推荐