Java多线程----创建线程、线程池ExecutorService、异步编排

文章目录

  • 创建线程的四种方式
    • 方式一、继承Thread
    • 方式二、自定义实现Runnable接口
    • 方式三、Thread + FutureTask + Callable返回值
    • 方式四、线程池ThreadPoolExecutor
  • 线程池的简单介绍
  • 通过ThreadPoolExecutor创建自定义线程池
  • ThreadPoolExecutor创建线程池的7大参数
  • 线程池处理任务的执行流程
  • 四种RejectedExecutionHandler策略
    • AbortPolicy,丢弃并抛出异常
    • DiscardPolicy,静默丢弃什么也不做
    • DiscardOldestPolicy,丢弃最老后当前任务重新进池
    • CallerRunsPolicy,同步调用run()执行任务
  • Executors中四种常用的线程池
    • newCachedThreadPool
    • newFixedThreadPool
    • newScheduledThreadPool
    • newSingleThreadExecutor
  • CompletableFuture异步编排

创建线程的四种方式

通过方式一、方式二、方式三创建线程,都离不开Thread类以及它的start()来启动线程。所以它们和Thread类息息相关。创建线程本质就是new Thread();只是Thread类提供了多种不同入参的构造方法;
如下图所示;其中Runnable target类型的入参,衍生出两种创建线程的方式

  • 自定义实现Runnable接口
  • 构建Runnable的实例对象FutureTask;通过FutureTask就扯出了Callable接口
    Java多线程----创建线程、线程池ExecutorService、异步编排_第1张图片

方式一、继承Thread

public class CreateThread {
    public static void main(String[] args) {
        System.out.println("start");
        // 创建线程
        Thread01 thread01 = new Thread01();
        // 开启线程
        thread01.start();

        System.out.println("end");
    }
    
    /**
     * 期望通过继承Thread的方式创建并开启一个线程
     **/
    static class Thread01 extends Thread {
        @Override
        public void run() {
            System.out.println(Thread.currentThread().getName() + "---running");
        }
    }
}	

方式二、自定义实现Runnable接口

自定义类实现Runnable接口;通过Thread(Runnable target)的方式创建线程

public class CreateThread {
    public static void main(String[] args) {
        System.out.println("start");
        // 方式1;继承Thread类
        // Thread01 thread01 = new Thread01();
        // thread01.start();
        // 方式2;实现Runnable接口
        new Thread(()->
            System.out.println(Thread.currentThread().getName() + "---running")
        ).start();
        System.out.println("end");
    }
}

方式三、Thread + FutureTask + Callable返回值

也是通过Thread(Runnable target)的方式创建线程,只不过Runnable类型不需要自定义创建,使用现成的FutureTask
Java多线程----创建线程、线程池ExecutorService、异步编排_第2张图片
FutureTask的构造器如下:

  • FutureTask(Runnable,V):说实话至今还没有见过使用使用场景,确实鸡肋。不常用
  • FutureTask(Callable):可以返回异步执行结果
    Java多线程----创建线程、线程池ExecutorService、异步编排_第3张图片
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        System.out.println("start");
        // 构建FutureTask对象,实现Callable接口
        FutureTask<Integer> futureTask = new FutureTask<>(() -> {
            System.out.println(Thread.currentThread().getName() + "---running");
            // 返回异步执行结果
            return 10 / 2;
        });
        Thread thread = new Thread(futureTask);
        thread.start();
        // get()阻塞等待,获取异步返回值
        Integer result = futureTask.get();
        System.out.println("result = " + result);
        System.out.println("end");
    }

方式四、线程池ThreadPoolExecutor

创建ThreadPoolExecutor自定义线程池,然后通过execute()向线程池中提交任务
Java多线程----创建线程、线程池ExecutorService、异步编排_第4张图片

public class ThreadPool {

    public static final ThreadPoolExecutor threadPool;

    static{
        threadPool = new ThreadPoolExecutor(5,
                20,
                30,
                TimeUnit.SECONDS,
                new LinkedBlockingQueue<>(50),
                Executors.defaultThreadFactory(),
                new ThreadPoolExecutor.DiscardPolicy());
    }

