【变强系列】线程池考点

279 阅读7分钟

线程池

  1. 说一说几种常见的线程池及适用场景?

    线程池创建可以有2种方式,Executors或用ThreadPoolExecutor自己手动创建,其实Executos也是用ThreadPoolExecutor创建的,只不过它是自己封装了一些入参。

    public ThreadPoolExecutor(int corePoolSize,
                                  int maximumPoolSize,
                                  long keepAliveTime,
                                  TimeUnit unit,
                                  BlockingQueue<Runnable> workQueue,
                                  ThreadFactory threadFactory, //默认线程工程,新建线程
                                  RejectedExecutionHandler handler) //默认是终止的拒绝策略
    

    newCacheThreadPool:可缓存的线程池

    new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                          60L, TimeUnit.SECONDS,
                                          new SynchronousQueue<Runnable>());
    

    newScheduledThreadPool:定期执行任务的线程池

    new ScheduledThreadPoolExecutor(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
                  new DelayedWorkQueue());
    

    newFixedThreadPool:固定大小的线程池

    new ThreadPoolExecutor(nThreads, nThreads,
                                          0L, TimeUnit.MILLISECONDS,
                                          new LinkedBlockingQueue<Runnable>());
    

    newSingleThreadExecutor:单线程的线程池,可用于顺序执行

    new ThreadPoolExecutor(1, 1,
                                        0L, TimeUnit.MILLISECONDS,
                                        new LinkedBlockingQueue<Runnable>());
    

    阿里巴巴java开发规范中指出,创建线程池不允许使用Executors去创建,而是通过ThreadPoolExecutor创建,这样可以让开发同学更加明确线程池的运行规则,规避资源耗尽的风险。

    Executors创建的线程池弊端如下:

    1)CachedThreadPool和ScheduledThreadPool:允许的创建线程数量是Integer.MAX_VALUE,可能会穿就创建大量线程,导致OOM。

    2)FixedPoolExecutor和SingleThreadExecutor:允许的请求队列长度是Integer.MAX_VALUE,可能会堆积大量的请求,导致OOM。FixedPool的核心线程数比较小时,和SingleThread的核心线程数是1,不创建大量的线程,那是怎么可以堆积请求的呢?这就和工作队列有关系了。

  2. 定时任务scheduleThreadPoolExecutor

