Java线程池Executor用法详解

目录
  • 线程池类图
  • 线程池的好处
    • new Thread的弊端
  • 线程池核心类-ThreadPoolExecutor
    • 使用Executors创建线程池
    • Executors.newCachedThreadPool
    • Executors.newSingleThreadExecutor
    • Executors.newFixedThreadPool
    • Executors.newScheduledThreadPool
    • 总结
  • 如何定义线程池参数

线程池类图

我们最常使用的Executors实现创建线程池使用线程主要是用上述类图中提供的类。在上边的类图中,包含了一个Executor框架,它是一个根据一组执行策略的调用调度执行和控制异步任务的框架,目的是提供一种将任务提交与任务如何运行分离开的机制。它包含了三个executor接口:

  • Executor:运行新任务的简单接口
  • ExecutorService:扩展了Executor,添加了用来管理执行器生命周期和任务生命周期的方法
  • ScheduleExcutorService:扩展了ExecutorService,支持Future和定期执行任务

线程池的好处

  • 降低资源消耗-重用存在的线程,减少对象创建、消亡的开销,性能好
  • 提高响应速度 -可有效控制最大并发线程数,提高系统资源利用率,同时可以避免过多资源竞争,避免阻塞。当任务到达时,任务可不用等待线程创建就能立即执行
  • 提高线程的可管理性-提供定时执行、定期执行、单线程、并发数控制等功能。

new Thread的弊端

  • 每次new Thread 新建对象,性能差
  • 线程缺乏统一管理,可能无限制的新建线程,相互竞争,可能占用过多的系统资源导致死机或者OOM(out of memory 内存溢出),这种问题的原因不是因为单纯的new一个Thread,而是可能因为程序的bug或者设计上的缺陷导致不断new Thread造成的。
  • 缺少更多功能,如更多执行、定期执行、线程中断。

线程池核心类-ThreadPoolExecutor

参数说明:ThreadPoolExecutor一共有七个参数,这七个参数配合起来,构成了线程池强大的功能。

corePoolSize:核心线程数量

maximumPoolSize:线程最大线程数

workQueue:阻塞队列,存储等待执行的任务,很重要,会对线程池运行过程产生重大影响

当我们提交一个新的任务到线程池,线程池会根据当前池中正在运行的线程数量来决定该任务的处理方式。处理方式有三种:

1、直接切换(SynchronusQueue)

2、无界队列(LinkedBlockingQueue)能够创建的最大线程数为corePoolSize,这时maximumPoolSize就不会起作用了。当线程池中所有的核心线程都是运行状态的时候,新的任务提交就会放入等待队列中。

3、有界队列(ArrayBlockingQueue)最大maximumPoolSize,能够降低资源消耗,但是这种方式使得线程池对线程调度变的更困难。因为线程池与队列容量都是有限的。所以想让线程池的吞吐率和处理任务达到一个合理的范围,又想使我们的线程调度相对简单,并且还尽可能降低资源的消耗,我们就需要合理的限制这两个数量 分配技巧: [如果想降低资源的消耗包括降低cpu使用率、操作系统资源的消耗、上下文切换的开销等等,可以设置一个较大的队列容量和较小的线程池容量,这样会降低线程池的吞吐量。如果我们提交的任务经常发生阻塞,我们可以调整maximumPoolSize。如果我们的队列容量较小,我们需要把线程池大小设置的大一些,这样cpu的使用率相对来说会高一些。但是如果线程池的容量设置的过大,提高任务的数量过多的时候,并发量会增加,那么线程之间的调度就是一个需要考虑的问题。这样反而可能会降低处理任务的吞吐量。]

keepAliveTime:线程没有任务执行时最多保持多久时间终止(当线程中的线程数量大于corePoolSize的时候,如果这时没有新的任务提交核心线程外的线程不会立即销毁,而是等待,直到超过keepAliveTime)

unit:keepAliveTime的时间单位

