vlambda博客
学习文章列表

面试7. 线程池用过么?

线程池的优势

线程池做的工作主要是控制运行的线程的数量,处理过程中将任务放入队列,然后在线程创建后启动这些任务,如果线程数量超过了最大数量,超出的线程排队等候,等其他线程执行完毕,再从队列中取出任务来执行

主要特点:线程复用,控制最大并发数,管理线程

核心都是ThreadPoolExecutor

三种线程池:newFixedThreadPool():传固定值的线程池,适合长期的任务,性能好

newSingleThreadExecutor():固定一个线程,适合依次执行任务的场景,一个一个执行任务的场景

newCachedThreadPool():可缓存线程池,执行很多短期异步的小程序或负载较轻的任务

public static void main(String[] args) {
//ExecutorService executorService = Executors.newFixedThreadPool(5);//一池5线程
//ExecutorService executorService = Executors.newSingleThreadExecutor();//一池1线程
ExecutorService executorService = Executors.newCachedThreadPool();//一池N线程,具体不确定,看处理情况
try {
for (int i = 0; i < 10; i++) {
int j = i;
executorService.execute(()->{
System.out.println(Thread.currentThread().getName() + "\t" + j);
});
}
} catch (Exception e) {
e.printStackTrace();
} finally {
executorService.shutdown();
}
}

源码可以看到:底层是ThreadPoolExecutor和阻塞队列LinkedBlockingQueue和同步队列SynchronousQueue

线程池的参数

public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
{

7个参数

  • corePoolSize:核心线程数

  • maximumPoolSize:最大线程数

  • keepAliveTime:多余的空闲线程的存活时间

  • unit:keepAliveTime单位

  • workQueue:阻塞队列

  • threadFactory:线程工厂,使用默认的

  • handler:拒绝策略

线程池 == 银行网点

核心线程数:今日当值的受理窗口

最大线程数:最多的受理窗口

阻塞队列:银行的候客区

线程工厂:银行的logo,工作人员的胸卡,使用默认的

拒绝策略:如果最大线程数满了,阻塞队列也满了,就拒绝客户进入到网点了

如果核心线程数都用了,阻塞队列也满了,就扩大到最大线程数,处理完,如果有空闲,超过keepAliveTime就销毁掉多的线程,到核心线程数就行

线程池的工作原理

  1. 在创建线程池后,等待提交过来的任务请求

  2. 当调用了execute()方法添加一个请求任务时,线程池会做出如下判断

  1. 如果正在运行的线程数量小于corePoolSize,那么创建线程运行这个任务

  2. 如果正在运行的线程数量大于或等于corePoolSize,那么将这个任务方法队列

  3. 如果这个时候任务队列满了且正在运行的线程数小于MaximumPoolSize,那么创建非核心线程运行任务

  4. 如果队列满了,且当前线程数大于等于maximumPoolSize,那么线程池会启动拒绝策略来执行

  1. 当一个线程完成任务时,它会从队列中取出下一个任务来执行

  2. 当一个线程无事可做,超过一定的时间,线程池会判断

如果当前运行线程数大于corePoolSize,那么这个线程会停掉,会最终收缩到corePoolSize的大小

线程池的拒绝策略

默认是抛异常

AbortPolicy(默认):直接抛出RejectedExecutionException异常,阻止系统正常运行

CallerRunsPolicy:"调用者运行"一种调节机制,该策略不会抛弃任务,也不会抛异常,而是将某些任务回退到调用者,从而降低新任务的流量

DiscardOldestPolicy:抛弃队列中等待最久的任务,然后把当前任务加入队列中尝试再次提交当前任务

DiscardPolicy:直接丢弃任务,不处理也不抛异常,如果允许任务丢失,这是最好的一种方案

这几种策略,都实现了RejectedExecutionHandler接口,可以自定义拒绝策略

//自定义拒绝策略,抛异常
private static class CustomRejectPolicy implements RejectedExecutionHandler {
@Override
public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
throw new RejectedExecutionException("异步工具类task满了,抛出异常拒绝执行任务! " + r.toString() +
" rejected from " +
executor.toString());
}
}

生产中的线程池的使用

private static int CORE_POOL_SIZE = Runtime.getRuntime().availableProcessors();//核心线程数:cpu核数
private static int MAX_POOL_SIZE = Runtime.getRuntime().availableProcessors() * 2;//最大线程数:cpu核数*2
private static ThreadPoolExecutor EXECUTOR_SERVICE = new ThreadPoolExecutor(CORE_POOL_SIZE,
MAX_POOL_SIZE,
60L,
TimeUnit.SECONDS,
new ArrayBlockingQueue<Runnable>(40000*MAX_POOL_SIZE),
Executors.defaultThreadFactory(),
new CustomRejectPolicy());

实际工作中使用的是架构师写的线程池工具类。因为那几种线程池的创建,阻塞队列的最大值是Integer.MAX_VALUE,会使堆塞满了,这样会导致OOM异常

测试拒绝策略

public static void main(String[] args) {
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(2,5,2L, TimeUnit.SECONDS,new ArrayBlockingQueue<>(3),
Executors.defaultThreadFactory(),
//new ThreadPoolExecutor.AbortPolicy()//默认的,会抛异常
//new ThreadPoolExecutor.CallerRunsPolicy()//回退给调用线程
//new ThreadPoolExecutor.DiscardOldestPolicy()//抛弃最久的任务
new ThreadPoolExecutor.DiscardPolicy()//直接抛弃任务 );
try {
for (int i = 0; i <= 20; i++) {
int j = i;
threadPoolExecutor.execute(()->{
System.out.println(Thread.currentThread().getName() + "\t" +j);
});
}
} catch (Exception e) {
e.printStackTrace();
} finally {
threadPoolExecutor.shutdown();
}
}

线程池配置合理线程数

确定最大线程数的标准

cpu密集型:任务需要大量的运算,没有阻塞,cpu全速运行,尽可能减少线程数

一般公式:cpu核数+1

IO密集型:有大量的阻塞,有两种配置方式

(1)cpu核数*2

(2)cup核数 /1-阻塞系数 (阻塞系统在0.8-0.9之间)8核cpu的话,8/1-0.9=80个线程数