【Java】线程相关面试题 (基础)
文章目录
- 线程与进程区别
- 并行与并发区别解析
- 概念含义
- 资源利用
- 执行方式
- 应用场景
- 创建线程
- 线程状态
- 如何保证新建的三个线程按顺序执行
- wait方法和sleep方法的不同
- 所属类和使用场景
- 方法签名和参数说明
- 调用`wait`方法的前提条件
- 被唤醒的方式
- 与`notify`/`notifyAll`方法的协作
- 使用示例
- 注意事项
- 停止线程的三种方式
线程与进程区别
- 进程的定义与实例
- 进程是当程序被运行,从磁盘加载程序代码到内存时开启的。例如打开谷歌浏览器或txt文档等程序就是开启了一个进程,在Windows中有多实例进程(可打开多份,如浏览器、txt文档)和单实例进程(如tears客户端、企业微信在系统层面只能打开一份)。
- 线程的定义与作用
- 线程包含指令,交给CPU运行。进程至少包含一到多个线程,每个线程执行不同任务。
- 线程与进程的区别
- 进程是正在运行的程序实例,包含多个线程执行不同任务。不同进程使用不同内存空间,而当前进程下的所有线程可以共享该进程的内存空间。线程更轻量,其上下文切换成本一般比进程上下文切换成本低。面试时主要回答这三点:进程和线程的关系、内存占用情况(强调进程下线程共享内存)、线程更轻量且切换成本低。
并行与并发区别解析
概念含义
- 并行:同一时间动手做多件事情的能力。例如在多核CPU下,多个核心可以同时执行不同的线程,像四核CPU能同时执行四个线程,这些线程是真正意义上的同时进行。
- 并发:同一时间应对多件事情的能力。在单核CPU中,由于只有一个核心,多个线程不能同时执行,而是通过任务管理器分配时间片,轮流使用CPU,虽然每个时间片只有一个线程执行,但因CPU切换速度快,宏观上给人一种并行的感觉,微观上实际是串行执行。例如家庭主妇独自做饭、打扫卫生、给孩子喂奶,一个人轮流交替做这些事,就如同单核CPU处理多线程任务。
资源利用
- 并行:需要多核CPU等硬件资源支持,每个核心可以独立运行一个线程,实现真正的同时处理多个任务,充分利用了多核CPU的计算能力,提高了整体任务处理效率。
- 并发:主要依赖于操作系统的调度机制,在单核CPU环境下,通过合理分配时间片给不同线程,让多个任务看起来像是同时在处理,有效利用了单个CPU的时间资源,避免某个线程长时间占用CPU导致其他线程等待过久,但整体效率受限于单核CPU的处理能力。
执行方式
- 并行:多个任务在多个处理器或多核CPU的不同核心上同时执行,任务之间相互独立,不存在资源竞争(除非访问共享资源时需要进行同步处理),执行顺序是真正意义上的同时进行。
- 并发:多个任务在单核CPU上通过时间片轮转的方式交替执行,每个任务执行一段时间后暂停,切换到下一个任务,由于时间片很短,给用户造成任务在同时进行的错觉,但实际上在微观层面是串行执行的,任务之间可能存在频繁的上下文切换开销。
应用场景
- 并行:适用于计算密集型任务,如大规模数据处理、复杂科学计算等,通过将任务分解到多个核心上同时计算,可以显著缩短计算时间,提高计算性能。例如在图像渲染、视频编码解码等领域,利用多核CPU并行处理不同部分的图像或视频数据,能快速完成处理工作。
- 并发:常用于I/O密集型任务,如网络通信、文件读写等操作,这些任务在等待I/O操作完成时会阻塞线程,使用并发可以在等待一个任务的I/O操作时切换到其他任务执行,提高CPU利用率,避免线程长时间空闲等待。比如在一个Web服务器中,同时处理多个客户端的请求,每个请求在等待数据库查询或文件读取等I/O操作时,服务器可以切换去处理其他客户端请求,提高整体响应能力。
创建线程
-
创建线程的方式介绍
- 创建线程共有四种方式,分别是继承Thread类、实现Runnable接口、实现Callable接口和使用线程池创建线程。
-
继承Thread类创建线程
- 定义一个类继承Thread类,重写run方法,在run方法中编写线程要执行的代码。
- 使用时先创建该类的对象,然后调用start方法开启线程,new两次对象相当于开两个线程。
-
实现Runnable接口创建线程
- 定义一个类实现Runnable接口,重写run方法,该方法为线程执行的代码。
- 使用时先创建类的对象,将其包装在Thread类中,再调用Thread对象的start方法开启线程,new两次对象相当于开两个线程。
-
实现Callable接口创建线程
- 定义一个类实现Callable接口,重写call方法,call方法有返回值(通过泛型指定)且可抛异常,call方法的代码为线程要执行的逻辑。
- 使用时先创建类的对象,配合FutureTask包装该对象,再将FutureTask包装在Thread类中,调用Thread对象的start方法开启线程,通过FutureTask的get方法获取线程执行后的返回值。
-
使用线程池创建线程
- 创建一个类实现Runnable或Callable接口,编写线程执行逻辑。
- 使用时先创建固定大小的线程池(线程池后期会详细讲解),通过线程池的submit方法提交任务(即实现接口的类的对象),线程池会自动执行线程中的逻辑。
-
Runnable和Callable的区别
- 返回值:Runnable接口的run方法无返回值,Callable接口的call方法有返回值且需配合FutureTask使用get方法获取返回值。
- 异常处理:run方法不能抛异常,只能内部try - catch处理;call方法可以抛异常。
-
start方法和run方法的区别
- 功能:start方法用于启动线程,线程独立执行run方法中的代码;run方法是普通方法,直接调用如同调用普通方法,在当前线程顺序执行代码。
- 调用次数:start方法只能被调用一次启动线程,多次调用会抛异常;run方法可多次调用。
-
总结
- 创建线程有继承Thread类、实现Runnable接口、实现Callable接口和使用线程池四种方式,项目中一般使用线程池创建线程。
- Runnable和Callable的区别主要在返回值、异常处理方面。
- start方法用于启动线程且只能调用一次,run方法是普通方法可多次调用。
线程状态
- 线程状态面试题介绍
- 状态定义:参考Thread类的内部枚举类State,定义了六个线程状态,即new(新建)、runnable(可运行)、block(阻塞)、waiting(等待)、time waiting(时间等待)、terminated(终结)。
- 线程状态及转换初步讲解
- 新建状态:创建线程对象时进入,如创建线程t1和t2时。
- 就绪与运行状态:调用线程方法后进入就绪状态,抢到CPU时间片才有执行权,线程运行完成后进入死亡状态。
- 阻塞状态:线程加锁时,未获得锁的线程进入阻塞状态,获得到锁后转为可运行状态。
- 等待状态:线程内部调用wait方法进入等待状态,其他线程调用notify或notifyAll方法唤醒后变为可运行状态。
- 时间等待状态:线程调用sleep方法进入时间等待状态,时间结束后转为可运行状态。
- 总结线程状态及转换
- 线程状态总结:包含六个状态,新建、可运行、阻塞、等待、计时等待、终止状态。
- 状态转换关系梳理
- 新建到可执行:创建线程对象为新建状态,调用方法后转换为可执行状态。
- 可执行到终止:线程获取CPU执行权并执行结束后为终止状态。
- 可执行状态的其他转换
- 可执行到阻塞:未获取到锁(如synchronized或Lock锁)进入阻塞状态,获得到锁的执行权后切换为可执行状态。
- 可执行到等待:调用wait方法进入等待状态,其他线程调用notify或notifyAll唤醒后切换为可执行状态。
- 可执行到计时等待:调用sleep方法进入计时等待状态,时间到后切换为可执行状态。
文章目录
- 线程与进程区别
- 并行与并发区别解析
- 概念含义
- 资源利用
- 执行方式
- 应用场景
- 创建线程
- 线程状态
- 如何保证新建的三个线程按顺序执行
- wait方法和sleep方法的不同
- 所属类和使用场景
- 方法签名和参数说明
- 调用`wait`方法的前提条件
- 被唤醒的方式
- 与`notify`/`notifyAll`方法的协作
- 使用示例
- 注意事项
- 停止线程的三种方式
如何保证新建的三个线程按顺序执行
- 方法介绍
- 对于“如何保证新建t1、t2、t3三个线程按顺序执行”这一面试题,可使用线程中的“join方法”来解决。该方法的作用是等待线程运行结束,调用此方法的线程会被阻塞,进入time waiting(时间等待)状态,直到被调用“join方法”的线程执行完成后,调用者才能继续执行。
- 代码演示
- 在代码中创建了t1、t2、t3三个线程,在t2线程中调用了t1的“join方法”,这意味着t2线程想要运行必须等待t1线程结束;在t3线程中调用了t2的“join方法”,所以t3线程需等待t2线程运行结束后才能运行。启动线程的顺序不影响最终结果,最终会按t1、t2、t3的顺序执行。通过代码执行结果展示了t1先执行,完成后t2执行,t2执行完t3执行,从而验证了这种方法可保证线程按顺序执行。
-
notify和notifyAll的区别
为notify是只随机唤醒一个等待(wait)方法的线程,而notifyAll是唤醒所有等待方法的线程。
wait方法和sleep方法的不同
在Java中,wait
方法用于使当前线程等待,直到其他线程调用该对象的notify
方法或notifyAll
方法唤醒它,或者等待一定的时间(如果指定了超时时间)。以下是关于wait
方法的详细介绍:
所属类和使用场景
- 所属类:
wait
方法属于Object
类,这意味着Java中的任何对象都可以调用该方法。 - 使用场景:主要用于多线程编程中,实现线程之间的协作和同步。例如,当一个线程需要等待某个条件满足时,可以调用
wait
方法进入等待状态,直到其他线程改变了共享资源的状态并通知它。
方法签名和参数说明
- 方法签名:
public final void wait() throws InterruptedException
和public final native void wait(long timeout) throws InterruptedException
。 - 参数说明
- 无参的
wait
方法会使当前线程无限期等待,直到被唤醒。 - 带参数的
wait
方法接受一个long
类型的参数,表示等待的超时时间(以毫秒为单位)。如果在指定时间内没有被唤醒,线程会自动苏醒并继续执行。
- 无参的
调用wait
方法的前提条件
- 当前线程必须拥有该对象的锁。也就是说,
wait
方法必须在同步代码块(synchronized
块)中调用,否则会抛出IllegalMonitorStateException
异常。 - 例如,以下代码演示了正确调用
wait
方法的方式:
synchronized (object) {
// 当前线程获取了object对象的锁,可以调用wait方法
object.wait();
}
被唤醒的方式
- 其他线程调用
notify
方法:唤醒在此对象监视器上等待的单个线程。如果有多个线程在等待,选择是任意的,由操作系统的调度策略决定。 - 其他线程调用
notifyAll
方法:唤醒在此对象监视器上等待的所有线程。被唤醒的线程将竞争重新获取对象的锁,然后继续执行。 - 等待超时:如果调用了带超时参数的
wait
方法,当超时时间到达时,线程会自动苏醒,继续执行后续代码。
与notify
/notifyAll
方法的协作
wait
方法与notify
/notifyAll
方法必须在同一对象上调用,以实现线程之间的正确协作。- 通常,一个线程在等待某个条件时调用
wait
方法,而另一个线程在改变条件后调用notify
或notifyAll
方法来唤醒等待的线程。
使用示例
以下是一个简单的示例,展示了wait
方法和notify
方法的基本用法:
public class WaitNotifyExample {
public static void main(String[] args) {
final Object lock = new Object();
// 线程1:等待条件满足
Thread thread1 = new Thread(() -> {
synchronized (lock) {
try {
System.out.println("线程1:开始等待");
lock.wait(); // 释放锁并等待
System.out.println("线程1:被唤醒,继续执行");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
// 线程2:改变条件并通知线程1
Thread thread2 = new Thread(() -> {
synchronized (lock) {
System.out.println("线程2:改变条件,并通知线程1");
lock.notify(); // 唤醒等待的线程1
}
});
thread1.start();
try {
Thread.sleep(1000); // 确保线程1先进入等待状态
} catch (InterruptedException e) {
e.printStackTrace();
}
thread2.start();
}
}
在上述示例中,线程1获取lock
对象的锁后调用wait
方法进入等待状态,同时释放锁。线程2获取lock
对象的锁后调用notify
方法唤醒线程1,线程1被唤醒后重新竞争锁,获取锁后继续执行后续代码。
注意事项
- 在使用
wait
方法时,必须在循环中调用,以避免虚假唤醒(spurious wakeup)的问题。虚假唤醒是指线程在没有被其他线程明确唤醒的情况下苏醒,可能是由于操作系统或JVM的内部原因。例如:
while (condition) {
synchronized (object) {
object.wait();
}
}
- 调用
wait
方法的线程会释放对象的锁,但在被唤醒后重新竞争锁。如果多个线程同时竞争锁,唤醒顺序是不确定的,取决于操作系统的调度策略。 wait
方法会抛出InterruptedException
异常,当线程在等待过程中被中断时,会抛出该异常。因此,在调用wait
方法时,需要正确处理异常,以确保程序的稳定性和正确性。
文章目录
- 线程与进程区别
- 并行与并发区别解析
- 概念含义
- 资源利用
- 执行方式
- 应用场景
- 创建线程
- 线程状态
- 如何保证新建的三个线程按顺序执行
- wait方法和sleep方法的不同
- 所属类和使用场景
- 方法签名和参数说明
- 调用`wait`方法的前提条件
- 被唤醒的方式
- 与`notify`/`notifyAll`方法的协作
- 使用示例
- 注意事项
- 停止线程的三种方式
停止线程的三种方式
- 停止线程的三种方式
- 使用退出标志:通过定义一个标志变量(如
flag
),在run
方法中使用循环条件控制线程执行,当标志变量改变时,线程正常退出。例如,在my interrupt 1
类中,run
方法里使用while
循环(while (!flag)
),线程在循环内打印信息并睡眠3秒。主线程启动该线程后睡眠6秒,然后将flag
改为true
,使线程在6秒后正常退出。 - 调用stop方法(不推荐):
stop
方法可以强行终止线程,但此方法已作废不推荐使用。 - 调用interrupt方法:该方法包含两种情况。
- 一是打断阻塞的线程(如处于
sleep
、wait
、join
状态的线程),调用interrupt
会抛出InterruptException
异常; - 二是打断正常的线程,可根据线程的打断状态标记是否退出线程,与第一种使用退出标志的方式类似。
- 一是打断阻塞的线程(如处于
- 使用退出标志:通过定义一个标志变量(如