文章目录

  • 线程池讲解(上)
  • 一、线程池基本概念
  • 1. 什么是线程池
  • 2. 为什么使用线程池
  • 3. 线程池应用场景
  • 二、如何创建线程池
  • 1. 通过ThreadPoolExecutor构造方法实现
  • 2. 通过 Executor 框架的工具类 Executors 来实现
  • 三、Executor框架
  • 1. 简介
  • 2. Executor 框架结构
  • 2.1 任务(Runnable/Callable)
  • 2.2 任务的执行(Executor)
  • 2.3 异步计算的结果(Future)
  • 3. Executor 框架的使用示意图
  • 四、ThreadPoolExecutor 类介绍
  • 1. 线程池核心参数
  • 2. 线程池状态
  • 3. 线程池执行任务的流程
  • 五、线程池简单实现
  • 1. 参数设计分析
  • 2. 自定义线程池实现-Runnable
  • 3. 自定义线程池实现-Callable


线程池讲解(上)

一、线程池基本概念

1. 什么是线程池

线程池是一种线程使用模式。在线程池中维护着多个线程,等待着监督管理者分配可并发执行的任务。这避免了在处理短时间任务时创建与销毁线程的代价。处理过程中可以将任务添加到队列中,然后在创建线程后自动启动这些任务。 这里的任务就是我们前面学过的实现了Runnable或Callable接口的实例对象;

线程池提供了一种限制和管理资源的方式,每个线程池可以维护一些基本的统计信息。

也就是说,线程池实现了一个线程在执行完一段任务后,不销毁,继续执行下一段任务。

使用线程池不仅能够保证内核的充分利用,还能防止过分调度。WEB服务器完成网页请求这样的任务,使用线程池技术是非常合适的。

2. 为什么使用线程池

java中经常需要用到多线程来处理一些业务,我们非常不建议单纯使用继承Thread或者实现Runnable接口的方式来创建线程,那样势必有创建及销毁线程耗费资源、线程上下文切换问题。同时创建过多的线程也可能引发资源耗尽的风险,这个时候引入线程池比较合理,方便线程任务的管理。

java中涉及到线程池的相关类均在jdk1.5开始的java.util.concurrent包中,涉及到的几个核心类及接口包括:ExecutorExecutorsExecutorServiceThreadPoolExecutorFutureTaskCallableRunnable等。

使用线程池最大的原因就是可以根据系统的需求和硬件环境灵活的控制线程的数量,且可以对所有线程进行统一的管理和控制,从而提高系统的运行效率,降低系统运行压力

使用线程池的具体优势如下:

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

3. 线程池应用场景

常见应用场景如下:

  • 网购商品秒杀
  • 云盘文件上传和下载
  • 12306网上购票系统

二、如何创建线程池

1. 通过ThreadPoolExecutor构造方法实现

ThreadPoolExecutor类对应的构造方法如图所示:

核心参数的讲解在后面介绍。

java jedis 线程池 java 线程池详解_java

2. 通过 Executor 框架的工具类 Executors 来实现

Java中创建线程池很简单,只需要调用Executors中相应的便捷方法即可,比如Executors.newFixedThreadPool(int nThreads),虽然便捷且隐藏了复杂性,但也为我们埋下了潜在的隐患(OOM,线程耗尽)。

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

java jedis 线程池 java 线程池详解_开发语言_02


注意:

《阿里巴巴 Java 开发手册》中强制线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。

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

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

三、Executor框架

1. 简介

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

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

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

2. Executor 框架结构

2.1 任务(Runnable/Callable)

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

2.2 任务的执行(Executor)

如下图所示,包括任务执行机制的核心接口 Executor ,以及继承自 Executor 接口的 ExecutorService 接口。ThreadPoolExecutorScheduledThreadPoolExecutor 这两个关键类实现了 ExecutorService 接口

这里提了很多底层的类关系,但是,实际上我们需要更多关注的是 ThreadPoolExecutor 这个类,这个类在我们实际使用线程池的过程中,使用频率还是非常高的。

