Java中高级核心知识全面解析——线程池(好处、Executor-框架、ThreadPoolExecutor类简单介绍)

本文详细介绍了Java线程池的使用好处,Executor框架,特别是ThreadPoolExecutor类的重要参数和使用示例。强调了使用线程池可以降低资源消耗、提高响应速度和管理性。文章还讨论了线程池的饱和策略,如AbortPolicy、CallerRunsPolicy、DiscardPolicy和DiscardOldestPolicy。此外,分析了FixedThreadPool、SingleThreadExecutor、CachedThreadPool和ScheduledThreadPoolExecutor四种常见线程池的优缺点,并提供了线程池大小确定的指导原则。
摘要由CSDN通过智能技术生成

一、使用线程池的好处

池化技术相比大家已经屡见不鲜了,线程池、数据库连接池、Http 连接池等等都是对这个思想的应用。池化技术的思想主要是为了减少每次获取资源的消耗,提高对资源的利用率。

线程池提供了一种限制和管理资源(包括执行一个任务)。 每个线程池还维护一些基本统计信息,例如已完成任务的数量。
这里借用《Java 并发编程的艺术》提到的来说一下使用线程池的好处:

  • 降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
  • 提高响应速度。当任务到达时,任务可以不需要的等到线程创建就能立即执行。
  • 提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统
    的稳定性,使用线程池可以进行统一的分配,调优和监控。

二、 Executor 框架

1.简介

Executor 框架是 Java5 之后引进的,在 Java 5 之后,通过 Executor来启动线程比使用 Threadstart方法更好,除了更易管理,效率更好(用线程池实现,节约开销)外,还有关键的一点:有助于避免 this 逃逸问题

补充:this 逃逸是指在构造函数返回之前其他线程就持有该对象的引用. 调用尚未构造完全的对象的方法可能引发令人疑惑的错误。

Executor 框架不仅包括了线程池的管理,还提供了线程工厂队列以及拒绝策略等,Executor 框架让并发编程变得更加简单。

2.Executor 框架结构(主要由三大部分组成)

1) 任务( Runnable / Callable )

执行任务需要实现的Runnable接口Callable接口Runnable接口Callable接口实现类都可以被 ThreadPoolExecutorScheduledThreadPoolExecutor执行。

2) 任务的执行( Executor )

如下图所示,包括任务执行机制的核心接口Executor,以及继承自Executor接口的
ExecutorService接口ThreadPoolExecutorScheduledThreadPoolExecutor这两个关键类实现了 ExecutorService 接口
这里提了很多底层的类关系,但是,实际上我们需要更多关注的是 ThreadPoolExecutor 这个类,这个类在我们实际使用线程池的过程中,使用频率还是非常高的。

注意: 通过查看ScheduledThreadPoolExecutor源代码我们发现ScheduledThreadPoolExecutor实际上是继承了 ThreadPoolExecutor并实现了ScheduledExecutorService,而 ScheduledExecutorService又实现了ExecutorService
正如我们下面给出的类关系图显示的一样。

ThreadPoolExecutor类描述:

//AbstractExecutorService实现了ExecutorService接口 
public class ThreadPoolExecutor extends AbstractExecutorService

ScheduledThreadPoolExecutor类描述:

//ScheduledExecutorService实现了ExecutorService接口 
public class ScheduledThreadPoolExecutor 
        extends ThreadPoolExecutor 
        implements ScheduledExecutorService

3) 异步计算的结果( Future )

Future接口以及Future接口的实现类FutureTask类都可以代表异步计算的结果。

当我们把Runnable接口Callable接口的实现类提交给 ThreadPoolExecutorScheduledThreadPoolExecutor执行。(调用submit()方法时会返回一个FutureTask对象)

