Java线程池详解

@Configuration
public class ThreadPoolConfig {

    /**
     * IO密集型任务  = 一般为2*CPU核心数(常出现于线程中:数据库数据交互、文件上传下载、网络数据传输等等)
     * CPU密集型任务 = 一般为CPU核心数+1(常出现于线程中:复杂算法)
     * 混合型任务  = 视机器配置和复杂度自测而定
     */
    @Bean(name = "asyncTaskExecutor")
    public ThreadPoolTaskExecutor asyncTaskExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        //1: 核心线程数目
        executor.setCorePoolSize(5);
        //2: 指定最大线程数,只有在缓冲队列满了之后才会申请超过核心线程数的线程
        executor.setMaxPoolSize(10);
        //3: 队列中最大的数目
        executor.setQueueCapacity(200);
        //4: 线程名称前缀
        executor.setThreadNamePrefix("kap-Thread-");
        //5:当pool已经达到max size的时候,如何处理新任务
        // CallerRunsPolicy: 会在execute 方法的调用线程中运行被拒绝的任务,如果执行程序已关闭,则会丢弃该任务
        // AbortPolicy: 抛出java.util.concurrent.RejectedExecutionException异常
        // DiscardOldestPolicy: 抛弃旧的任务
        // DiscardPolicy: 抛弃当前的任务
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        //6: 线程空闲后的最大存活时间(默认值 60),当超过了核心线程出之外的线程在空闲时间到达之后会被销毁
        executor.setKeepAliveSeconds(60);
        //7:线程空闲时间,当线程空闲时间达到keepAliveSeconds(秒)时,线程会退出,直到线程数量等于corePoolSize,如果allowCoreThreadTimeout=true,则会直到线程数量等于0
        executor.setAllowCoreThreadTimeOut(false);
        executor.initialize();
        return executor;
    }
}

线程池的构造函数有7个参数,分别是corePoolSize、maximumPoolSize、keepAliveTime、unit、workQueue、threadFactory、handler。

1. corePoolSize 线程池核心线程大小

当向线程池提交一个任务时,若线程池已创建的线程数小于corePoolSize,即便此时存在空闲线程,也会通过创建一个新线程来执行该任务,直到已创建的线程数大于或等于corePoolSize。

2. maximumPoolSize 线程池最大线程数量

线程池所允许的最大线程个数。当队列满了,且已创建的线程数小于maximumPoolSize,则线程池会创建新的线程来执行任务。

3.keepAliveTime 空闲线程存活时间

当线程池中线程数大于核心线程数时,线程的空闲时间如果超过线程存活时间,那么这个线程就会被销毁,直到线程池中的线程数小于等于核心线程数。一个线程如果处于空闲状态,并且当前的线程数量大于corePoolSize,那么在指定时间后,这个空闲线程会被销毁,这里的指定时间由keepAliveTime来设定

4.unit 空闲线程存活时间单位

keepAliveTime的计量单位

5.workQueue 工作队列

用于传输和保存等待执行任务的阻塞队,dk中提供了四种工作队列:

  1. ArrayBlockingQueue
    基于数组的有界阻塞队列,按FIFO排序。新任务进来后,会放到该队列的队尾,有界的数组可以防止资源耗尽问题。当线程池中线程数量达到corePoolSize后,再有新任务进来,则会将任务放入该队列的队尾,等待被调度。如果队列已经是满的,则创建一个新线程,如果线程数量已经达到maxPoolSize,则会执行拒绝策略。
  2. LinkedBlockingQuene
    基于链表的无界阻塞队列(其实最大容量为Interger.MAX),按照FIFO排序。由于该队列的近似无界性,当线程池中线程数量达到corePoolSize后,再有新任务进来,会一直存入该队列,而不会去创建新线程直到maxPoolSize,因此使用该工作队列时,参数maxPoolSize其实是不起作用的。
  3. SynchronousQuene
    一个不缓存任务的阻塞队列,生产者放入一个任务必须等到消费者取出这个任务。也就是说新任务进来时,不会缓存,而是直接被调度执行该任务,如果没有可用线程,则创建新线程,如果线程数量达到maxPoolSize,则执行拒绝策略。
  4. PriorityBlockingQueue
    具有优先级的无界阻塞队列,优先级通过参数Comparator实现。

6.threadFactory 线程工厂

创建一个新线程时使用的工厂,可以用来设定线程名、是否为daemon线程等等,threadFactory创建的线程也是采用new Thread()方式,threadFactory创建的线程名都具有统一的风格:pool-m-thread-n(m为线程池的编号,n为线程池内的线程编号)

7.handler 拒绝策略

当线程池和队列都满了,再加入线程会执行此策略。

  1. CallerRunsPolicy
    该策略下,在调用者线程中直接执行被拒绝任务的run方法,除非线程池已经shutdown,则直接抛弃任务
  2. AbortPolicy
    该策略下,直接丢弃任务,并抛出RejectedExecutionException异常。
  3. DiscardPolicy
    该策略下,直接丢弃任务,什么都不做。
  4. DiscardOldestPolicy
    该策略下,抛弃进入队列最早的那个任务,然后尝试把这次拒绝的任务放入队列