threadFactory:线程工厂,用来创建线程,有一个默认的工场来创建线程,这样新创建出来的线程有相同的优先级,是非守护线程、设置好了名称)

rejectHandler:当拒绝处理任务时(阻塞队列满)的策略(AbortPolicy默认策略直接抛出异常、CallerRunsPolicy用调用者所在的线程执行任务、DiscardOldestPolicy丢弃队列中最靠前的任务并执行当前任务、DiscardPolicy直接丢弃当前任务)

corePoolSize、maximumPoolSize、workQueue 三者关系:如果运行的线程数小于corePoolSize的时候,直接创建新线程来处理任务。即使线程池中的其他线程是空闲的。如果运行中的线程数大于corePoolSize且小于maximumPoolSize时,那么只有当workQueue满的时候才创建新的线程去处理任务。如果corePoolSize与maximumPoolSize是相同的,那么创建的线程池大小是固定的。这时有新任务提交,当workQueue未满时,就把请求放入workQueue中。等待空线程从workQueue取出任务。如果workQueue此时也满了,那么就使用另外的拒绝策略参数去执行拒绝策略。

初始化方法:由七个参数组合成四个初始化方法

其他方法:

execute();	//提交任务,交给线程池执行
submit();//提交任务,能够返回执行结果 execute+Future
shutdown();//关闭线程池,等待任务都执行完
shutdownNow();//关闭线程池,不等待任务执行完
getTaskCount();//线程池已执行和未执行的任务总数
getCompleteTaskCount();//已完成的任务数量
getPoolSize();//线程池当前的线程数量
getActiveCount();//当前线程池中正在执行任务的线程数量

线程池生命周期:

  • running:能接受新提交的任务,也能处理阻塞队列中的任务
  • shutdown:不能处理新的任务,但是能继续处理阻塞队列中任务
  • stop:不能接收新的任务,也不处理队列中的任务
  • tidying:如果所有的任务都已经终止了,这时有效线程数为0
  • terminated:最终状态

使用Executors创建线程池

使用Executors可以创建四种线程池:分别对应上边提到的四种线程池初始化方法

Executors.newCachedThreadPool

newCachedThreadPool是一个根据需要创建新线程的线程池,当一个任务提交时,corePoolSize为0不创建核心线程,SynchronousQueue是一个不存储元素的队列,可以理解为队里永远是满的,因此最终会创建非核心线程来执行任务。 对于非核心线程空闲60s时将被回收。因为Integer.MAX_VALUE非常大,可以认为是可以无限创建线程的,在资源有限的情况下容易引起OOM异常。

//创建newCachedThreadPool线程池源码
public static ExecutorService newCachedThreadPool() {
		/**
        *corePoolSize: 0,核心线程池的数量为0
		*maximumPoolSize:  Integer.MAX_VALUE,可以认为最大线程数是无限的
		*keepAliveTime: 60L
		*unit: 秒
		*workQueue: SynchronousQueue
        **/
        return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                      60L, TimeUnit.SECONDS,
                                      new SynchronousQueue<Runnable>());
    }

使用案例:

public static void main(String[] args) {
    ExecutorService executor = Executors.newCachedThreadPool();
    for (int i = 0; i < 10; i++) {
        final int index = i;
        executor.execute(new Runnable() {
            @Override
            public void run() {
                log.info("task:{}",index);
            }
        });
    }
}

值得注意的一点是,newCachedThreadPool的返回值是ExecutorService类型,该类型只包含基础的线程池方法,但却不包含线程监控相关方法,因此在使用返回值为ExecutorService的线程池类型创建新线程时要考虑到具体情况。

Executors.newSingleThreadExecutor

newSingleThreadExecutor是单线程线程池,只有一个核心线程,用唯一的一个共用线程执行任务,保证所有任务按指定顺序执行(FIFO、优先级…)