3.Executor 框架的使用示意图

  1. 主线程首先要创建实现Runnable或者Callable接口的任务对象
  2. 把创建完成的实现Runnable / Callable接口的对象直接交给ExecutorService执行: ExecutorService.execute(Runnable command))或者也可以把Runnable对象或Callable对象提交给ExecutorService执行( ExecutorService.submit(Runnable task)ExecutorService.submit(Callable <T> task))
  3. 如果执行ExecutorService.submit(…)ExecutorService将返回一个实现Future接口的对象(我们刚刚也提到过了执行execute()方法和submit()方法的区别,submit()会返回一个FutureTask对象)。由于FutureTask实现了Runnable,我们也可以创建FutureTask,然后直接交给ExecutorService执行。
  4. 最后,主线程可以执行FutureTask.get()方法来等待任务执行完成。主线程也可以执行FutureTask.cancel(boolean mayInterruptIfRunning)来取消此任务的执行

三、(重要)ThreadPoolExecutor类简单介绍

线程池实现类 ThreadPoolExecutor 是 Executor 框架最核心的类。

1.ThreadPoolExecutor 类分析

ThreadPoolExecutor类中提供的四个构造方法。我们来看最长的那个,其余三个都是在这个构造方
法的基础上产生(其他几个构造方法说白点都是给定某些默认参数的构造方法比如默认制定拒绝策略是什么),这里就不贴代码讲了,比较简单。

/**
* 用给定的初始参数创建一个新的ThreadPoolExecutor。 
*/
public ThreadPoolExecutor(int corePoolSize,//线程池的核心线程数量 
                          int maximumPoolSize,//线程池的最大线程数 
                          long keepAliveTime,//当线程数大于核心线程数时,多余的空闲线程存活的最长时间 
                          TimeUnit unit,//时间单位 
                          BlockingQueue<Runnable> workQueue,//任务队列,用来储 存等待执行任务的队列 
                          ThreadFactory threadFactory,//线程工厂,用来创建线程, 一般默认即可

                          RejectedExecutionHandler handler//拒绝策略,当提交的 任务过多而不能及时处理时,我们可以定制策略来处理任务
                           ) {
    
    if (corePoolSize < 0 || 
        maximumPoolSize <= 0 || 
        maximumPoolSize < corePoolSize || 
        keepAliveTime < 0) 
        throw new IllegalArgumentException(); 
    if (workQueue == null || threadFactory == null || handler == null) 
        throw new NullPointerException(); 
    this.corePoolSize = corePoolSize; 
    this.maximumPoolSize = maximumPoolSize; 
    this.workQueue = workQueue;
    this.keepAliveTime = unit.toNanos(keepAliveTime); 
    this.threadFactory = threadFactory; 
    this.handler = handler; 
}

下面这些对创建 非常重要,在后面使用线程池的过程中你一定会用到!所以,务必拿着小本本记清楚。
ThreadPoolExecutor3个最重要的参数

  • corePoolSize: 核心线程数线程数定义了最小可以同时运行的线程数量。
  • maximumPoolSize: 当队列中存放的任务达到队列容量的时候,当前可以同时运行的线程数量变为最大线程数。
  • workQueue: 当新任务来的时候会先判断当前运行的线程数量是否达到核心线程数,如果达到的话,信任就会被存放在队列中。

ThreadPoolExecutor其他常见参数:

  1. keepAliveTime:当线程池中的线程数量大于corePoolSize的时候,如果这时没有新的任务提交,核心线程外的线程不会立即销毁,而是会等待,直到等待的时间超过了keepAliveTime才会
    被回收销毁;
  2. unit:keepAliveTime参数的时间单位。
  3. threadFactory:executor创建新线程的时候会用到。
  4. handler:饱和策略。关于饱和策略下面单独介绍一下。

下面这张图可以加深你对线程池中各个参数的相互关系的理解

ThreadPoolExecutor饱和策略定义:
如果当前同时运行的线程数量达到最大线程数量并且队列也已经被放满了任时,
ThreadPoolTaskExecutor定义一些策略:

  • ThreadPoolExecutor.AbortPolicy:抛出RejectedExecutionException来拒绝新任务的处理。
  • ThreadPoolExecutor.CallerRunsPolicy:调用执行自己的线程运行任务,也就是直接在调用execute方法的线程中运行( run )被拒绝的任务,如果执行程序已关闭,则会丢弃该任务。因此这种策略会降低对于新任务提交速度,影响程序的整体性能。另外,这个策略喜欢增加队列容量。如果您的应用程序可以承受此延迟并且你不能任务丢弃任何一个任务请求的话,你可以选择这个策略。
  • ThreadPoolExecutor.DiscardPolicy: 不处理新任务,直接丢弃掉。
  • ThreadPoolExecutor.DiscardOldestPolicy: 此策略将丢弃最早的未处理的任务请求。

