作为Java后端程序员,「多线程」绝对是初级面试里的高频考点——几乎所有校招、1-2年经验的面试,都会问到:Thread和Runnable有啥区别?线程有哪些状态?怎么手动创建一个线程?
很多小白被问懵,不是因为知识点难,而是分不清“理论”和“实战”,要么记混状态流转,要么写不出简单的实战代码。
今天就用「通俗讲解+实战案例+面试避坑」的方式,把多线程基础吃透,看完直接能应对面试,也能上手写demo!
(文末附面试高频提问总结,建议收藏备用✨)
一、先搞懂:为什么需要多线程?
在讲Thread和Runnable之前,先想一个简单的问题:我们为什么要用多线程?
举个生活中的例子:你煮泡面的时候,不需要一直盯着锅——可以同时烧开水、拿调料包、洗杯子,几件事并行做,节省总时间。
Java程序也是一样:单线程程序是“排队做事”,一次只能执行一个任务,效率很低;而多线程能让多个任务“并行执行”(比如同时处理多个用户请求、异步处理日志),提升程序的执行效率和响应速度。
这也是后端开发中,多线程的核心作用:提升程序并发能力,优化执行效率。
二、核心知识点:Thread vs Runnable(面试必问)
创建Java线程,最常用的两种方式就是「继承Thread类」和「实现Runnable接口」。两者的区别,是初级面试的重中之重,记住一句话:Runnable是任务,Thread是线程载体。
1. 方式一:继承Thread类
Thread类是Java提供的「线程类」,继承它之后,重写run()方法,就能定义线程要执行的任务;调用start()方法,就能启动线程。
实战代码(直接复制可运行):
// 1. 继承Thread类
class MyThread extends Thread {
// 重写run()方法:定义线程要执行的任务
@Override
public void run() {
for (int i = 0; i < 5; i++) {
// Thread.currentThread().getName():获取当前线程名称
System.out.println(Thread.currentThread().getName()
+ ":执行任务" + i);
try {
// 线程休眠100毫秒,模拟任务执行耗时
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
// 测试类
public class ThreadDemo {
public static void main(String[] args) {
// 2. 创建线程对象
MyThread thread1 = new MyThread();
MyThread thread2 = new MyThread();
// 3. 给线程设置名称(便于区分)
thread1.setName("线程A");
thread2.setName("线程B");
// 4. 启动线程(注意:不能调用run()方法,否则就是单线程)
thread1.start();
thread2.start();
}
}
运行结果(重点观察):
线程A和线程B会交替执行,不会一直是A执行完再执行B——这就是多线程的“并行”效果(实际是CPU快速切换,看起来并行)。
⚠️ 面试避坑:启动线程必须用start()方法,不能用run()!调用run()方法,本质还是主线程在执行,不会开启新线程。
2. 方式二:实现Runnable接口
Runnable接口是Java提供的「任务接口」,里面只有一个抽象方法run(),用来定义线程要执行的任务。
它的核心优势:解决Java单继承的局限性(一个类继承了Thread,就不能继承其他类,但可以实现多个接口)。
实战代码(直接复制可运行):
// 1. 实现Runnable接口,定义任务
class MyRunnable implements Runnable {
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName()
+ ":执行任务" + i);
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
// 测试类
public class RunnableDemo {
public static void main(String[] args) {
// 2. 创建任务对象(不是线程对象)
MyRunnable task = new MyRunnable();
// 3. 创建线程对象,把任务传入Thread(线程载体承载任务)
Thread thread1 = new Thread(task, "线程C");
Thread thread2 = new Thread(task, "线程D");
// 4. 启动线程
thread1.start();
thread2.start();
}
}
运行结果和继承Thread类类似,线程C和线程D交替执行。
3. Thread和Runnable核心区别(面试必背)
用表格总结,清晰好记,面试直接说:
|
对比维度 |
Thread类 |
Runnable接口 |
|
本质 |
线程类,本身承载线程和任务 |
任务接口,只定义任务,不承载线程 |
|
继承限制 |
单继承,不能继承其他类 |
无继承限制,可实现多个接口 |
|
资源共享 |
难实现(需静态变量) |
易实现(多个线程共用一个任务对象) |
|
推荐度 |
低(有继承限制) |
高(灵活、无限制) |
补充:实际开发中,我们几乎不用继承Thread类,都是用Runnable接口(或Lambda表达式简化,下文实战会讲),因为它更灵活,也符合“单一职责”设计原则(任务和线程分离)。
三、核心考点:线程的5种状态(图文+流转,记牢不混)
线程从创建到销毁,会经历5种状态,这是面试高频提问(比如:线程有哪些状态?sleep和wait会让线程进入什么状态?),结合图文理解,不用死记硬背。
1. 线程的5种状态(Java官方定义)
用通俗的语言解释,结合生活场景,一看就懂:
•新建状态(New):创建了线程对象,但还没调用start()方法(比如new Thread()后,线程就是新建状态,相当于“泡面刚放进碗里,还没加水”)。
•就绪状态(Runnable):调用了start()方法后,线程等待CPU调度(相当于“泡面对好了,等你拿起叉子吃”,CPU就是“你”,调度就是“拿起叉子”)。
•运行状态(Running):CPU调度到该线程,线程执行run()方法里的任务(相当于“你正在吃泡面”)。
•阻塞状态(Blocked):线程暂时停止执行,等待某个条件满足后,再回到就绪状态(相当于“吃泡面中途,接了个电话,暂停吃面,挂了电话再继续吃”)。
•死亡状态(Terminated):线程执行完run()方法,或被中断、异常终止(相当于“泡面吃完了,碗空了”,线程彻底结束,不能再启动)。
2. 线程状态流转图(面试画出来,加分!)
核心流转路径(记准,不要乱):
新建状态(New) → 就绪状态(Runnable)【调用start()】 → 运行状态(Running)【CPU调度】 → 阻塞状态(Blocked)【sleep/wait/同步锁】 → 就绪状态(Runnable)【阻塞条件解除】 → 运行状态(Running) → 死亡状态(Terminated)【任务执行完/异常】
⚠️ 重点提醒:
•线程一旦进入死亡状态,就不能再调用start()方法重启(相当于泡面吃完了,不能再泡一次)。
•sleep()和wait()的区别(面试常问):sleep()会让线程进入阻塞状态,但不会释放锁;wait()会让线程进入阻塞状态,且会释放锁(后续会专门讲,先记核心)。
四、实战升级:用Lambda简化Runnable,模拟真实场景
初级面试中,除了让你写Thread和Runnable的demo,还可能让你“模拟多线程处理任务”(比如:多线程下载文件、多线程统计数据)。
下面用Lambda表达式简化Runnable(Java 8+特性,面试写出来更加分),模拟「多线程处理用户请求」的场景,实战代码可直接运行、直接套用。
实战场景:3个线程同时处理3个用户请求
public class LambdaThreadDemo {
public static void main(String[] args) {
// 用Lambda表达式简化Runnable,不用单独写实现类
// 线程1:处理用户A的请求
Thread threadA = new Thread(() -> {
System.out.println("用户A请求处理中...");
try {
// 模拟处理耗时200毫秒
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("用户A请求处理完成✅");
}, "处理线程1");
// 线程2:处理用户B的请求
Thread threadB = new Thread(() -> {
System.out.println("用户B请求处理中...");
try {
Thread.sleep(150);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("用户B请求处理完成✅");
}, "处理线程2");
// 线程3:处理用户C的请求
Thread threadC = new Thread(() -> {
System.out.println("用户C请求处理中...");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("用户C请求处理完成✅");
}, "处理线程3");
// 启动所有线程
threadA.start();
threadB.start();
threadC.start();
}
}
运行结果分析:
三个线程会同时启动,处理请求的顺序不确定(因为CPU调度随机),但最终都会执行完任务——这就是真实后端开发中,多线程处理并发请求的简单模拟。
面试加分点:能说出“多线程处理请求,能提升系统响应速度,避免单个请求耗时过长,导致其他请求阻塞”。
五、面试高频提问总结(背会直接应对面试)
整理了初级面试中,多线程基础的5个高频问题,答案直接记,不用再翻书:
1.Q:Thread和Runnable的区别?
A:① Thread是线程类,承载线程和任务;Runnable是任务接口,只定义任务。② Thread有单继承限制,Runnable无限制。③ 推荐用Runnable,更灵活、易实现资源共享。
2.Q:启动线程用start()还是run()?为什么?
A:用start();因为start()会开启新线程,让线程进入就绪状态,等待CPU调度;而run()只是主线程调用的普通方法,不会开启新线程。
3.Q:Java线程有哪些状态?
A:新建(New)、就绪(Runnable)、运行(Running)、阻塞(Blocked)、死亡(Terminated),共5种。
4.Q:线程进入阻塞状态的常见场景?
A:① 调用sleep()方法;② 调用wait()方法;③ 等待同步锁(synchronized);④ 等待IO操作(比如文件读取)。
5.Q:线程死亡后,还能重启吗?
A:不能;线程进入死亡状态(任务执行完/异常终止),就不能再调用start()方法重启,只能重新创建线程对象。
最后总结
多线程基础,是Java后端面试的“敲门砖”——初级面试不会问太难的源码,重点考察你对「Thread/Runnable区别」「线程状态」「实战代码」的掌握。
今天的内容,从基础讲解到实战案例,再到面试总结,覆盖了所有初级考点,建议大家把实战代码复制到IDE里运行一遍,亲手感受多线程的执行效果,比死记硬背更有效。
📌 收藏本文,面试前快速过一遍,轻松应对多线程基础提问!
评论区留言:你面试时,被多线程问过哪些问题?一起交流避坑~
+ 实战,小白也能懂&spm=1001.2101.3001.5002&articleId=159155189&d=1&t=3&u=5f492bbee66740019641d4a65d1b8747)
616

被折叠的 条评论
为什么被折叠?