    public static void main(String[] args) {
        threadPool.execute(()->{
            int i = 10 / 1;
            System.out.println(Thread.currentThread().getName() + "running....");
        });
    }
    
}

线程池的简单介绍

Java中线程池就是Executor或者ExecutorService对象实例

之前通过new Thread()的方式创建线程存在以下几个问题:

  • 不能重复利用线程,有多少任务就创建多少个线程
  • 如果需要处理大量任务,就需要频繁地创建和销毁线程会浪费时间和效率
  • 如果同一时刻存在大量的线程,那么线程之间还存在竞争资源,CPU上下切换等问题

线程池通过预先创建一定数量的线程,让这些线程处理来自任务队列中的任务,而不是频繁创建和销毁线程。任务执行完成后,线程不会被销毁,而是放回线程池中以供下一次使用,这避免了频繁创建和销毁线程的开销。同时,线程池还可以限制线程的数量,避免线程数量过多导致资源竞争、上下文切换等问题,从而提高程序的执行效率。

1、线程是一种宝贵的资源,因此使用线程池可以减少创建和销毁线程的次数,从而提高应用程序的性能。线程池中的工作线程可以重复利用,减少了线程的创建和销毁开销。
2、通过调整线程池中工作线程的数量,可以根据系统的承受能力来适配线程池,防止过多的内存消耗导致服务器崩溃。这可以提高应用程序的可靠性和稳定性。

通过ThreadPoolExecutor创建自定义线程池

public class ThreadPool {

    public static final ThreadPoolExecutor threadPool;

    static{
        // 创建自定义线程池
        threadPool = new ThreadPoolExecutor(5,
                20,
                30,
                TimeUnit.SECONDS,
                new LinkedBlockingQueue<>(50),
                Executors.defaultThreadFactory(),
                new ThreadPoolExecutor.DiscardPolicy());
    }

    public static void main(String[] args) {
        // 向线程池提交一个任务
        threadPool.execute(()->{
            int i = 10 / 1;
            System.out.println(Thread.currentThread().getName() + "running....");
        });
    }

}

ThreadPoolExecutor创建线程池的7大参数

Java多线程----创建线程、线程池ExecutorService、异步编排_第5张图片

    public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler)
参数名 含义 说明
corePoolSize 核心线程数 线程池初始化线程数为0;每提交一个任务就会新建一个线程,即使线程池中存在空闲的线程,但是只要线程数量没有达到该参数值,它也会创建一个新的线程来执行当前任务
maximumPoolSize 线程池能创建的最大线程数 线程池中线程数量能达到的最大值;当核心线程数已满,并且工作队列也已经存放满的情况下,才会去判断当前线程数是否小于maximumPoolSize,小于则继续创建线程处理任务maximumPoolSize
keepAliveTime 闲置超时时间 当线程池中的线程数corePoolSize的线程在经过keepAliveTime的时间依然没有任务执行,则销毁线程
unit 超时时间单位 参数keepAliveTime的时间单位
workQueue 工作队列 当核心线程们都在忙着处理任务,没有一个空闲的此时新提交的任务就会放到任务队列中,等待空闲的线程来执行
threadFactory 线程池创建新线程的工厂 常用来定义线程的名称,一般使用默认的Executors.defaultThreadFactory()即可
handler 拒绝策略 达到最大线程数的线程们没有空闲且工作队列已满,此时提交的新任务就可以使用该参数来进行相应的处理

线程池处理任务的执行流程

Java多线程----创建线程、线程池ExecutorService、异步编排_第6张图片

下面对线程做了区分:核心线程数和非核心线程数;但是它们本质都是现成根本就没有区别,只是叫法不同而已

线程池初始化线程数为0而不是corePoolSize;当任务被提交给线程池,首先判断核心线程数corePoolSize

  • 如果当前核心线程数量小于corePoolSize;就会重新创建新的核心线程来执行当前任务,即使在有空闲核心线程的情况下
  • 如果当前核心线程数量大于corePoolSize;就会考虑是否能暂时放入工作队列中。
    • 如果工作队列没满,就会将当前任务放入工作队列中等待空闲线程执行
    • 如果工作队列已满,就要考虑创建更多的线程(非核心线程),直至maximumPoolSize最大核心数
      • 核心线程正忙、队列已满的情况下才会一直持续创建非核心线程数直至最大线程数。所以当核心线程数 + 非核心线程数达到最大线程数的上限,那么这个任务只能执行拒绝策略的业务逻辑