线程池流程

Java线程池详解_第1张图片

  1. 判断核心线程池是否已满,没满则创建一个新的工作线程来执行任务。已满则。
  2. 判断任务队列是否已满,没满则将新提交的任务添加在工作队列,已满则。
  3. 判断整个线程池是否已满,没满则创建一个新的工作线程来执行任务,已满则执行饱和策略。
  • 判断线程池中当前线程数是否大于核心线程数,如果小于,在创建一个新的线程来执行任务,如果大于则
  • 判断任务队列是否已满,没满则将新提交的任务添加在工作队列,已满则。
  • 判断线程池中当前线程数是否大于最大线程数,如果小于,则创建一个新的线程来执行任务,如果大于,则执行饱和策略。

java中提供的线程池

Executors类提供了4种不同的线程池:

  • newCachedThreadPool,
  • newFixedThreadPool,
  • newScheduledThreadPool,
  • newSingleThreadExecutor
  1. newCachedThreadPool:用来创建一个可以无限扩大的线程池,适用于负载较轻的场景,执行短期异步任务。(可以使得任务快速得到执行,因为任务时间执行短,可以很快结束,也不会造成cpu过度切换)

  2. newFixedThreadPool:创建一个固定大小的线程池,因为采用无界的阻塞队列,所以实际线程数量永远不会变化,适用于负载较重的场景,对当前线程数量进行限制。(保证线程数可控,不会造成线程过多,导致系统负载更为严重)

  3. newSingleThreadExecutor:创建一个单线程的线程池,适用于需要保证顺序执行各个任务。

  4. newScheduledThreadPool:适用于执行延时或者周期性任务。

execute()和submit()方法

  1. execute(),执行一个任务,没有返回值。
  2. submit(),提交一个线程任务,有返回值。
    submit(Callable task)能获取到它的返回值,通过future.get()获取(阻塞直到任务执行完)。一般使用FutureTask+Callable配合使用(IntentService中有体现)。

submit(Runnable task, T result)能通过传入的载体result间接获得线程的返回值。
submit(Runnable task)则是没有返回值的,就算获取它的返回值也是null。

Future.get方法会使取结果的线程进入阻塞状态,知道线程执行完成之后,唤醒取结果的线程,然后返回结果。

java线程池的调优以及监控

线程池的调优(线程池的合理配置)

先从以下几个角度分析任务的特性:

  • 任务的性质:CPU 密集型任务、IO 密集型任务和混合型任务。
  • 任务的优先级:高、中、低。
  • 任务的执行时间:长、中、短。
  • 任务的依赖性:是否依赖其他系统资源,如数据库连接。

任务性质不同的任务可以用不同规模的线程池分开处理。可以通过 Runtime.getRuntime().availableProcessors() 方法获得当前设备的 CPU 个数。

CPU 密集型任务配置 尽可能小的线程,如配置N^cpu+1个线程的线程池。

IO 密集型任务则由于线程并不是一直在执行任务,则配置尽可能多的线程,如2*N^cpu。

混合型任务 如果可以拆分,则将其拆分成一个 CPU 密集型任务和一个 IO 密集型任务。只要这两个任务执行的时间相差不是太大,那么分解后执行的吞吐率要高于串行执行的吞吐率;如果这两个任务执行时间相差太大,则没必要进行分解。

  • 优先级不同的任务可以使用优先级队列 PriorityBlockingQueue 来处理,它可以让优先级高的任务先得到执行。但是,如果一直有高优先级的任务加入到阻塞队列中,那么低优先级的任务可能永远不能执行。
  • 执行时间不同的任务可以交给不同规模的线程池来处理,或者也可以使用优先级队列,让执行时间短的任务先执行。
  • 依赖数据库连接池的任务,因为线程提交 SQL 后需要等待数据库返回结果,线程数应该设置得较大,这样才能更好的利用 CPU。
  • 建议使用有界队列,有界队列能增加系统的稳定性和预警能力。可以根据需要设大一点,比如几千。使用无界队列,线程池的队列就会越来越大,有可能会撑满内存,导致整个系统不可用。② 线程池的监控

可以通过线程池提供的参数读线程池进行监控,有以下属性可以使用:

  • taskCount:线程池需要执行的任务数量,包括已经执行完的、未执行的和正在执行的。
  • completedTaskCount:线程池在运行过程中已完成的任务数量,completedTaskCount <= taskCount。
  • largestPoolSize:线程池曾经创建过的最大线程数量,通过这个数据可以知道线程池是否满过。如等于线程池的最大大小,则表示线程池曾经满了。
  • getPoolSize: 线程池的线程数量。如果线程池不销毁的话,池里的线程不会自动销毁,所以线程池的线程数量只增不减。
  • getActiveCount:获取活动的线程数。
    通过继承线程池并重写线程池的 beforeExecute,afterExecute 和 terminated 方法,我们可以在任务执行前,执行后和线程池关闭前干一些事情。

如监控任务的平均执行时间,最大执行时间和最小执行时间等。这几个方法在线程池里是空方法,如:

protected void beforeExecute(Thread t, Runnable r) { }

你可能感兴趣的:(Java)