//newSingleThreadExecutor创建线程池源码
public static ExecutorService newSingleThreadExecutor() {
    /**
      *  corePoolSize : 1,核心线程池的数量为1

      *  maximumPoolSize : 1,只可以创建一个非核心线程

      *  keepAliveTime : 0L

      *  unit => 秒

      *  workQueue => LinkedBlockingQueue
      **/
        return new FinalizableDelegatedExecutorService
            (new ThreadPoolExecutor(1, 1,
                                    0L, TimeUnit.MILLISECONDS,
                                    new LinkedBlockingQueue<Runnable>()));
    }

当一个任务提交时,首先会创建一个核心线程来执行任务,如果超过核心线程的数量,将会放入队列中,因为LinkedBlockingQueue是长度为Integer.MAX_VALUE的队列,可以认为是无界队列,因此往队列中可以插入无限多的任务,在资源有限的时候容易引起OOM异常,同时因为无界队列,maximumPoolSize和keepAliveTime参数将无效,压根就不会创建非核心线程。

Executors.newFixedThreadPool

定长线程池,核心线程数和最大线程数由用户传入,可以设置线程的最大并发数,超出在队列等待

//newFixedThreadPool创建线程池源码
public static ExecutorService newFixedThreadPool(int nThreads) {
    	/**
          *  corePoolSize : 核心线程的数量为自定义输入nThreads

          *  maximumPoolSize : 最大线程的数量为自定义输入nThreads

          *  keepAliveTime : 0L

          *  unit : 秒

          *  workQueue : LinkedBlockingQueue
          **/
        return new ThreadPoolExecutor(nThreads, nThreads,
                                      0L, TimeUnit.MILLISECONDS,
                                      new LinkedBlockingQueue<Runnable>());
    }

newFixedThreadPool和SingleThreadExecutor类似,唯一的区别就是核心线程数不同,并且由于使用的是LinkedBlockingQueue,在资源有限的时候容易引起OOM异常。

Executors.newScheduledThreadPool

定长线程池,核心线程数由用户传入,支持定时和周期任务执行

//newScheduledThreadPool创建线程池源码
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
        return new ScheduledThreadPoolExecutor(corePoolSize);
}

public ScheduledThreadPoolExecutor(int corePoolSize) {
    /**
      *  corePoolSize : 核心线程的数量为自定义输入corePoolSize

      *  maximumPoolSize : 最大线程的数量为Integer.MAX_VALUE

      *  keepAliveTime : 0L

      *  unit : 纳秒

      *  workQueue : DelayedWorkQueue
      **/
    super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
          new DelayedWorkQueue());
}

当一个任务提交时,corePoolSize为自定义输入,首先创建核心线程,核心线程满了之后,因此最终会创建非核心线程来执行任务。非核心线程使用后将被回收。因为Integer.MAX_VALUE非常大,可以认为是可以无限创建线程的,在资源有限的情况下容易引起OOM异常。因为使用的DelayedWorkQueue可以实现定时和周期任务。 ScheduledExecutorService提供了三种方法可以使用:

schedule:延迟后执行任务 scheduleAtFixedRate:以指定的速率执行任务 scheduleWithFixedDelay:以指定的延迟执行任务 使用案例:

    public static void main(String[] args) {

        ScheduledExecutorService executorService = Executors.newScheduledThreadPool(1);

//        executorService.schedule(new Runnable() {
//            @Override
//            public void run() {
//                log.warn("schedule run");
//            }
//         //延迟3秒后执行
//        }, 3, TimeUnit.SECONDS);
        //        executorService.shutdown();

//        executorService.scheduleWithFixedDelay(new Runnable() {
//            @Override
//            public void run() {
//                log.warn("scheduleWithFixedDelay run");
//            }
//            //延迟一秒后每隔3秒执行
//        }, 1, 3, TimeUnit.SECONDS);

        executorService.scheduleAtFixedRate(new Runnable() {
            @Override
            public void run() {
                log.warn("schedule run");
            }
            //延迟一秒后每隔3秒执行
        }, 1, 3, TimeUnit.SECONDS);

        /**
         * 定时器调度,不推荐使用,推荐ScheduledExecutorService调度
         */
//        Timer timer = new Timer();
//        timer.schedule(new TimerTask() {
//            @Override
//            public void run() {
//                log.warn("timer run");
//            }
//        //从当前时间每隔5秒执行
//        }, new Date(), 5 * 1000);
    }

