避坑!线程 / 线程池从入门到华为云实战,面试官听了都点头

咱后端 er 天天跟 "并发""性能" 打交道,可一被问 "线程和进程啥区别?""线程池参数咋调?",是不是偶尔会脑子卡壳?别急,今天咱用 "食堂打饭""公司搬砖" 的例子,把线程那点事儿说透,最后再结合日志处理、华为云 IotDA 实战,保证你看完能直接拿去用!

一、先搞懂基础:线程是啥?跟进程啥关系?

线程,是可并发执行的独立指令序列,由操作系统进行调度,从而实现多任务同时处理 邪修的理解:

  • 每个线程相当于一个人,多个线程就是多人一起完成任务
  • 由操作系统CPU调度管理这些线程

多线程的好处:有效使用多线程处理任务,可以提高处理的速度。

1. 线程:后端的 "搬砖小弟"

你可以把线程理解成一个帮你干活的小弟------ 比如处理用户下单、计算订单金额、推送消息,每个小弟负责一个具体任务。它轻量级,吃的 "内存资源" 少,启动快,还能跟其他小弟共享 "工具间"(也就是进程的资源)。

2. 进程 vs 线程:像 "公司" 和 "部门"?

举个栗子:

  • 进程 = 一家互联网公司:有自己的办公室(内存空间)、营业执照(系统资源),公司之间互不干扰(进程隔离)。
  • 线程 = 公司里的技术部、产品部:共享公司的办公室和设备(进程资源),但各干各的活(独立执行)。

划重点:一个进程至少有一个线程(比如单线程程序),也能有多个线程(比如多线程处理并发请求)。要是进程挂了,里面的线程全得歇菜 ------ 就像公司倒闭,部门也没了。

二、并发 vs 并行:食堂打饭的两种姿势

这俩词天天见,但总有人搞混?看食堂例子就懂了:

场景 并发(Concurrency) 并行(Parallelism)
食堂打饭 1 个窗口,10 个人轮流打(CPU 切换快,看似同时) 3 个窗口,3 个人同时打(真・同时执行)
底层逻辑 单核 CPU 的 "时间分片"(快速切换任务) 多核 CPU 的 "多任务同步执行"
后端常用场景 单核心处理多用户请求 多核心处理大批量数据(比如统计订单)

简单说:并发是 "假装同时",并行是 "真同时" ------ 咱写代码优化性能,本质就是让程序能更好地利用并发 / 并行能力。

三、Java 里的 Thread 类:如何指挥 "搬砖小弟"?

Java 里操作线程的核心就是Thread类,咱不用记所有方法,重点掌握 3 个 "指挥小弟" 的姿势:

使用多线程时,要能够:

  1. 要能够创建多个线程出来
  2. 要给每个线程设置它的任务代码
  3. 启动线程:线程就会开始处理它的任务了

常见的线程创建方式有:

  • 继承Thread类,重写Thread的run方法:直接将任务代码放到 run方法里
  • 实现Runnable接口,重写run方法,将任务代码写在run方法里。然后将Runnable交给创建出来的Thread
  • 实现Callable接口,重写call方法,将任务代码写在call方法里。然后将Callable交给创建出来的Thread
方式 优点 缺点
继承Thread类 编程比较简单,可以直接使用Thread类中的方法 扩展性较差,不能再继承其他的类,不能返回线程执行的结果
实现Runnable接口【建议】 扩展性强,实现该接口的同时还可以继承其他的类。 编程相对复杂,不能返回线程执行的结果
实现Callable接口 扩展性强,实现该接口的同时还可以继承其他的类。可以得到线程执行的结果 编程复杂

1. 启动小弟:start () vs run () 别搞混!

scss 复制代码
// 错误示范:直接调用run(),本质是普通方法,不会启动新线程
Thread wrongThread = new Thread(() -> System.out.println("我是普通方法"));
wrongThread.run(); // 主线程里执行,不算新线程!
// 正确姿势:调用start(),才会真正启动新线程
Thread rightThread = new Thread(() -> System.out.println("我是新线程干活"));
rightThread.start(); // 新线程执行,跟主线程并行

踩坑提醒:start () 是 "叫醒小弟让他自己干",run () 是 "你替小弟干" ------ 面试常考,别踩!

⚠️注意:

  • 当有多个线程在执行时,哪个线程先执行、哪个线程后执行 是我们目前无法控制的,所以每次运行时输出的结果都不保证一样
  • 在main方法里要先启动子线程。否则就不再是2个线程同时交叉执行了,而是主线程的代码执行完毕后才开始子线程