java jedis 线程池 java 线程池详解_jvm_03


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

ThreadPoolExecutor 类描述:

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

ScheduledThreadPoolExecutor 类描述:

//ScheduledExecutorService继承ExecutorService接口
public class ScheduledThreadPoolExecutor
        extends ThreadPoolExecutor
        implements ScheduledExecutorService

2.3 异步计算的结果(Future)

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

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

3. Executor 框架的使用示意图

java jedis 线程池 java 线程池详解_java_04

  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接口的对象。由于 FutureTask 实现了 Runnable,我们也可以创建 FutureTask,然后直接交给 ExecutorService 执行。
  4. 最后,主线程可以执行 FutureTask.get()方法来等待任务执行完成(get()方法会阻塞当前线程直到任务完成)。主线程也可以执行 FutureTask.cancel(boolean mayInterruptIfRunning)来取消此任务的执行。

四、ThreadPoolExecutor 类介绍

ThreadPoolExecutor 是线程池最为核心的一个类,而线程池为它提供了四个构造方法,我们先来看一下其中最原始的一个构造方法,其余三个都是由它衍生而来

1. 线程池核心参数

//构造方法
public ThreadPoolExecutor(
    int corePoolSize,     //核心线程数
    int maximumPoolSize,  //最大线程数
    long keepAliveTime,   //最大空闲时间
    TimeUnit unit,        //时间单位
    BlockingQueue<Runnable> workQueue, //任务队列
    ThreadFactory threadFactory, //线程工厂
    RejectedExecutionHandler handler //饱和策略
)
{...}

1、corePoolSize: 线程池的核心线程数。 当有任务提交到线程池的时候,如果当前线程数数量没有达到核心线程数量corePoolSize,就会新开一个线程执行此任务。

2、maximumPoolSize: 线程池能创建的最大线程的数量。 在核心线程都被占用的时候,继续申请的任务会被搁置在任务队列里面,而当任务队列满了的时候,线程池就会把线程数量创建至maximumPoolSize 个。

3、keepAliveTime: 最大空闲时间(非核心线程的心跳时间)。 当线程数大于核心线程数时,多余的空闲线程存活的最长时间,即当大于 corePoolSize 的线程在经过 keepAliveTime 仍然没有任务执行,则销毁线程。

4、unit :参数keepAliveTime的时间单位。

5、workQueue: 阻塞队列(任务队列)。 核心线程被占有时,任务被搁置在任务队列。比如有ArrayBlockingQueueLinkedBlockingQueue等。

6、ThreadFactory:线程工厂。 允许我们自己参与创建线程的过程,一般默认即可。

7、handler:饱和策略。 即当线程池和任务队列都达到最大负荷量时,下一个任务来临时采取的策略。ThreadPoolExecutor类中一共有4种饱和策略:

  • AbortPolicy: 线程任务丢弃报错,抛出 RejectedExecutionException异常。默认的饱和策略。
  • DiscardPolicy: 线程任务直接丢弃不报错。
  • DiscardOldestPolicy:workQueue队首任务丢弃,将最新线程任务重新加入到队列执行。
  • CallerRunPolicy:线程池之外的线程直接调用run方法执行。即调用执行自己的线程运行任务,也就是直接在调用execute方法的线程中运行被拒绝的任务,如果执行程序已关闭,则会丢弃该任务。因此这种策略会降低对于新任务提交速度,影响程序的整体性能。如果您的应用程序可以承受此延迟并且你要求任何一个任务请求都要被执行的话,你可以选择这个策略。

    由上图可以看出,非核心线程数量 = maximumPoolSize - corePoolSize 在默认情况下,ThreadPoolExecutor 将抛出 RejectedExecutionException 来拒绝新来的任务 ,这代表你将丢失对这个任务的处理。 对于可伸缩的应用程序,建议使用ThreadPoolExecutor.CallerRunsPolicy。当最大池被填满时,此策略为我们提供可伸缩队列。