总结

  • FixedThreadPool和SingleThreadExecutor 允许的请求队列长度为Integer.MAX_VALUE,可能会堆积大量的请求,从而引起OOM异常
  • CachedThreadPool 和newScheduledThreadPool允许创建的线程数为Integer.MAX_VALUE,可能会创建大量的线程,从而引起OOM异常

这就是为什么禁止使用Executors去创建线程池,而是推荐自己去创建ThreadPoolExecutor的原因

如何定义线程池参数

CPU密集型 : 线程池的大小推荐为CPU数量 + 1,CPU数量可以根据Runtime.availableProcessors方法获取 IO密集型 : CPU数量 * CPU利用率 * (1 + 线程等待时间/线程CPU时间) 混合型 : 将任务分为CPU密集型和IO密集型,然后分别使用不同的线程池去处理,从而使每个线程池可以根据各自的工作负载来调整 阻塞队列 : 推荐使用有界队列,有界队列有助于避免资源耗尽的情况发生 拒绝策略 : 默认采用的是AbortPolicy拒绝策略,直接在程序中抛出RejectedExecutionException异常【因为是运行时异常,不强制catch】,这种处理方式不够优雅。处理拒绝策略有以下几种比较推荐:

  • 在程序中捕获RejectedExecutionException异常,在捕获异常中对任务进行处理。针对默认拒绝策略
  • 使用CallerRunsPolicy拒绝策略,该策略会将任务交给调用execute的线程执行【一般为主线程】,此时主线程将在一段时间内不能提交任何任务,从而使工作线程处理正在执行的任务。此时提交的线程将被保存在TCP队列中,TCP队列满将会影响客户端,这是一种平缓的性能降低
  • 自定义拒绝策略,只需要实现RejectedExecutionHandler接口即可
  • 如果任务不是特别重要,使用DiscardPolicy和DiscardOldestPolicy拒绝策略将任务丢弃也是可以的

如果使用Executors的静态方法创建ThreadPoolExecutor对象,可以通过使用Semaphore对任务的执行进行限流也可以避免出现OOM异常

