咱后端 er 天天跟 "并发""性能" 打交道,可一被问 "线程和进程啥区别?""线程池参数咋调?",是不是偶尔会脑子卡壳?别急,今天咱用 "食堂打饭""公司搬砖" 的例子,把线程那点事儿说透,最后再结合日志处理、华为云 IotDA 实战,保证你看完能直接拿去用!
一、先搞懂基础:线程是啥?跟进程啥关系?
线程,是可并发执行的独立指令序列,由操作系统进行调度,从而实现多任务同时处理 邪修的理解:
- 每个线程相当于一个人,多个线程就是多人一起完成任务
- 由操作系统CPU调度管理这些线程
多线程的好处:有效使用多线程处理任务,可以提高处理的速度。
1. 线程:后端的 "搬砖小弟"
你可以把线程理解成一个帮你干活的小弟------ 比如处理用户下单、计算订单金额、推送消息,每个小弟负责一个具体任务。它轻量级,吃的 "内存资源" 少,启动快,还能跟其他小弟共享 "工具间"(也就是进程的资源)。
2. 进程 vs 线程:像 "公司" 和 "部门"?
举个栗子:
- 进程 = 一家互联网公司:有自己的办公室(内存空间)、营业执照(系统资源),公司之间互不干扰(进程隔离)。
- 线程 = 公司里的技术部、产品部:共享公司的办公室和设备(进程资源),但各干各的活(独立执行)。
划重点:一个进程至少有一个线程(比如单线程程序),也能有多个线程(比如多线程处理并发请求)。要是进程挂了,里面的线程全得歇菜 ------ 就像公司倒闭,部门也没了。
二、并发 vs 并行:食堂打饭的两种姿势
这俩词天天见,但总有人搞混?看食堂例子就懂了:
场景 | 并发(Concurrency) | 并行(Parallelism) |
---|---|---|
食堂打饭 | 1 个窗口,10 个人轮流打(CPU 切换快,看似同时) | 3 个窗口,3 个人同时打(真・同时执行) |
底层逻辑 | 单核 CPU 的 "时间分片"(快速切换任务) | 多核 CPU 的 "多任务同步执行" |
后端常用场景 | 单核心处理多用户请求 | 多核心处理大批量数据(比如统计订单) |
简单说:并发是 "假装同时",并行是 "真同时" ------ 咱写代码优化性能,本质就是让程序能更好地利用并发 / 并行能力。
三、Java 里的 Thread 类:如何指挥 "搬砖小弟"?
Java 里操作线程的核心就是Thread类,咱不用记所有方法,重点掌握 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 种
- AbortPolicy(默认):抛异常,提醒 "活太多了!"
- CallerRunsPolicy:让提交任务的线程自己干(比如主线程自己处理)
- DiscardPolicy:悄悄扔掉最新的任务(不推荐,丢任务)
- 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 的消息队列,每个线程拉取消息后独立处理,避免单线程瓶颈。
总结:线程 / 线程池核心考点
- 进程是资源单位,线程是执行单位;
- 并发是 "假装同时"(单核),并行是 "真同时"(多核);
- 线程安全用 synchronized/ReentrantLock,volatile 只保证可见性;
- 线程池用 ThreadPoolExecutor 自定义,拒绝策略按需选;
- 项目里用线程池,核心是 "异步解耦""提高吞吐量"。
最后问一句:你项目里线程池参数是咋调的?有没有踩过线程安全的坑?评论区聊聊,一起避坑!