2. 线程池状态

ThreadPoolExecutor中定义了一个volatile变量,另外定义了几个static final变量表示线程池的各个状态:

volatile int runState;
private static final int RUNNING    = -1 << COUNT_BITS;
private static final int SHUTDOWN   =  0 << COUNT_BITS;
private static final int STOP       =  1 << COUNT_BITS;
private static final int TIDYING    =  2 << COUNT_BITS;
private static final int TERMINATED =  3 << COUNT_BITS;

runState表示当前线程池的状态,它是一个volatile变量用来保证线程之间的可见性;下面的几个static final变量表示runState可能的几个取值。

  • RUNNING: 线程池创建之后的初始状态,这种状态下可以执行任务
  • SHUTDOWN:该状态下线程池不再接受新任务,但是会将工作队列中的任务执行完毕
  • STOP:该状态下线程池不再接受新任务,也不会处理工作队列中的剩余任务,并且将会中断所有工作线程
  • TIDYING:该状态下所有任务都已终止或者处理完成,将会执行terminated()钩子方法
  • TERMINATED:线程池彻底终止。执行完terminated()钩子方法之后的状态
  • java jedis 线程池 java 线程池详解_java jedis 线程池_05


3. 线程池执行任务的流程

java jedis 线程池 java 线程池详解_jvm_06


1、线程池执行execute/submit方法向线程池添加任务,当任务小于核心线程数corePoolSize,则线程池中可以创建新的线程。

2、当任务大于核心线程数corePoolSize,就向任务队列添加任务。

3、如果任务队列已满,需要通过比较参数maximumPoolSize,在线程池创建新的线程时,当线程数量大于maximumPoolSize,说明当前设置的线程池中的线程已经无法处理了,就会执行饱和策略。

五、线程池简单实现

1. 参数设计分析

要设计一个好的线程池,就必须合理的设置线程池的4个重要参数:

1、核心线程数(corePoolSize)

核心线程数的设计需要依据任务的处理时间和每秒产生的任务数量来确定。

例如:执行一个任务需要0.1秒,系统百分之80的时间每秒都会产生100个任务,那么要想在1秒内处理完这100个任务,就需要10个线程,此时我们就可以设计核心线程数为10;当然实际情况不可能这么平均,所以我们一般按照8020原则设计即可,既按照百分之80的情况设计核心线程数,剩下的百分之20可以利用最大线程数处理。

2、任务队列长度(workQueue)

任务队列长度一般设计为:(核心线程数/单个任务执行时间)*2即可。

例如上面的场景中,核心线程数设计为10,单个任务执行时间为0.1秒,则队列长度可以设计为200。

3、最大线程数(maximumPoolSize)

最大线程数的设计除了需要参照核心线程数的条件外,还需要参照系统每秒产生的最大任务数决定。

例如:上述环境中,如果系统每秒最大产生的任务是1000个,那么,最大线程数=(最大任务数-任务队列长度)*单个任务执行时间,即:最大线程数=(1000-200)*0.1=80个;

4、最大空闲时间(keepAliveTime)

这个参数的设计完全参考系统运行环境和硬件压力设定,没有固定的参考值,用户可以根据经验和系统产生任务的时间间隔合理设置一个值即可。

注意:

如上所述只是一般的设计原则,并不是固定的,用户也可以根据实际情况灵活调整!

2. 自定义线程池实现-Runnable

首先,创建一个 Runnable 接口的实现类(当然也可以是 Callable 接口)

import java.util.Date;
/**
 * 这是一个简单的Runnable类,需要大约5秒钟来执行其任务。
 */
public class MyTask implements Runnable{
    private int taskId; //线程id
    private String taskName; //线程名字
    public MyTask() {
    }
    public MyTask(int taskId, String taskName) {
        this.taskId = taskId;
        this.taskName = taskName;
    }
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + " Start. Time = " + new Date());
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + " End. Time = " + new Date());
    }
}

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

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

