并发编程面试题
目录
1.什么是多线程?
多线程是指程序中包含多个执行流,即在一个程序中可以同时运行多个不同的线程来执行不同的任务, 也就是说允许单个程序创建多个并行执行的线程来完成各自的任务。
2.开发过程中什么时候会用到多线程?
异步处理的时候需要多线程。
场景:记录日志、发短信。
- 发短信:app或网站发送消息提醒等场景。
如:在营销活动中,用户中奖了,需要短信提醒。可以将发短信的业务放入到另外一个线程中执行,用户晚一会收到短信对整体的业务流程也不会受到影响,反而提升了用户体验。 - 推送场景:
如:有一个业务场景,有一个报表监管系统,当收到数据之后,需要将这些数据发送给第三方的监管系统,数据量有百万之多,一条数据按照一秒计算,那需要经过百万秒,200多个小时才能处理完,考虑引入多线程进行并发操作,降低数据推送时间,提高数据推送的实时性。
3.java创建线程的几种方式
(1)继承Thread类
(2)实现Runnable接口
(3)实现Callable接口
通过ExecutorService和Callable实现有返回值的线程
(4)基于线程池创建线程
3.1继承Thread类创建线程
Thread类实现了Runnable接口并定义了操作线程的一些方法,我们可以通过继承Thread类的方式创建一个线程。
(1)具体的实现过程:创建一个类并继承Thread类,然后实例化线程对象并调用start方法启动线程。start方法是一个native方法,通过在操作中系统上启动一个新线程,并最终执行run方法来启动一个线程。run方法内的代码是线程类的具体实现逻辑。
(2)具体的实现代码:
//1.通过继承Thread类创建NewThread线程
public class NewThread extends Thread{
@Override
public void run(){
System.out.println("create a thread by extends Thread");
}
}
//2.实例化一个NewThread线程对象
NewThread newThread=new NewThread();
//3.调用start方法启动NewThread线程
newThread.start();
以上的代码定义了一个名为NewThread的线程类,该类继承了Thread,run方法内的代码为县城的具体执行逻辑,在使用该线程时新建一个该线程的对象并调用气start方法即可。
3.2实现Runnable接口创建线程
基于Java编程规范,如果子类已经继承(extends)了一个类的话就不能在直接继承Thread类了。此时可以通过实现Runnable接口创建线程。
(1)具体实现过程:通过实现Runnable接口创建ChildrenClassThread线程,实例化名称为childrenThread的线程实例,创建Thread类的实例并传入childrenThread的线程实例,调用线程的start方法启动线程。
(2)具体实现代码:
//1.通过事先Runnable接口创建ChildrenClassThread线程
public class ChildrenClassThread extends SuperClass implements Runnable{
@Override
public void run(){
System.out.println("create a thread by implements Runnable");
}
//2.实例化一个ChildrenClassThread对象
ChildrenClassThread childrenClassThread=new ChildrenClassThread();
//3.创建一个线程对象并为其传入已经实例化好的childrenClassThread的线程实例
Thread thread=new Thread(childrenClassThread);
//4.调用start方法启动线程
thread.start();
事实上,在传入一个实现了Runnable的线程实例target给Thread后,Thread的run方法在执行时就会调用target.run方法并执行该线程具体的实现逻辑,在JDK源码中run方法的实现代码如下:
@Override
public void run(){
if(target!=null){
target.run();
}
3.3通过ExecutorService和Callable接口实现有返回值的线程
当我们需要在主线程中开启多个子线程并发执行一个任务,然后收集各个线程返回的接货并将最终结果汇总起来,这时就需要用到Callable接口。
(1)具体的实现过程:
创建一个类并实现Callable接口,在call方法中实现具体的运算逻辑并返回计算结果。具体的调用过程:创建一个线程池、一个用于接收返回结果的FutureList及Callable线程实例,使用线程池提交任务并将线程执行后的结果保存在FutureList中,在线程执行结束后遍历FutureList中的Future对象,在该对象上调用get方法就可以获取Callable线程任务返回的数据并汇总结果。
(2)具体的实现代码:
//1.通过实现Callable接口创建MyCallable线程
public class MyCallable implements Callable<String>{
private String name;
//通过构造函数为线程传递参数,以定义线程的名称
public MyCallable(String name){
this.name=name;
}
@Override
public String call()throw Exception{
//call方法内为线程的实现逻辑
return name;
}
//2.创建一个固定大小为5的线程池
ExecutorService pool=ExecutorService.newFixedThreadPool(5);
//3.创建多个有返回值的任务列表List
List<Future> list=new ArrayList<Future>();
for(int i=0;i<5;i++){
//4.创建一个有返回值的线程实例
Callable c=new MyCallable(i+ " ");
//5.提交线程,获取Future对象并将其保存到FutureList中
Future future=pool.submit(c);
System.out.println("submit a callable thread:" +i);
list.add(future);
}
//6.关闭线程池,等待线程执行结束
pool.shutdown();
//7.遍历所有的线程运行结果
for(Future future:list){
//从Future对象上获取任务的返回值,并将结果输出到控制台
System.out.println("get the result from callable thread:"+f.get().toString());
}
3.4基于线程池
线程是非常宝贵的计算资源,在每次需要创建并运行结束后销毁是非常浪费系统资源的。我们可以使用缓存策略并通过线程池来创建线程。
(1)具体实现过程:创建一个线程池并用该线程池提交线程任务。
(2)具体实现代码:
//1.创建大小为10的线程池
ExecutorService threadPoll=ExecutorService.newFixedThreadPool(10);
for(int i=0;i<10;i++){
//2.提交多个线程任务并执行
threadPoll.execute(new Runnable){
@Override
public void run(){
System.out.println(Thread.currentThread().getName + "is running");
});
}
}
4.Thread类中的start()和run()有什么区别?
(1)通过调用Thread类的start()方法来启动一个线程,这时此线程是处于就绪状态,并没有运行。
(2)run方法是thread的一个普通方法调用。
(3)调用start方法后,一旦得到cpu时间片,就开始执行run()方法。
5.使用线程池的优势是什么?
(1)降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
(2)提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。
(3)提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。
6.线程池的原理
JVM先根据用户的参数创建一定数量的可运行的线程任务,并将其放入队列中,在线程创建后启动这些任务,如果正在运行的线程数量超过了最大线程数量(用户设置的线程池大小),则超出数量的线程排队等候,再有任务执行完毕后,线程池调度器会发现可用的线程,进而再次从队列中取出任务并执行。
7.线程池的主要作用
(1)线程复用(高效)
(2)线程资源管理
(3)控制操作系统的最大并发数(安全)
以保证系统高效且安全的运行。
8.线程复用怎么实现的?(可简化回答)
在Java中,每个Thread类中都有一个start方法。在程序调用start方法启动线程时,Java虚拟机会调用该类的run方法。在Thread类的run方法中其实调用了Runnable对象的run方法,因此可以继承Thread类,在start方法中的不断循环调用传递进来的Runnable对象,程序就会不断执行run方法中的代码。可以将循环方法中不断获取的Runnable对象存放在队列中,当前线程获取下一个Runnable对象之前可以是阻塞的,这样既能有效控制正在执行的线程个数,也能保证系统中正在等待执行的其他线程有序执行。就简单实现了一个线程池,达到了线程复用的效果。
9.线程池的核心组件有哪些?
Java线程池主要有四个核心组件。
(1)线程池管理器:用于创建并管理线程池。
(2)工作线程:线程池中执行具体任务的线程。
(3)任务接口:用于定义工作线程的调度和执行策略,只有线程实现了该接口,线程中的任务才能够被线程池调度。
(4)任务队列:存放待处理的任务,新的任务将不断被加入队列中,执行完成的任务将被从队列中移除。
10.线程池的核心类
Java中的线程池是通过Executor框架实现的,在该框架中用到了
Executor、Executors、ExecutorService、ThreadPoolExecutor、Callable、Future、FutureTask这几个核心类。
11.构建线程的核心方法及七大参数
(1)核心方法:
public ThreadPoolExecutor(int corePoolSize,int maximumPollSize,long keepAliveTime,TimeUnit unit,BlockingQueue<Runnable> workQueue){
this(corePoolSize,maximumPollSize,keepAliveTime,unit,workQueue,Executors.defaultThreadFactory,defaultHandler);
}
(2)七大参数:
序号 | 参数 | 说明 |
---|---|---|
1 | corePoolSize | 线程池中核心线程数量 |
2 | maximumPollSize | 线程池中最大线程数量 |
3 | keepAliveTime | 当前线程数量超过corePoolSize(核心线程数量)时,空闲线程的存活时间 |
4 | unit | keepAliveTime(空闲线程存活时间)的单位 |
5 | workQueue | 任务队列,被提交但尚未被执行的任务存放的地方 |
6 | threadFactory | 线程工厂,用于创建线程,可使用默认的线程工厂或自定义线程工厂 |
7 | handler | 由于任务过多或其他原因导致线程池无法处理是的任务拒绝策略 |
12.Java线程池的工作流程
Java线程池的工作流程:线程池刚被创建时,只是向系统申请一个用于执行线程队列和管理线程池的线程资源。在调用execute()添加一个任务时,线程池会按照如下流程执行任务。
工作流程:
(1)如果正在执行的线程数量少于corePoolSize(用户定义的核心线程数量),线程池就会立刻创建线程并执行该线程任务。
(2)如果正在运行的线程数量大于或等于corePoolSize用户定义的核心线程数量),该任务就将被放入阻塞队列中。
(3)在阻塞队列已满且正在运行的线程数量少于maximumPollSize时,线程池会创建非核心线程立刻执行该线程任务。
(4)在阻塞队列已满且正在运行的线程数量大于或等于maximumPollSize时,线程池将拒绝执行该线程任务并抛出RejectExecutionException异常。
(5)在线程任务执行完毕后,该任务将被从线程池队列中移除,线程池将从队列中取下一个线程任务继续执行。
(6)在线程处于空闲状态的时间超过keepAliveTime(线程存活)时间时,正在运行的线程数量超过corePoolSize(核心线程数量),该线程将会被认定为空闲线程并停止。因此在线程池中的所有线程任务都执行完毕后,线程池会收缩到corePoolSize大小。
图例说明:
若图片不可见则访问地址:https://www.processon.com/view/link/64786dc8b463350a792dc4de
13.线程池的拒绝策略
四种拒绝策略
AbortPolicy 直接丢弃任务,抛出RejectedExecution异常,是默认策略
DiscardPolicy 直接丢弃任务,但不抛出异常
DiscardOldestPolicy 丢弃等待队列中最旧的任务,并执行当前任务
CallerRunsPolicy 用调用者所在的线程处理任务
14.进程和线程的区别?
(1)本质区别:进程是操作系统资源分配的基本单位,而线程是处理器任务调度和执行的基本单位。
(2)包含关系:一个进程至少有一个线程,线程是进程的一部分,所以线程也被称为轻权进程或者轻量级进程。
(3)资源开销:每个进程都有独立的地址空间,进程之间的切换会有较大的开销;线程可以看做轻量级的进程,同一个进程内的线程共享进程的地址空间,每个线程都有自己独立的运行栈和程序计数器,线程之间切换的开销小。
(4)影响关系:一个进程崩溃后,在保护模式下其他进程不会被影响,但是一个线程崩溃可能导致整个进程被操作系统杀掉,所以多进程要比多线程健壮。
15.五种常用的线程池?
(1)newCachedThreadPool:缓存线程池
(2)newFixedThreadPool:固定数量的线程池
(3)newScheduledThreadPool:可定时调度的线程池
(4)newSingleThreadExecutor:保证线程池中有且只有一个可用的线程
(5)newWorkStealingPool:创建持有足够现成的线程池
16.线程的生命周期
线程的生命周期有六种状态:新建、可运行、阻塞、等待、超时等待、终止。
17.线程的几种状态解释?
(1)新建(New):调用new方法新建了一个线程,这时线程处于新建状态。
(2)可运行(Runnable):调用start方法启动了一个线程,这时线程处于可运行状态。
可运行状态又分就绪(Ready)和运行中(Running)两种状态。处于就绪状态的线程等待线程获取CPU资源,在获取CPU资源后,线程会调用run方法进入运行中状态;处于运行中状态的线程在调用yeild方法或丢失处理器资源时,会再次进入就绪状态。
(3)阻塞(Blocked):处于运行中状态的线程在执行sleep方法、I/O阻塞、等待同步锁、等待通知、suspend方法等后,会挂起并进入阻塞状态。
处于阻塞状态的线程由于出现sleep时间已到、I/O方法返回、获得同步锁、收到通知、调用resume方法等情况,会再次进入可运行状态中的就绪状态,继续等待CPU时间片的轮询。该线程在获取CPU资源后,会再次进入运行中状态。
(4)等待(Waiting):线程在调用Object.wait()、Object.join()、LockSupport.park()后会进入等待状态。
处于等待状态的线程在调用Object.notify()、Object.notifyAll()、LockSupport.unpark(Thread)后会再次进入可运行状态。
(5)超时等待(Timed_Waiting):处于可运行状态额线程在调用Thread.sleep(long)、Object.wait(long)、Thread.join(long)、LockSupport.parkNanos()、LockSupport.parkUntil()后会进入超时等待状态。
处于超时等待状态的县城出现超时时间到、等待进入synchronized方法、等待进入synchronized块或者调用Object.notify()、Object.notifyAll()、LockSupport.unpark(Thread)后会再次进入可运行状态。
(6)终止(Terminated):处于可运行状态的线程,在调用run方法或call方法正常执行完成、调用stop方法停止线程或者程序执行错误导致异常退出时,会进入终止状态。
线程的生命周期图例;
若图片不可见则访问地址:https://www.processon.com/view/link/64799d1197dfee0f88682689
18.线程的基本方法
线程相关的基本方法有wait、notify、notifyAll、setDaemon、sleep、join、yield、interrupt等这些方法控制线程的运行并影响线程的状态变化。
19.sleep和wait的区别
(1)sleep方法属于Thread类,wait方法属于Object类。
(2)sleep方法暂停执行指定的时间,让出CPU给其他线程,但其监控状态依然保持,在指定的时间过后又会自动恢复运行状态。
(3)在调用sleep方法时,线程不会释放对象锁。
(4)在调用wait方法时,线程会放弃对象锁,进入等待锁池。只有针对此对象调用notify方法后,该线程才能进入对象锁池准备获取对象锁,并进入运行状态。
20.start和run方法的区别
【 线程状态:新建、就绪、阻塞、等待、超时等待、终止】。
(1)start方法用于启动线程,真正实现了多线程运行。在调用了线程的start方法后,线程会在后台执行,无需等待run方法体的代码执行完毕,就可以继续执行下面的代码。
(2)在通过调用Thread类的start方法启动一个线程时,此线程处于就绪状态,并没有在运行。
(3)run方法也叫做线程体,包含了要执行的线程的逻辑代码,在调用run方法后,线程会进入运行状态,开始运行run方法中的代码。在run方法运行结束后,该线程终止,CPU再次调度其他线程。