假设:corePoolSize=5,maximumPoolSize=20,workQueue.capacity=50;当前100的并发任务,请简述线程池的处理流程
注意:这是高并发场景,根本不考虑出现空闲线程的情况

  1. 前5个任务会使线程池依次创建5个核心线程来执行任务
  2. 队列里还能塞50个任务,到这是55个任务
  3. 最大核心线程数是20,所以还会再依次创建15个线程来处理任务;到这是55 + 15 = 70个任务
  4. 剩下的30个任务只能执行拒绝策略;权衡利弊进行丢弃或同步run方法调用

四种RejectedExecutionHandler策略

Java多线程----创建线程、线程池ExecutorService、异步编排_第7张图片

AbortPolicy,丢弃并抛出异常

拒绝任务的处理程序,该处理程序将抛出RejectedExecutionException。这是ThreadPoolExecutorScheduledThreadPoolExecutor的默认处理程序。
Java多线程----创建线程、线程池ExecutorService、异步编排_第8张图片

DiscardPolicy,静默丢弃什么也不做

被拒绝任务的处理程序,它以静默方式丢弃被拒绝的任务。
Java多线程----创建线程、线程池ExecutorService、异步编排_第9张图片

DiscardOldestPolicy,丢弃最老后当前任务重新进池

被拒绝任务的处理程序,它丢弃最老(队列头部)的未处理请求,然后重试执行,如果线程池已被关闭,那么任务将被直接丢弃。
Java多线程----创建线程、线程池ExecutorService、异步编排_第10张图片

CallerRunsPolicy,同步调用run()执行任务

被拒绝任务的处理程序,它直接在execute()中调用被拒绝任务的run(),这样也可以执行任务,只不过是同步调用run()的方式;如果线程池已被关闭,那么任务将被直接丢弃。
Java多线程----创建线程、线程池ExecutorService、异步编排_第11张图片

Executors中四种常用的线程池

通常情况下通过ThreadPoolExecutor创建自定义线程池;也可以直接使用Executors工具中提供的一些已创建好的线程池。下面记录四种比较常用的线程池。
Java多线程----创建线程、线程池ExecutorService、异步编排_第12张图片

newCachedThreadPool

创建一个线程池,该线程池根据需要创建新线程,但在以前构造的线程可用时重用它们。这些池通常会提高执行许多短期异步任务程序的性能。执行调用将重用先前构造的线程(如果可用)。如果没有可用的现有线程,将创建一个新线程并将其添加到池中。60秒内未使用的线程将被终止并从缓存中删除。因此,该类型的池长时间保持空闲状态也不会消耗任何资源。请注意,可以使用ThreadPoolExecutor构造函数创建具有相似属性但不同细节(例如,超时参数)的池

    public static ExecutorService newCachedThreadPool() {
        return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                      60L, TimeUnit.SECONDS,
                                      new SynchronousQueue<Runnable>());
        // 最后缺失的两个参数将使用默认值                               
    }

特点:

  • 核心线程数为0,最大线程数为Integer最大值
  • 执行任务先复用线程,如果不能复用再创建新线程
  • 60s的空闲时间,因此不用担心没有任务的情况下资源浪费的问题
  • 通常会提高执行许多短期异步任务程序的性能

newFixedThreadPool

创建一个线程池,该线程池在无界队列上操作固定数量的线程,并在需要时使用提供的ThreadFactory创建新线程。核心线程数和最大线程数都是nThreads所以在任何时候,最多有nThreads线程处于活动状态处理任务。如果在所有线程都处于活动状态时提交额外的任务,它们将在队列中等待,直到其中一个线程可用。如果任何线程在关闭之前的执行过程中由于失败而终止,如果需要执行后续任务,则会有一个新线程取代它的位置(理解为:如果死了一个线程会立即创建一个新的线程来取代它的位置来执行后面的任务)。在显式关闭池之前,池中的线程将一直存在。

