Java并发的那些事儿(四)之线程池的使用及原理

238 阅读4分钟

这是我参与11月更文挑战的第11天,活动详情查看:2021最后一次更文挑战

JDK的线程池的标准实现一般有3种:

  • java.util.concurrent.ForkJoinPool (”窃取“任务,一定场景下高吞吐量)
  • java.util.concurrent.ScheduledThreadPoolExecutor (定时任务,异步的线程定时执行)
  • java.util.concurrent.ThreadPoolExecutor (普通的线程池)

线程池的创建可以通过ThreadPoolExecutor的方法实现。

g422lD.png

线程池的几个参数

  • corePoolSize :核心线程数;
  • maximumPoolSizeL:最大线程数;
  • keepAliveTime:最大线程数可以存活的时间,当线程中没有任务执行时,最大线程就会销毁一部分,最终保持核心线程数量的线程,也即空闲线程的存活时间;
  • TimeUnit:等待时间的单位;
  • BlockingQueue:堵塞队列,用于存储等待执行的任务;
  • ThreadFactory:线程工厂,用于生产线程;
  • RejectedExecutionHandler:拒绝策略,当线程达到最大线程数时有新的任务进来,此时所采用的拒绝策略;

执行流程

当有新的任务进来时,线程数小于核心线程数,则创建一个新的线程去处理;当线程数等于核心线程数时,会将新的任务丢到堵塞队列中等候;当堵塞队列已满,且线程数小于最大线程数,创建新的线程;当线程数等于最大线程数了,还有新任务进来,则使用拒绝策略;

堵塞队列

一个阻塞队列,用来存储线程池等待执行的任务,均为线程安全,它包含以下 7 种类型:

  • ArrayBlockingQueue:一个由数组结构组成的有界阻塞队列。
  • LinkedBlockingQueue:一个由链表结构组成的有界阻塞队列。
  • SynchronousQueue:一个不存储元素的阻塞队列,即直接提交给线程不保持它们。
  • PriorityBlockingQueue:一个支持优先级排序的无界阻塞队列。
  • DelayQueue:一个使用优先级队列实现的无界阻塞队列,只有在延迟期满时才能从中提取元素。
  • LinkedTransferQueue:一个由链表结构组成的无界阻塞队列。与SynchronousQueue类似,还含有非阻塞方法。
  • LinkedBlockingDeque:一个由链表结构组成的双向阻塞队列。

较常用的是 LinkedBlockingQueue 和 Synchronous,线程池的排队策略与 BlockingQueue 有关。

线程工厂

线程工厂,顾名思义是为生成线程的地方,采用工厂模式封装了创建线程的细节; 默认是使用Executors.DefaultThreadFactory 实现的。

 /**
     * The default thread factory
     */
    static class DefaultThreadFactory implements ThreadFactory {
        private static final AtomicInteger poolNumber = new AtomicInteger(1);   // 原子类,线程池编号
        private final ThreadGroup group;    // 线程组
        private final AtomicInteger threadNumber = new AtomicInteger(1); // 线程数量
        private final String namePrefix;    // 线程名的前缀

        DefaultThreadFactory() {
            SecurityManager s = System.getSecurityManager();
            // 获取线程组
            group = (s != null) ? s.getThreadGroup() :
                                  Thread.currentThread().getThreadGroup();
            namePrefix = "pool-" +
                          poolNumber.getAndIncrement() +
                         "-thread-";
        }

        public Thread newThread(Runnable r) {
        // 创建线程,设置线程组,名字,任务
            Thread t = new Thread(group, r,
                                  namePrefix + threadNumber.getAndIncrement(),
                                  0);
           // 设置为非守护线程                       
            if (t.isDaemon())
                t.setDaemon(false);
           // 设置为默认的优先级     
            if (t.getPriority() != Thread.NORM_PRIORITY)
                t.setPriority(Thread.NORM_PRIORITY);
            return t;
        }
    }

也可以自己实现ThreadFactory,构建创建线程的方法。

拒绝策略

JDk自带了几种默认的拒绝策略实现。

  • 默认拒绝策略(丢弃任务且抛异常):ThreadPoolExecutor.AbortPolicy
  • 最旧淘汰策略(丢弃队列中最前面的任务):ThreadPoolExecutor.DiscardOldestPolicy
  • 丢弃任务但不抛出异常: ThreadPoolExecutor.DiscardPolicy
  • 由调用线程处理该任务: ThreadPoolExecutor.CallerRunsPolicy

AbortPolicy 策略

可以看出,丢弃任务且抛出异常;

g4r0e0.png

DiscardOldestPolicy 策略

如果线程池没关,则出队队首的任务,将下一个任务提交;

g4rbSH.png

DiscardPolicy 策略

do nothing... 啥事不干,单纯丢弃任务

g4roFO.png

CallerRunsPolicy 策略

如果线程池没关,则在该线程上直接调用,即交给调用线程去处理了。

g4sV00.png

如果JDK提供的默认拒绝策略不符合业务场景,则可以直接实现RejectedExecutionHandler,重写rejectedExecution方法即可。

线程池提交任务的两种方式

Java中的线程池在进行任务提交时,有两种方式:executesubmit方法。

区别是:

  • execute只能提交Runnable类型的任务,无返回值。submit既可以提交Runnable类型的任务,也可以提交Callable类型的任务,会有一个类型为Future的返回值,但当任务类型为Runnable时,返回值为null。
  • execute在执行任务时,如果遇到异常会直接抛出,而submit不会直接抛出,只有在使用Future的get方法获取返回值时,才会抛出异常。

使用示例

测试当线程数和队列已满时,实现自定义的拒绝策略,从结果中可以看出,c任务提交时执行了拒绝策略实现的方法。如果是采用默认的拒绝策略实现,则提交c任务时会抛出异常,注意任务a和b是会继续执行的。

参考资料

www.cnblogs.com/vipstone/p/…

深入线程池原理:zhuanlan.zhihu.com/p/157116692