作者:夜幕下的猫眼_398 | 来源:互联网 | 2023-10-09 19:51
点击上方关注 “终端研发部”
大家好我是小于哥啊,最近的面试中,关于线程、多线程,线程池相关的面试题还是挺多的。今天我们来分享一下最近常问线程,多线程,以及线程池相关的面试题~
什么是多线程
多线程就是在同一时刻执行多个线程,举个简单易懂的例子,多线程相当于一条马路上的多条车道,单车道行驶车辆速度较慢,且可能产生拥堵,多车道可缓解车速、拥堵情况。
多线程不是为了提高程序的执行速度,而是为了提高程序的使用率
开发中多线程的应用场景
1、后台任务,例如:定时向大量(100w以上)的用户发送邮件;其实web服务器本身就利用的是多线程;以及各种专用服务器(如游戏服务器);创建线程的三种方式
第一种方式创建
继承Thread类,并重写Thread中的run方法第二种方式创建
继承Thread类,并重写Thread中的run方法
第三种方式创建
有返回值的任务必须实现 Callable 接口,类似的,无返回值的任务必须 Runnable 接口。执行Callable 任务后,可以获取一个 Future 的对象,在该对象上调用 get 就可以获取到 Callable 任务返回的 Object 了,再结合线程池接口ExecutorService 就可以实现传说中有返回结果的多线程了。多线程特点
代码的运行结果与代码执行顺序后调用代码的顺序是无关的
即线程是一个子任务,CPU以随机的时间来调用线程中的方法。注意:1.不能多次调用Thread中的start()方法,否则会抛出IllegalThreadStateException异常。2.启动线程的方法不是run()方法而是start方法,如果调用的是run()方法就是同步的,并不能异步执行。3.执行start()方法的顺序不代表线程启动的顺序,即并不是说,越早调用某个线程的start()方法,它就能越早的执行其中的run()方法。Callable方法是什么
Callable接口是属于Executor,对比与Runnable接口功能的区别是:(1).Callable可以在任务结束后提供一个返回值,Runnable没有这个功能(2).Callable中的call()方法可以抛出异常,而Runnable的run()方法不能抛出异常(3).运行Callable可以拿到一个Future对象,Future独享表示异步计算的结果,它提供了检查计算是否完成的方法。由于线程属于异步计算模型,因此无法从别的线程中得到函数的返回值,在这种情况下,就可以使用Future来监视目标线程调用call()方法的情况,
放调用Future的get()方法以获取结果时,当前线程就会阻塞,知道call()方法结束返回结果。用那种方式实现 多线程比较好,为什么?
(1).Thread类中定义了多种方法可以被派生类使用或重写,但是只有run()方法必须被重写的,在run()方法中实现啊这个线程的主要功能,这就是Runnable接口所需实现的方法(2).通过继承Thread的实现方法与实现Runnable接口的效果相同,并且Java只能是单继承、多实现,如果一个类中已经继承其他所需的类,那实现一个接口是必须的。线程的声明周期
当线程被创建并启动以后,它既不是一启动就进入了执行状态,也不是一直处于执行状态。在线程的生命周期中,它要经过新建(New)、就绪(Runnable)、运行(Running)、阻塞
(Blocked)和死亡(Dead)5 种状态。尤其是当线程启动以后,它不可能一直”霸占”着 CPU 独自 运行,所以 CPU 需要在多条线程之间切换,于是线程状态也会多次在运行、阻塞之间切换终止线程 4 种方式
定义了一个退出标志 exit,当 exit 为 true 时,while 循环退出,exit 的默认值为 false.在定义 exit
时,使用了一个 Java 关键字 volatile,这个关键字的目的是使 exit 同步,也就是说在同一时刻只
能由一个线程来修改 exit 的值。public class ThreadSafe extends Thread {
public volatile boolean isExit = false;
public void run() {
while (!isExit ){
to do my something
}
}
}
3、使用 interrupt()方法来中断线程有两种情况:如使用了 sleep,同步锁的 wait,socket 中的 receiver,accept 等方法时,会使线程处于阻塞状态。当调用线程的 interrupt()方法时,会抛出 InterruptException 异常。阻塞中的那个方法抛出这个异常,通过代码捕获该异常,然后 break 跳出循环状态,从而让我们有机会结束这个线程的执行通常很多人认为只要调用 interrupt 方法线程就会结束,实
际上是错的, 一定要先捕获 InterruptedException 异常之后通过 break 来跳出循环,才能正
常结束 run 方法用 isInterrupted()判断线程的中断标志来退出循环。当使用
interrupt()方法时,中断标志就会置 true,和使用自定义的标志来控制循环是一样的道理。
4、stop 方法终止线程(线程不安全)程序中可以直接使用 thread.stop()来强行终止线程,但是 stop 方法是很危险的,就象突然关闭计算机电源,而不是按正常程序关机一样,可能会产生不可预料的结果,不安全主要是:
thread.stop()调用之后,创建子线程的线程就会抛出 ThreadDeatherror 的错误,并且会释放子线程所持有的所有锁。一般任何进行加锁的代码块,都是为了保护数据的一致性,如果在调用thread.stop()后导致了该线程所持有的所有锁的突然释放(不可控制),那么被保护数据就有可能呈现不一致性,其他线程在使用这些被破坏的数据时,有可能导致一些很奇怪的应用程序错误。因此,并不推荐使用 stop 方法来终止线程。
线程池简介
线程池:thread pool, java.util.concurrent.Executors提供了一个 java.util.concurrent.Executor接口的实现用于创建线程池。是一种线程使用模式,线程池维护着多个线程,等待着监督管理者分配可并发执行的任务。多线程技术主要解决处理器单元内多个线程执行的问题,它可以显著减少处理器单元的闲置时间,增加处理器单元的吞吐能力。线程池是一种多线程处理形式,处理过程中将任务添加到队列,然后在创建线程后自动启动这些任务。线程池线程都是后台线程。每个线程都使用默认的堆栈大小,以默认的优先级运行,并处于多线程单元中。如果某个线程在托管代码中空闲(如正在等待某个事件),则线程池将插入另一个辅助线程来使所有处理器保持繁忙。如果所有线程池线程都始终保持繁忙,但队列中包含挂起的工作,则线程池将在一段时间后创建另一个辅助线程但线程的数目永远不会超过最大值。超过最大值的线程可以排队,但他们要等到其他线程完成后才启动。
Java中的4 种线程池
newCachedThreadPool
创建一个可根据需要创建新线程的线程池,但是在以前构造的线程可用时将重用它们。对于执行
很多短期异步任务的程序而言,这些线程池通常可提高程序性能。调用 execute 将重用以前构造
的线程(如果线程可用)。如果现有线程没有可用的,则创建一个新线程并添加到池中。终止并
从缓存中移除那些已有 60 秒钟未被使用的线程。因此,长时间保持空闲的线程池不会使用任何资
源。newFixedThreadPool
创建一个可重用固定线程数的线程池,以共享的无界队列方式来运行这些线程。在任意点,在大
多数 nThreads 线程会处于处理任务的活动状态。如果在所有线程处于活动状态时提交附加任务,
则在有可用线程之前,附加任务将在队列中等待。如果在关闭前的执行期间由于失败而导致任何
线程终止,那么一个新线程将代替它执行后续的任务(如果需要)。在某个线程被显式地关闭之
前,池中的线程将一直存在。newScheduledThreadPool
创建一个线程池,它可安排在给定延迟后运行命令或者定期地执行。newSingleThreadExecutor
Executors.newSingleThreadExecutor()返回一个线程池(这个线程池只有一个线程),这个线程池可以在线程死后(或发生异常时)重新启动一个线程来替代原来的线程继续执行下去!线程复用
每一个 Thread 的类都有一个 start 方法。当调用 start 启动线程时 Java 虚拟机会调用该类的 run方法。那么该类的 run() 方法中就是调用了 Runnable 对象的 run() 方法。我们可以继承重写Thread 类,在其 start 方法中添加不断循环调用传递过来的 Runnable 对象。这就是线程池的实现原理。循环方法中不断获取 Runnable 是用 Queue 实现的,在获取下一个 Runnable 之前可以是阻塞的。线程池的组成
- 任务接口:每个任务必须实现的接口,用于工作线程调度其运行
- 任务队列:用于存放待处理的任务,提供一种缓冲机制
Java 中的线程池是通过 Executor 框架实现的,该框架中用到了 Executor,Executors,ExecutorService,ThreadPoolExecutor ,Callable 和 Future、FutureTask 这几个类。
ThreadPoolExecutor 的构造方法如下:- corePoolSize:指定了线程池中的线程数量。
- maximumPoolSize:指定了线程池中的最大线程数量。
- keepAliveTime:当前线程池数量超过 corePoolSize 时,多余的空闲线程的存活时间,即多次时间内会被销毁。
- workQueue:任务队列,被提交但尚未被执行的任务。
- threadFactory:线程工厂,用于创建线程,一般用默认的即可。
- handler:拒绝策略,当任务太多来不及处理,如何拒绝任务。
Java 线程池工作过程
- 线程池刚创建时,里面没有一个线程。任务队列是作为参数传进来的。不过,就算队列里面有任务,线程池也不会马上执行它们。
- 当调用 execute() 方法添加一个任务时,线程池会做如下判断:
a) 如果正在运行的线程数量小于 corePoolSize,那么马上创建线程运行这个任务;
b) 如果正在运行的线程数量大于或等于 corePoolSize,那么将这个任务放入队列;
c) 如果这时候队列满了,而且正在运行的线程数量小于 maximumPoolSize,那么还是要
创建非核心线程立刻运行这个任务;
d) 如果队列满了,而且正在运行的线程数量大于或等于 maximumPoolSize,那么线程池
会抛出异常 RejectExecutionException。
- 当一个线程完成任务时,它会从队列中取下一个任务来执行。
- 当一个线程无事可做,超过一定的时间(keepAliveTime)时,线程池会判断,如果当前运
行的线程数大于 corePoolSize,那么这个线程就被停掉。所以线程池的所有任务完成后,它
最终会收缩到 corePoolSize 的大小。
线程池都有哪几种工作队列
ArrayBlockingQueue是一个基于数组结构的有界阻塞队列,此队列按 FIFO(先进先出)原则对元素进行排序。LinkedBlockingQueue一个基于链表结构的阻塞队列,此队列按FIFO (先进先出) 排序元素,吞吐量通常要高于ArrayBlockingQueue。静态工厂方法Executors.newFixedThreadPool()使用了这个队列。SynchronousQueue一个不存储元素的阻塞队列。每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于LinkedBlockingQueue,静态工厂方法Executors.newCachedThreadPool使用了这个队列。PriorityBlockingQueue一个具有优先级的无限阻塞队列。线程池原理
线程池做的工作主要是控制运行的线程的数量,处理过程中将任务放入队列,然后在线程创建后启动这些任务,如果线程数量超过了最大数量超出数量的线程排队等候,等其它线程执行完毕,再从队列中取出任务来执行。他的主要特点为:线程复用;控制最大并发数;管理线程。
BAT等大厂Java面试经验总结
想获取 Java大厂面试题学习资料
扫下方二维码回复「BAT」就好了
回复 【加群】获取github掘金交流群
回复 【电子书】获取2020电子书教程
回复 【C】获取全套C语言学习知识手册
回复 【Java】获取java相关的视频教程和资料
回复 【爬虫】获取SpringCloud相关多的学习资料
回复 【Python】即可获得Python基础到进阶的学习教程
回复 【idea破解】即可获得intellij idea相关的破解教程关注我gitHub掘金,每天发掘一篇好项目,学习技术不迷路!
回复 【idea激活】即可获得idea的激活方式
回复 【Java】获取java相关的视频教程和资料
回复 【SpringCloud】获取SpringCloud相关多的学习资料
回复 【python】获取全套0基础Python知识手册
回复 【2020】获取2020java相关面试题教程
回复 【加群】即可加入终端研发部相关的技术交流群
阅读更多
在华为鸿蒙 OS 上尝鲜,我的第一个“hello world”,起飞!
相信自己,没有做不到的,只有想不到的
在这里获得的不仅仅是技术!
如果喜欢就给个“在看”