public class MyThreadPool {
    private static final int CORE_POOL_SIZE = 5;     //核心线程数
    private static final int MAX_POOL_SIZE = 10;     //最大线程数量
    private static final int QUEUE_CAPACITY = 100;   //任务队列长度
    private static final Long KEEP_ALIVE_TIME = 1L;  //最大空闲时间
    public static void main(String[] args) {
        //通过ThreadPoolExecutor构造函数自定义参数创建线程池
        ThreadPoolExecutor pool = new ThreadPoolExecutor(
                CORE_POOL_SIZE,
                MAX_POOL_SIZE,
                KEEP_ALIVE_TIME,
                TimeUnit.SECONDS,
                new ArrayBlockingQueue<>(QUEUE_CAPACITY),
                new ThreadPoolExecutor.AbortPolicy()
        );
        //创建10个任务并提交
        for(int i=1; i<=10; i++){
            //创建 MyRunnable 对象(MyRunnable 类实现了Runnable 接口)
            Runnable worker = new MyTask();
            //执行Runnable
            pool.execute(worker);
        }
        //关闭线程池
        pool.shutdown();
        //当调用shutdown()方法后,并且所有提交的任务完成后返回true
        while (!pool.isTerminated()){
        }
        System.out.println("Finished all threads");
    }
}

运行结果如下:

java jedis 线程池 java 线程池详解_开发语言_07


我们通过代码输出结果可以看出:线程池首先会先执行 5 个任务,然后这些任务有任务被执行完的话,就会去拿新的任务执行。

分析:我们在代码中模拟了 10 个任务,配置的核心线程数为 5 、等待队列容量为 100 ,所以每次只可能存在 5 个任务同时执行,剩下的 5 个任务会被放到任务队列中去。当前的5个任务中如果有任务被执行完了,线程池就会去拿新的任务执行。

3. 自定义线程池实现-Callable

MyCallable.java

import java.util.concurrent.Callable;

public class MyCallable implements Callable<String> {
    @Override
    public String call() throws Exception {
        Thread.sleep(1000);
        //返回执行当前 Callable 的线程名字
        return Thread.currentThread().getName();
    }
}

CallableDemo.java

import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

public class CallableDemo {
    private static final int CORE_POOL_SIZE = 5;
    private static final int MAX_POOL_SIZE = 10;
    private static final int QUEUE_CAPACITY = 100;
    private static final Long KEEP_ALIVE_TIME = 1L;

    public static void main(String[] args) {
        //使用阿里巴巴推荐的创建线程池的方式
        //通过ThreadPoolExecutor构造函数自定义参数创建
        ThreadPoolExecutor executor = new ThreadPoolExecutor(
                CORE_POOL_SIZE,
                MAX_POOL_SIZE,
                KEEP_ALIVE_TIME,
                TimeUnit.SECONDS,
                new ArrayBlockingQueue<>(QUEUE_CAPACITY),
                new ThreadPoolExecutor.CallerRunsPolicy());

        List<Future<String>> futureList = new ArrayList<>();
        Callable<String> callable = new MyCallable();
        for (int i = 0; i < 10; i++) {
            //提交任务到线程池
            Future<String> future = executor.submit(callable);
            //将返回值 future 添加到 list,我们可以通过 future 获得 执行 Callable 得到的返回值
            futureList.add(future);
        }
        for (Future<String> fut : futureList) {
            try {
                System.out.println(new Date() + "::" + fut.get());
            } catch (InterruptedException | ExecutionException e) {
                e.printStackTrace();
            }
        }
        //关闭线程池
        executor.shutdown();
    }
}

输出:

java jedis 线程池 java 线程池详解_开发语言_08


参考链接:

https://javaguide.cn/java/concurrent/java-thread-pool-summary.html#%E4%B8%80-%E4%BD%BF%E7%94%A8%E7%BA%BF%E7%A8%8B%E6%B1%A0%E7%9A%84%E5%A5%BD%E5%A4%84