举个例子:

Spring 通过ThreadPoolTaskExecutor或者我们直接通过ThreadPoolExecutor的构造函数创建线程池的时候,当我们不指定RejectedExecutionHandler饱和策略的话来配置线程池的时候默认使用的是ThreadPoolExecutor.AbortPolicy。在默认情况下,
ThreadPoolExecutor将抛出RejectedExecutionException来拒绝新来的任务 ,这代表你将丢失对这个任务的处理。 对于可伸缩的应用程序,建议使用
ThreadPoolExecutor.CallerRunsPolicy。当最大池被填满时,此策略为我们提供可伸缩队列。(这个直接查看ThreadPoolExecutor的构造函数源码就可以看出,比较简单的原因,这里就不贴代码了。)

2.推荐使用 ThreadPoolExecutor 构造函数创建线程池

为什么呢?

使用线程池的好处是减少在创建和销毁线程上所消耗的时间以及系统资源开销,解决资源不足的问题。如果不使用线程池,有可能会造成系统创建大量同类线程而导致消耗完内存或者“过度切换”的问题。

强制线程池不允许使用 Executors 去创建,而是通过ThreadPoolExecutor构造函数的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险

Executors 返回线程池对象的弊端如下:

  • FixedThreadPoolSingleThreadExecutor: 允许请求的队列长度为Integer.MAX_VALUE,可能堆积大量的请求,从而导致OOM。
  • CachedThreadPoolScheduledThreadPool: 允许创建的线程数量为Integer.MAX_VALUE ,可能会创建大量线程,从而导致 OOM。

方式一:通过ThreadPoolExecutor构造函数实现(推荐)

方式二:通过 Executor 框架的工具类 Executors 来实现
我们可以创建三种类型的 ThreadPoolExecutor:

  • FixedThreadPool
  • SingleThreadExecutor
  • CachedThreadPool

对应 Executors 工具类中的方法如图所示:

四、 (重要)ThreadPoolExecutor 使用示例

我们上面讲解了Executor框架以及ThreadPoolExecutor类,下面让我们实战一下,来通过写一个ThreadPoolExecutor的小Demo来回顾上面的内容。

1.示例代码:Runnable+ ThreadPoolExecutor

首先创建一个Runnable接口的实现类(当然也可以是Callable接口,我们上面也说了两者的区别。)

MyRunnable.java

import java.util.Date;

/**
* 这是一个简单的Runnable类,需要大约5秒钟来执行其任务。 
* @author shuang.kou 
*/

public class MyRunnable implements Runnable {
   
        private String command; 

        public MyRunnable(String s) {
    
            this.command = s; 
}
@Override 
        public void run() {
    
        System.out.println(Thread.currentThread().getName() + "               Start. Time = " 
+ new Date()); 
        processCommand(); 
        System.out.println(Thread.currentThread().getName() + " End. Time = " 
+ new Date()); 
}

        private void processCommand() {
    
            try {
   
                  Thread.sleep(5000);
            } catch (InterruptedException e) {
    
                e.printStackTrace(); 
            } 
}
        @Override 
        public String toString() {
    
        return this.command; 
    } 
}

编写测试程序,我们这里以阿里巴巴推荐的使用ThreadPoolExecutor构造函数自定义参数的方式来创建线程池。

ThreadPoolExecutorDemo.java

import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor; 
import java.util.concurrent.TimeUnit;

public class ThreadPoolExecutorDemo {
   
      private static final int CORE_POOL_SIZE = 5;
      private static final int MAX_POOL_SIZE = 10; 
      private static final int QUEUE_CAPACITY = 100;
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值