public static ExecutorService newFixedThreadPool(int nThreads, ThreadFactory threadFactory) {
        return new ThreadPoolExecutor(nThreads, nThreads,
                                      0L, TimeUnit.MILLISECONDS,
                                      new LinkedBlockingQueue<Runnable>(),
                                      threadFactory);
    }

特点

  • 固定的线程数量
  • 在池没有关闭之前会一直存活

newScheduledThreadPool

一个ThreadPoolExecutor,它可以额外安排命令在给定延迟后运行,或定期执行。当需要多个工作线程时,或者需要ThreadPoolExecutor(该类扩展)的额外灵活性或功能时,该类优于java.util.Timer。
延迟任务不会在启用后立即执行,但是在启用后,它们何时开始执行没有任何实时保证。为完全相同的执行时间安排的任务以先进先出(FIFO)的提交顺序启用。
当提交的任务在运行之前被取消时,执行将被抑制。默认情况下,这样一个被取消的任务不会自动从工作队列中删除,直到它的延迟结束。虽然这样可以进一步检查和监视,但也可能导致取消的任务无限制地保留。为了避免这种情况,使用setRemoveOnCancelPolicy使任务在取消时立即从工作队列中删除。
通过scheduleAtFixedRate或scheduleWithFixedDelay调度的周期性任务的连续执行不会重叠。虽然不同的线程可以执行不同的执行,但是之前执行的效果会在后续执行之前发生。
虽然这个类继承自ThreadPoolExecutor,但继承的一些调优方法对它没有用处。特别是,因为它使用corePoolSize线程和无界队列充当固定大小的池,所以对maximumPoolSize的调整没有任何有用的影响。此外,将corePoolSize设置为零或使用allowCoreThreadTimeOut几乎从来都不是一个好主意,因为这可能会使池中没有线程来处理任务,一旦它们有资格运行。
与ThreadPoolExecutor一样,如果没有特别指定,这个类使用Executors.defaultThreadFactory作为默认线程工厂,并且使用ThreadPoolExecutor。AbortPolicy作为默认的被拒绝执行处理程序。
扩展注意事项:该类覆盖execute和submit方法来生成内部schedulefuture对象,以控制每个任务的延迟和调度。为了保留功能,在子类中对这些方法的任何进一步重写都必须调用超类版本,这有效地禁用了额外的任务自定义。然而,这个类提供了另一种受保护的扩展方法decorateTask (Runnable和Callable各有一个版本),可用于自定义用于执行通过execute, submit, schedule, scheduleAtFixedRate和scheduleWithFixedDelay输入的命令的具体任务类型。默认情况下,ScheduledThreadPoolExecutor使用扩展FutureTask的任务类型。然而,这可以通过使用表单的子类来修改或替换:

    public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
        return new ScheduledThreadPoolExecutor(corePoolSize);
    }

newSingleThreadExecutor

创建一个在无界队列上操作单个工作线程的线程池。(但是请注意,如果这个单线程在关闭之前的执行过程中由于失败而终止(死亡了),如果需要执行后续任务,将会有一个新的线程取代它)。保证任务按顺序执行,并且在任何时间活动的线程数量不超过一个。与newFixedThreadPool(1)不同,返回的执行器保证不会被重新配置以使用其他线程(注释中这句话不理解,仅仅只是发现了它俩构造方法不同,如下图)。
Java多线程----创建线程、线程池ExecutorService、异步编排_第13张图片

特点:

  • 只有唯一的一个工作线程
  • 工作队列无边界,容易导致OOM
    public static ExecutorService newSingleThreadExecutor() {
        return new FinalizableDelegatedExecutorService
            (new ThreadPoolExecutor(1, 1,
                                    0L, TimeUnit.MILLISECONDS,
                                    new LinkedBlockingQueue<Runnable>()));
    }

上面源码可知核心线程数、最大线程数都是1,线程空闲回收时间配置也就没有意义了,所以给0,队列使用LinkedBlockingQueue这种无界的工作队列;剩余两个参数ThreadFactoryRejectedExecutionHandler都是用默认的
Java多线程----创建线程、线程池ExecutorService、异步编排_第14张图片

CompletableFuture异步编排

你可能感兴趣的:(Java,java,多线程,线程池,并发编程)