测试代码

    public static void testScheduledThreadPool() {
       ScheduledExecutorService ses = Executors.newScheduledThreadPool(3);
//        ses.scheduleAtFixedRate(new Task("myScheduledTask"),4,3,TimeUnit.SECONDS);
       ses.scheduleWithFixedDelay(new Task("myScheduledTask"),4,3,TimeUnit.SECONDS);
       System.out.println("have scheduled the task " + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
       //ses.shutdown();
   }

   static class Task implements Runnable {
       private final String name;
       public Task(String name) {
           this.name = name;
       }
       @Override
       public void run() {
           System.out.println("\nstart task " + name + " " + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
           try {
               Thread.sleep(1000);
           } catch (InterruptedException e) {
               e.printStackTrace();
           }
           System.out.println("end task " + name + " " + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
       }
   }

scheduleAtFixedRate,开始任务开始算固定间隔,如果任务执行时间超过了间隔时间就马上执行。

have scheduled the task 2021-10-12 11:18:38

start task myScheduledTask 2021-10-12 11:18:42
end task myScheduledTask 2021-10-12 11:18:43

start task myScheduledTask 2021-10-12 11:18:45
end task myScheduledTask 2021-10-12 11:18:46

start task myScheduledTask 2021-10-12 11:18:48
end task myScheduledTask 2021-10-12 11:18:49

scheduleWithFixedDelay,结束任务开始算固定间隔,如果任务执行时间超过了间隔时间依旧延后间隔时间执行。

have scheduled the task 2021-10-12 11:21:54

start task myScheduledTask 2021-10-12 11:21:58
end task myScheduledTask 2021-10-12 11:21:59

start task myScheduledTask 2021-10-12 11:22:02
end task myScheduledTask 2021-10-12 11:22:03

start task myScheduledTask 2021-10-12 11:22:06
end task myScheduledTask 2021-10-12 11:22:07

start task myScheduledTask 2021-10-12 11:22:10
end task myScheduledTask 2021-10-12 11:22:11

为了测试任务时长超过间隔时间,可以把sleep改成5秒。

扩展:spring的定时任务实现原理?

请查看:juejin.cn/post/701850…

  1. 线程池都有哪几种工作队列?

    1)直接提交SynchronousQueue,size写死0,isEmpty()一直true

    2)有界限队列ArrayBlockingQueue,数组实现,容量固定

    3-1)无界队列也可以无界LinkedBlockingQueue,FIFO,也可以是有界限的,当构造函数传了容量大小就是有界队列【另外有个LinkedBlockingDeque双向队列线程池用不上】,如果是无界队列,maxPoolSize就没有作用了。

    3-2)无界队列PriorityBlockingQueue,Comparator规则,数组实现,初始化容量大小11,可扩容

  2. 高并发、任务执行时间短的业务怎样使用线程池?并发不高、任务执行时间长的业务怎样使用线程池?并发高、业务执行时间长的业务怎样使用线程池?

    1)高并发、任务执行时间短: corePoolSize=CPU数量+1,减少线程上下文的切换,maxSize=尽可能大

    2)并发不高、任务执行时间长:a)如果执行时间长在IO操作上,corePoolSizek可设置大一些,让CPU处理更多的业务 ; b)如果执行时间长在计算上,没有办法了,和1)一样,尽量减少线程上下文切换。

    3)高并发、任务执长:解决这类问题关键不在怎么设置线程池的大小,而在于整体架构的设计,看看业务上是否运行做缓存,然后考虑增加服务器,最后看看能否对任务进行拆分,分解步骤,看看是否有些步骤可以放在其他地方执行。corePoolSize也是=CPU数量+1。

    4)并发不高、任务执行时间短:用不用线程关系不大。

  3. 线程池的拒绝策略

    JDK提供了4个,默认是终止策略

    AbortPolicy终止策略,抛出异常,终止流程。

    DiscardPolicy静默丢弃,不抛异常

    DiscardOldestPolicy静默丢弃最早的任务,执行最新的任务,不抛异常;适用于老的任务是可以丢弃的情况,旧的任务没处理完,新的任务又来了,旧的任务丢弃不会造成影响。

    CallerRunsPolicy调用者运行策略,适用于不允许失败,对性能要求不高,并发量较小的情况,当多次提交任务时,阻塞后续任务执行,性能和效率会下降。

    三方策略

    1)打印线程池当前状态日志、打印堆栈

    2)新起线程,可能会耗尽系统资源

    3)责任链模式,可以依次执行多个拒绝策略。

    问:线程执行过程中抛异常了,后续的线程是否还能继续执行?

    答:如果是带返回值的,后续线程会被中断;如果是不带返回值的,后续线程不会被中断。

    问:使用的是AbortPolicy,触发拒绝策略时,后续的线程还会继续执行吗?

    答:不会。

    问:什么情况下才会触发拒绝策略呢?

    答:

    当提交的线程数量超过核心线程数,并且超过阻塞队列的大小,且超过最大线程数量就会触发拒绝策略。

  4. 新建线程的过程是怎么样的?

7.线程池监控

/**
 * 该类继承ThreadPoolExecutor类,覆盖了shutdown(), shutdownNow(), beforeExecute() 和 afterExecute()
 * 方法来统计线程池的执行情况
 *
 */
public class ExecutorsUtil extends ThreadPoolExecutor {

    private static final Logger LOGGER = LoggerFactory.getLogger(ExecutorsUtil.class);

    // 保存任务开始执行的时间,当任务结束时,用任务结束时间减去开始时间计算任务执行时间
    private ConcurrentHashMap<String, Date> startTimes;