2. 常用方法:给小弟发指令

  • sleep(long ms):让小弟歇会儿(比如歇 1000ms),期间不释放锁(后面讲锁会提)。
  • join():让主线程等小弟干完再走(比如主线程要等统计线程算完结果)。
  • interrupt():提醒小弟 "别干了"(不是强制终止,需小弟配合判断isInterrupted())。

四、线程安全:为啥多小弟干活会 "打架"?

1. 问题场景:抢厕所的悲剧

假设 10 个线程(小弟)同时改一个变量count(比如统计网站在线人数):

arduino 复制代码
private static int count = 0;
public static void main(String[] args) {
    for (int i = 0; i < 10; i++) {
        new Thread(() -> {
            for (int j = 0; j < 1000; j++) {
                count++; // 看似简单,实则是"读-改-写"三步,多线程会乱
            }
        }).start();
    }
    System.out.println(count); // 结果大概率不是10000!
}

原因:多个线程同时 "读 count→改 count→写 count",会出现 "覆盖"------ 比如线程 A 读了count=5,还没写完,线程 B 又读了count=5,最后俩线程都写 6,实际该是 7。

2. 加锁:给 "厕所" 装上门

解决线程安全,核心是让多线程排队干活------ 也就是加锁。Java 里有 3 种常用方式:

方式 1:synchronized(内置锁,简单粗暴)

像给厕所装了个 "自动门",进去会锁门,出来自动开门:

csharp 复制代码
// 方法加锁:整个方法只能一个线程进
public synchronized void addCount() {
    count++;
}
// 代码块加锁:只锁关键代码(推荐,性能好)
public void addCount() {
    synchronized (this) { // this是锁对象,也可以用其他对象
        count++;
    }
}

方式 2:ReentrantLock(手动锁,更灵活)

像带钥匙的门,需要手动 "开锁""关锁",支持超时、中断:

csharp 复制代码
private static ReentrantLock lock = new ReentrantLock();
public void addCount() {
    lock.lock(); // 开锁
    try {
        count++; // 关键代码
    } finally {
        lock.unlock(); // 必须关锁!不然会死锁
    }
}

方式 3:volatile(轻量级,只保证可见性)

注意!volatile 不是锁,它只能让变量 "对所有线程可见"(比如线程 A 改了变量,线程 B 能立刻看到),但解决不了 "原子性"(比如count++还是会乱)。适合场景:单线程写、多线程读(比如配置变量)。

五、线程池:别天天 "招小弟",找个 "外包公司" 更省心

1. 为啥要用线程池?

如果每次处理任务都new Thread(),就像 "每次搬砖都招个临时工"------ 招人的时间(线程启动)、辞退的成本(线程销毁)太高,还容易招太多人把公司挤爆(内存溢出)。

线程池就是一个 "外包公司" ------ 提前养一批小弟(核心线程),有活就派小弟干,没活小弟歇着(空闲线程),活太多就排队(阻塞队列),实在忙不过来再临时加人(非核心线程),高效又可控。

2. 线程池好处:3 个关键词

  • 复用线程:避免频繁创建 / 销毁线程的开销。
  • 控制并发:防止线程太多导致 CPU / 内存过载。
  • 便于管理:能监控线程状态、统一配置(比如超时时间)。

3. 创建方式:2 种,推荐第 2 种!

方式 1:Executors 工具类(简单但有坑)

JDK 提供了现成的 "外包公司模板",但生产环境慎用

ini 复制代码
// 1. 固定线程数:适合任务量稳定的场景(比如处理订单)
ExecutorService fixedPool = Executors.newFixedThreadPool(5);
// 2. 单线程池:适合需要顺序执行的任务(比如日志写入)
ExecutorService singlePool = Executors.newSingleThreadExecutor();
// 坑点:newCachedThreadPool(缓存线程池)可能创建无限线程,导致OOM;newScheduledThreadPool(定时线程池)默认无界队列,也可能OOM

方式 2:ThreadPoolExecutor(自定义,推荐!)

手动配置 "外包公司" 参数,灵活又安全,核心构造方法:

java 复制代码
ThreadPoolExecutor executor = new ThreadPoolExecutor(
    2, // 核心线程数:外包公司固定员工数(再闲也不辞退)
    5, // 最大线程数:公司最多能招的人(固定+临时)
    60, // 空闲时间:临时员工没活干,60秒后辞退
    TimeUnit.SECONDS, // 时间单位
    new LinkedBlockingQueue<>(10), // 阻塞队列:活太多,10个任务排队
    Executors.defaultThreadFactory(), // 线程工厂:创建线程的方式
    new ThreadPoolExecutor.AbortPolicy() // 拒绝策略:活太多排不下,直接抛异常
);
划重点:拒绝策略有 4 种
  1. AbortPolicy(默认):抛异常,提醒 "活太多了!"
  1. CallerRunsPolicy:让提交任务的线程自己干(比如主线程自己处理)
  1. DiscardPolicy:悄悄扔掉最新的任务(不推荐,丢任务)
  1. DiscardOldestPolicy:扔掉队列里最老的任务,加新任务(谨慎用)

六、项目实战:线程池到底咋用?

光说不练假把式,结合两个真实场景聊聊:

1. 日志处理:AOP + 线程池,不卡主业务!

咱项目里用 AOP 记录接口日志(比如请求参数、响应时间),如果直接在 AOP 里同步写日志(比如写文件、存数据库),会阻塞主业务(比如用户下单要等日志写完才能返回)。

用线程池的目的:把 "写日志" 这个非核心任务,扔给线程池异步处理,主业务不用等,响应速度直接起飞!

核心代码思路:

java 复制代码
// 1. 定义线程池(推荐用ThreadPoolExecutor自定义)
@Bean
public Executor logExecutor() {
    return new ThreadPoolExecutor(2, 4, 60, TimeUnit.SECONDS,
        new LinkedBlockingQueue<>(20), new ThreadPoolExecutor.DiscardOldestPolicy());
}
// 2. AOP里异步调用
@Aspect
@Component
public class LogAspect {
    @Autowired
    private Executor logExecutor;
    @AfterReturning("execution(* com.xxx.controller.*.*(..))")
    public void logAfter(JoinPoint joinPoint) {
        // 把日志任务扔给线程池,主业务直接返回
        logExecutor.execute(() -> {
            String log = buildLog(joinPoint); // 构建日志内容
            logDao.insert(log); // 写入数据库
        });
    }
}

2. 华为云 IotDA:多线程处理设备消息,不堆消息!

华为云 IotDA 是用来接收设备数据的(比如智能电表、温湿度传感器),如果设备多(比如上千台),每秒会发几百条消息过来。

用多线程的目的:单线程处理消息会 "忙不过来"------ 消息堆积在队列里,导致设备数据延迟、甚至丢失。多线程能并行处理消息(比如解析数据、存储到时序数据库),提高吞吐量,保证消息不堆积、实时性高!

核心逻辑:用线程池监听 IotDA 的消息队列,每个线程拉取消息后独立处理,避免单线程瓶颈。

总结:线程 / 线程池核心考点

  1. 进程是资源单位,线程是执行单位;
  1. 并发是 "假装同时"(单核),并行是 "真同时"(多核);
  1. 线程安全用 synchronized/ReentrantLock,volatile 只保证可见性;
  1. 线程池用 ThreadPoolExecutor 自定义,拒绝策略按需选;
  1. 项目里用线程池,核心是 "异步解耦""提高吞吐量"。

最后问一句:你项目里线程池参数是咋调的?有没有踩过线程安全的坑?评论区聊聊,一起避坑!

相关推荐
pengzhuofan6 分钟前
Java设计模式-享元模式
java·设计模式·享元模式
灵魂猎手12 分钟前
10. Mybatis XML配置到SQL的转换之旅
java·后端·源码
掉鱼的猫12 分钟前
10分钟带你体验 Solon 的状态机
java
用户40993225021214 分钟前
如何让FastAPI在百万级任务处理中依然游刃有余?
后端·ai编程·trae
汪子熙15 分钟前
解决 Node.js 无法获取本地颁发者证书问题的详细分析与代码示例
javascript·后端
武子康15 分钟前
大数据-76 Kafka 从发送到消费:Kafka 消息丢失/重复问题深入剖析与最佳实践
大数据·后端·kafka
笃行35016 分钟前
在TencentOS3上部署OpenTenBase:从入门到实战的完整指南
后端
皮皮林55116 分钟前
从一个程序员的角度告诉你:“12306”有多牛逼?
java
AAA修煤气灶刘哥16 分钟前
被参数校验 / 日志逼疯?AOP:1 个切入点,所有方法自动加 buff
java·后端·面试
suntq18 分钟前
Kiran 桌面报错排查与日志速查表
后端