JUC相关面试题
谈谈什么是线程池?
线程池和数据库连接池非常类似,可以统一管理和维护线程,减少没必要的系统开销。
为什么要使用线程池?
因为频繁地开启或者停止线程,线程需要重新被CPU从就绪到运行状态调度,需要发生CPU的上下文切换,效率非常低。
你们哪些地方会使用到多线程?
实际开发项目中,禁止自己new线程。
必须使用线程池来维护和创建线程。
线程池有哪些作用?
核心点:复用机制 -- 提前创建好固定的线程一直保持在运行状态,从而实现复用,限制线程创建的数量。
1.降低资源消耗:通过池化技术重复利用自己创建的线程,降低线程创建和消耗造成的损耗。
2.提高响应速度:任务到达时无需等待线程创建即可立即执行。
3.提高了线程的可管理性:线程是稀缺资源,如果无限制创建,不仅仅会消耗系统资源,还会因为线程的不合理分布导致资源调度失衡,降低系统的稳定性。使用线程池可以统一地分配、调优和监控。
4.提供更多更强大的功能:线程池具备可扩展性,允许开发人员向其中增加更多的功能。比如延时定时线程池ScheduledThreadPoolExecutor,就允许任务延期执行或者是定时执行。
线程池的创建方式有哪些?
Executors.newCachedThreadPool();可缓存线程池
Executors.newFixedThreadPool();可定长度 限制了最大线程数
Executors.newScheduledThreadPool();可定时线程池
Executors.newSingleThreadExecutor();单例
底层都是基于ThreadPoolExceutor构造函数封装
线程池是如何实现复用的?
本质思想:创建一个线程,不会立马停止或者销毁,而是一直实现复用。
1.提前创建好固定大小的线程,一直保持着正在运行状态;(但是这也可能会非常消耗CPU资源)
2.当需要线程执行任务,将该任务提交缓存在并发队列中;如果缓存队列满了,则会执行拒绝策略。
3.正在运行的线程从并发队列中获取任务执行,从而实现多线程复用问题。
线程池的核心点:复用机制 ---
1.提前创建好固定的线程一直在运行状态----死循环实现
2.提交的线程任务缓存到一个并发队列集合中,交给我们正在运行的线程执行
3.正在运行的线程就从队列中获取该任务执行
package com.mayikt;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.BlockingDeque;
import java.util.concurrent.LinkedBlockingDeque;
/**
* 线程池核心点:复用机制 ----
* 1.提前创建好固定的线程,一直处于运行状态----死循环实现
* 2.提交的线程任务缓存到一个并发队列集合中,交给我们正在运行的线程执行
* 3.正在运行的线程就从队列中获取该任务执行
*/
public class MyExecutors {
// 线程并发队列集合
private List<WorkThread> workThreads;
// 缓存我们线程任务
private BlockingDeque<Runnable> runnableDeque;
private boolean isRun = true;
/**
* @param maxThreadCount 最大线程数
*/
public MyExecutors(int maxThreadCount, int dequeSize) {
//1.限制队列容量缓存
runnableDeque = new LinkedBlockingDeque<Runnable>(dequeSize);
//2.提前创建好固定的线程,一直处于运行状态----死循环实现
workThreads = new ArrayList<WorkThread>(maxThreadCount);
for (int i = 0; i < maxThreadCount; i++) {
new WorkThread().start();
}
}
class WorkThread extends Thread {
@Override
public void run() {
while (isRun || runnableDeque.size() > 0) {
Runnable runnable = runnableDeque.poll();
if (runnable != null) {
runnable.run();
}
}
}
}
public boolean execute(Runnable command) {
return runnableDeque.offer(command);
}
public static void main(String[] args) {
MyExecutors myExecutors = new MyExecutors(2, 20);
for (int i = 0; i < 10; i++) {
final int finalI = i;
myExecutors.execute(() -> System.out.println(Thread.currentThread().getName() + "," + finalI));
}
myExecutors.isRun = false;
}
}
ThreadPoolExecutor核心参数有哪些?
corePoolSize:核心线程数量--- 一直正在保持运行的线程的数量
maximumPoolSize:最大线程数,线程池允许创建的最大线程数
keepAliveTime:超出corePoolSize后创建的线程的存活时间
unit:keepAliveTime的时间单位
workQueue:任务队列,用于保存待执行的任务
threadFactory:线程池内部创建线程所用的工厂
handler:任务无法执行时的处理器
线程池创建的线程会一直保持为运行状态吗?
不会的。
例如:配置核心线程数corePoolSize为2,最大线程数maximumPoolSize为5
我们可以通过配置超出corePoolSize核心线程数后创建的线程的存活时间,例如为60s
在60s内,如果核心线程一直没有任务执行,则会停止该线程。
为什么阿里巴巴不建议使用Executors?
因为默认的Executors线程池底层是基于ThreadPoolExeutor构造函数封装的,采用无界队列存放缓存任务,这样会导致由于可无限制地缓存任务从而发生内存溢出,最终导致最大线程数失效。
线程池底层ThreadPoolExecutor底层实现原理?
1.当线程数小于核心线程数时,创建线程。
2.当线程数大于等于核心线程数,且任务队列未满时,将任务放入任务队列
3.当线程数大于等于核心线程数,且任务队列已满时
3.1若线程数小于最大线程数,创建线程
3.2若线程数等于最大线程数,抛出异常,拒绝任务
到底能执行多少个任务:公式:任务队列容量+最大线程数=真实能执行的任务量
线程池队列满了,任务是否会丢失?
如果队列满了,且任务总数>最大线程数,则当前线程走拒绝策略。
可以自定义拒绝异常,将该任务缓存到redis、本地文件、MySQL中后期项目启动实现补偿。
1.AbortPolicy 丢弃任务,抛运行时异常
2.CallerRunsPolicy 执行任务
3.DiscardPolicy 忽视,什么都不会发生
4.DiscardOldestPolicy 从队列中剔除最先进入队列(最后一个执行)的任务
5.实现RejectedExecutionHandler接口,可自定义处理器
线程池拒绝策略类型有哪些呢?
1.AbortPolicy 丢弃任务,抛运行时异常
2.CallerRunsPolicy 执行任务
3.DiscardPolicy 忽视,什么都不会发生
4.DiscardOldestPolicy 从队列中剔除最先进入队列(最后一个执行)的任务
5.实现RejectedExecutionHandler接口,可自定义处理器
线程池如何合理配置参数?
自定义线程池就需要我们自己配置最大线程数maximumPoolSize,为了高效地并发运行,当然这个不能够随便设置。这时需要看我们的业务是IO密集型还是CPU密集型。
CPU密集型
CPU密集的意思是该任务需要大量的运算,而没有阻塞,CPU一直全速运行。CPU密集任务只有在真正的多核CPU上才可能得到加速(通过多线程),而在单核CPU上,无论你开几个模拟器的多线程任务都不可能得到加速,因为CPU总的运算能力就那些。
CPU密集型任务配置尽可能少的线程数量:这是为了以保证每个CPU高效地运行一个线程。
一般公式:(CPU核数+1)个 线程的线程池
IO密集型
IO密集型,即该任务需要大量的IO,即大量的阻塞。在单线程上运行IO 密集型的任务会导致浪费大量的CPU运算能力在等待。所以在IO密集型任务中使用多线程可以大大地加速程序运行,即使在单核CPU上,这种加速主要就是利用了被浪费掉的阻塞时间。
IO密集型时,大部分线程都阻塞,故需要配置多线程数:
公式:
CPU核数×2
CPU核数÷(1-阻塞系数),阻塞系数在0.8~0.9之间
查看CPU核数:
System.out.println(Runtime.getRuntime().availableProcessors());
什么是悲观锁?什么是乐观锁?
悲观锁:
1.站在MySQL的角度分析:悲观锁就是比较悲观,当多个线程对同一行数据实现修改的时候,最后只有一个线程才能修改成功,只要谁能够对获取到行锁则其他线程是不能够对该数据做任何修改操作,且是阻塞状态。
2.站在Java锁层面,如果没有获取到锁,则会阻塞等待,后期唤醒的锁的成本就会非常高,重新被我们额CPU从就绪调度为运行状态。
Lock syn锁 悲观锁没有获取到锁的线程会阻塞等待。
乐观锁:
乐观锁比较乐观,通过预期或者版本号比较。如果不一致的情况则通过循环控制修改,当前线程不会被阻塞,是乐观,效率较高,但是乐观锁比较消耗CPU的资源。
乐观锁:获