    // 线程池名称,一般以业务名称命名,方便区分
    private String poolName;

    /**
     * 调用父类的构造方法,并初始化HashMap和线程池名称
     *
     * @param corePoolSize
     *            线程池核心线程数
     * @param maximumPoolSize
     *            线程池最大线程数
     * @param keepAliveTime
     *            线程的最大空闲时间
     * @param unit
     *            空闲时间的单位
     * @param workQueue
     *            保存被提交任务的队列
     * @param poolName
     *            线程池名称
     */
    public ExecutorsUtil(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue,
                         String poolName) {
        super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, new EventThreadFactory(poolName));
        this.startTimes = new ConcurrentHashMap<>();
        this.poolName = poolName;
    }

    /**
     * 线程池延迟关闭时(等待线程池里的任务都执行完毕),统计线程池情况
     */
    @Override
    public void shutdown() {
        // 统计已执行任务、正在执行任务、未执行任务数量
        LOGGER.info(String.format(this.poolName + " Going to shutdown. Executed tasks: %d, Running tasks: %d, Pending tasks: %d",
                this.getCompletedTaskCount(), this.getActiveCount(), this.getQueue().size()));
        super.shutdown();
    }

    /**
     * 线程池立即关闭时,统计线程池情况
     */
    @Override
    public List<Runnable> shutdownNow() {
        // 统计已执行任务、正在执行任务、未执行任务数量
        LOGGER.info(
                String.format(this.poolName + " Going to immediately shutdown. Executed tasks: %d, Running tasks: %d, Pending tasks: %d",
                        this.getCompletedTaskCount(), this.getActiveCount(), this.getQueue().size()));
        return super.shutdownNow();
    }

    /**
     * 任务执行之前,记录任务开始时间
     */
    @Override
    protected void beforeExecute(Thread t, Runnable r) {
        startTimes.put(String.valueOf(r.hashCode()), new Date());
    }

    /**
     * 任务执行之后,计算任务结束时间
     */
    @Override
    protected void afterExecute(Runnable r, Throwable t) {
        Date startDate = startTimes.remove(String.valueOf(r.hashCode()));
        Date finishDate = new Date();
        long diff = finishDate.getTime() - startDate.getTime();
        // 统计任务耗时、初始线程数、核心线程数、正在执行的任务数量、已完成任务数量、任务总数、队列里缓存的任务数量、池中存在的最大线程数、最大允许的线程数、线程空闲时间、线程池是否关闭、线程池是否终止
        LOGGER.info(String.format(this.poolName
                        + "-pool-monitor: Duration: %d ms, PoolSize: %d, CorePoolSize: %d, Active: %d, Completed: %d, Task: %d, Queue: %d, LargestPoolSize: %d, MaximumPoolSize: %d,  KeepAliveTime: %d, isShutdown: %s, isTerminated: %s",
                diff, this.getPoolSize(), this.getCorePoolSize(), this.getActiveCount(), this.getCompletedTaskCount(), this.getTaskCount(),
                this.getQueue().size(), this.getLargestPoolSize(), this.getMaximumPoolSize(), this.getKeepAliveTime(TimeUnit.MILLISECONDS),
                this.isShutdown(), this.isTerminated()));
    }
}

8.线程池是怎么复用线程的?又是怎么回收线程的?

复用:参考

通过取 Worker 的 firstTask 或者通过 getTask 方法从 workQueue 中不停地取任务,并直接调用 Runnable 的 run 方法来执行任务,这样就保证了每个线程都始终在一个循环中,反复获取任务,然后执行任务。

回收:参考

在getTask()返回null时,会从workQuery remove线程进行回收,getTask返回空的情况有2种情况:

工作线程数大于核心线程数、从workQuery阻塞队列中获取超时了,这个超时时间就是构造函数入参的keepAliveTime,当变量allowCoreTimeOut设置true时核心线程也会被回收。

参考:实现原理及美团业务实战