到此这篇关于Java线程池Executor用法详解的文章就介绍到这了,更多相关Java线程池Executor内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • Java 线程池ThreadPoolExecutor源码解析

    目录 引导语 1.整体架构图 1.1.类结构 1.2.类注释 1.3.ThreadPoolExecutor重要属性 2.线程池的任务提交 3.线程执行完任务之后都在干啥 4.总结 引导语 线程池我们在工作中经常会用到.在请求量大时,使用线程池,可以充分利用机器资源,增加请求的处理速度,本章节我们就和大家一起来学习线程池. 本章的顺序,先说源码,弄懂原理,接着看一看面试题,最后看看实际工作中是如何运用线程池的. 1.整体架构图 我们画了线程池的整体图,如下: 本小节主要就按照这个图来进行 Thre

  • 详解Java线程池和Executor原理的分析

    详解Java线程池和Executor原理的分析 线程池作用与基本知识 在开始之前,我们先来讨论下"线程池"这个概念."线程池",顾名思义就是一个线程缓存.它是一个或者多个线程的集合,用户可以把需要执行的任务简单地扔给线程池,而不用过多的纠结与执行的细节.那么线程池有哪些作用?或者说与直接用Thread相比,有什么优势?我简单总结了以下几点: 减小线程创建和销毁带来的消耗 对于Java Thread的实现,我在前面的一篇blog中进行了分析.Java Thread与内

  • 简单聊一聊Java线程池ThreadPoolExecutor

    目录 简介 参数说明 如何创建线程池 拒绝策略 总结 简介 ThreadPoolExecutor是一个实现ExecutorService接口的线程池,ExecutorService是主要用来处理多线程任务的一个接口,通常比较简单是用法是由Executors工厂类去创建. 线程池主要解决了两个不同的问题: 在执行大量异步任务时,为了能够提高性能,通常会减少每个任务的调用开销. 提供了一系列多线程任务的管理方法,便于多任务执行时合理分配资源以及一些异常情况的处理.每个ThreadPoolExecut

  • Java并发之线程池Executor框架的深入理解

    线程池 无限制的创建线程 若采用"为每个任务分配一个线程"的方式会存在一些缺陷,尤其是当需要创建大量线程时: 线程生命周期的开销非常高 资源消耗 稳定性 引入线程池 任务是一组逻辑工作单元,线程则是使任务异步执行的机制.当存在大量并发任务时,创建.销毁线程需要很大的开销,运用线程池可以大大减小开销. Executor框架 说明: Executor 执行器接口,该接口定义执行Runnable任务的方式. ExecutorService 该接口定义提供对Executor的服务. Sched

  • Java 线程池ExecutorService详解及实例代码

    Java 线程池ExecutorService 1.线程池 1.1什么情况下使用线程池 单个任务处理的时间比较短. 将需处理的任务的数量大. 1.2使用线程池的好处 减少在创建和销毁线程上所花的时间以及系统资源的开销. 如果不使用线程池,有可能造成系统创建大量线程而导致消耗系统内存以及"过度切换"; 2.ExecutorService和Executors 2.1简介 ExecutorService是一个接口,继承了Executor, public interface ExecutorS

  • Java线程池 ThreadPoolExecutor 详解

    目录 一 为什么要使用线程池 二 线程池原理详解 2.1 线程池核心组成 2.2 Execute 原理 三 线程池的使用 3.1 创建线程池 3.1.1 自定义线程池 3.1.2 功能线程池 3.1.3 功能线程池存在的问题 3.2 向线程池提交任务 3.3 关闭线程池 3.4 自定义线程池需要考虑因素 一 为什么要使用线程池 对于操作系统而言,创建一个线程的代价是十分昂贵的, 需要给它分配内存.列入调度,同时在线程切换时要执行内存换页,清空 CPU 缓存,切换回来时还要重新从内存中读取信息,破

  • Java线程池Executor用法详解

    目录 线程池类图 线程池的好处 new Thread的弊端 线程池核心类-ThreadPoolExecutor 使用Executors创建线程池 Executors.newCachedThreadPool Executors.newSingleThreadExecutor Executors.newFixedThreadPool Executors.newScheduledThreadPool 总结 如何定义线程池参数 线程池类图 我们最常使用的Executors实现创建线程池使用线程主要是用上

  • JAVA线程池原理实例详解

    本文实例讲述了JAVA线程池原理.分享给大家供大家参考,具体如下: 线程池的优点 1.线程是稀缺资源,使用线程池可以减少创建和销毁线程的次数,每个工作线程都可以重复使用. 2.可以根据系统的承受能力,调整线程池中工作线程的数量,防止因为消耗过多内存导致服务器崩溃. 线程池的创建 public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQu

  • 深入java线程池的使用详解

    在Java 5.0之前启动一个任务是通过调用Thread类的start()方法来实现的,任务的提于交和执行是同时进行的,如果你想对任务的执行进行调度或是控制 同时执行的线程数量就需要额外编写代码来完成.5.0里提供了一个新的任务执行架构使你可以轻松地调度和控制任务的执行,并且可以建立一个类似数据库连接 池的线程池来执行任务.这个架构主要有三个接口和其相应的具体类组成.这三个接口是Executor, ExecutorService.ScheduledExecutorService,让我们先用一个图

  • 模拟简单Java线程池的方法详解

    目录 一. 前言 二.线程池是什么? 三.线程池构造方法ThreadPoolExecutor的构造方法的参数都是啥意思? 四.模拟实现一个线程池 总结 一. 前言 为了实现并发编程,于是就引入了进程这个概念.进程就相当于操作系统的一个任务.多个进程同时执行任务,就实现了并发编程,能够更快的执行. 但是由于进程还不够轻量,创建一个进程,销毁一个进程消耗的资源不可忽视.如果进程数量不多的情况下,这些资源消耗是可以接受的,但是如果频繁的创建.销毁进程.就是一笔很大的开销了. 那要怎么办呢? 为了解决这

  • java 打造阻塞式线程池的实例详解

    java 打造阻塞式线程池的实例详解 原来以为tiger已经自带了这种线程池,就是在任务数量超出时能够阻塞住投放任务的线程,主要想用在JMS消息监听. 开始做法: 在ThreadPoolExcecutor中代入new ArrayBlockingQueue(MAX_TASK). 在任务超出时报错:RejectedExecutionException. 后来不用execute方法加入任务,直接getQueue().add(task), 利用其阻塞特性.但是发现阻塞好用了,但是任务没有被处理.一看Qu

  • Java中线程池自定义实现详解

    目录 前言 线程为什么不能多次调用start方法 线程池到底是如何复用的 前言 最初使用线程池的时候,网上的文章告诉我说线程池可以线程复用,提高线程的创建效率.从此我的脑海中便为线程池打上了一个标签——线程池可以做到线程的复用.但是我总以为线程的复用是指在创建出来的线程可以多次的更换run()方法的内容,来达到线程复用的目的,于是我尝试了一下.同一个线程调用多次,然后使run的内容不一样,但是我发现我错了,一个线程第一次运行是没问题的,当再次调用start方法是会抛出异常(java.lang.I

  • JAVA线程sleep()和wait()详解及实例

    JAVA线程sleep()和wait()详解及实例 sleep 1.sleep是Thread的一个静态(static)方法.使得Runnable实现的线程也可以使用sleep方法.而且避免了线程之前相互调用sleep()方法,引发死锁. 2.sleep()执行时需要赋予一个沉睡时间.在沉睡期间(阻塞线程期间),CPU会放弃这个线程,执行其他任务.当沉睡时间到了之后,该线程会自动苏醒,不过此时线程不会立刻被执行,而是要等CPU分配资源,和其他线程进行竞争. 3.此外如果这个线程之前获取了一个机锁,

  • java中stringBuilder的用法详解

    String对象是不可改变的.每次使用System.String类中的方法之一时,都要在内存中创建一个新的字符串对象,这就需要为该新对象分配新的空间.在需要对字符串执行重复修改的情况下,与创建新的String对象相关的系统开销可能会非常昂贵.如果要修改字符串而不创建新的对象,则可以使用System.Text.StringBuilder类.例如,当在一个循环中将许多字符串连接在一起时,使用StringBuilder类可以提升性能. 通过用一个重载的构造函数方法初始化变量,可以创建StringBui

  • java 中 ChannelHandler的用法详解

    java 中 ChannelHandler的用法详解 前言: ChannelHandler处理一个I/O event或者拦截一个I/O操作,在它的ChannelPipeline中将其递交给相邻的下一个handler. 通过继承ChannelHandlerAdapter来代替 因为这个接口有许多的方法需要实现,你或许希望通过继承ChannelHandlerAdapter来代替. context对象 一个ChannelHandler和一个ChannelHandlerContext对象一起被提供.一个

  • Java中isAssignableFrom的用法详解

    class1.isAssignableFrom(class2) 判定此 Class 对象所表示的类或接口与指定的 Class 参数所表示的类或接口是否相同,或是否是其超类或超接口.如果是则返回 true:否则返回 false.如果该 Class 表示一个基本类型,且指定的 Class 参数正是该 Class 对象,则该方法返回 true:否则返回 false. 1. class2是不是class1的子类或者子接口 2. Object是所有类的父类 一个例子搞定: package com.auuz

随机推荐