03_concurrency_09_thread_pool_and_service

第四十三章 线程池(Thread Pool)和ExecutorService接口

1 概述

线程池是多线程程序设计中一个常用的设计模式(Design Pattern)。在线程池模型(Threading Pool Pattern)下,程序预先启动多个线程,并将其放入线程池中。当接到任务时,将部分线程从池中“取出”,并将任务分配给它们运行。当任务执行完毕后,再将线程对象“还回”线程池。

使用线程池有诸多好处:

  1. 使用线程池能有效减少线程启动和停止的开销,缩短任务运行的时间。假设,当每个任务需要在独立的线程内运行时,如果在接收到新任务时才启动一个新线程的话,线程的启动时间是算在任务运行时间内的。所以,因为线程池事先已启动了线程,可以避免任务等待新线程启动的时间。
  2. 最高效的利用计算资源。在给定的运行环境中,使用少量的线程并不一定能完全利用所有的计算资源。而启动过多的线程则会耗费较多的计算资源来处理线程管理和线程切换。所以,如果为每个新任务而启动新线程处理的话,由于任务的随机性,通常情况下并不能合理高效的利用计算资源。线程池能较好的解决这个问题。开发人员可以设置一个固定的线程数;当任务较少时,空闲的线程不会占用和浪费很多的计算资源。而当任务量较重时,多出的任务需要等待,以使得运行环境保持在最高效的运行状态。

因此,线程池模型常常用于大型的应用程序中。以Web服务器为例。Apache TomcatEclipse Jetty是两个常见的Java Web服务器。在启动过程中,它们会创建一个线程池,用于处理用户请求。当它们接收到一个用户请求后,它们会将这个请求分配给一个空闲的线程处理。因为线程都已经事先创建好了,所以,在处理请求之前无需创建临时线程,缩短了处理请求的时间。因为用户请求的不可预测性,当用户请求较多且所有线程均处于运行中时,其他的线程需要等待,以避免因过多线程同时运行而造成的资源浪费。当线程过多时,维护线程的开销也会快速增长。因此,使用线程池既能较快的处理请求,缩短用户的等待时间;也能高效的使用服务器资源,避免过载运行。

(例如:web服务器)。

2 Java线程池

Java在标准库中提供了线程池的实现。在介绍线程池之前,我们需要先介绍几个概念。

2.1 Executor接口

java.util.concurrent.Executor是一个接口。该接口接收一个Runnable对象,并运行该对象。Java标准库使用Executor接口,将线程运行的代码(Runnable)和如何运行和管理这个线程(Executor)分离在两个接口中。所以,在线程中完成什么工作是在Runnable对象中描述的;而如何启动线程,如何调度线程则是通过使用Executor接口实现的。除了使用Thread创建并运行一个Runnable对象以外,Executor也提供了execute()方法,启动运行一个新的线程。如下面的代码所示:

Executor executor = Executors.newCachedThreadPool();
executor.execute(new Runnable() {
    public void run() {
        System.out.println("This is a Runnable object.");
    }
};

2.2 ExecutorService接口

java.util.concurrent.ExecutorService接口继承了Executor接口,并提供了更多的运行任务的方法。大致上讲,ExecutorService表示的是一个可以接收并运行任务的服务。所有的任务在ExecutorService中是异步完成的。换句话说,当向ExecutorService提交一个或者多个任务时,提交任务的方法立即返回一个Future对象。所以,在任务提交之后,任务在ExecutorService中运行;任务的提交者可以通过Future对象随时查看任务状态和执行结果。ExecutorService接收Callable任务或者Runnable任务

如下面的代码所示,ExecutorService可以一次接收一个任务,也可以一次接收多个任务。submit()方法可以接收一个Runnable对象任务,或者一个Callable对象任务,并返回一个Future对象。invokeAll()和invokeAny()方法可以接收一组Callable对象任务。invokeAll()方法会同时启动所有提交的任务开始运行;而invokeAny()只启动运行其中一个任务。

import java.util.List;
import java.util.Arrays;
import java.util.concurrent.Executor;
import java.util.concurrent.Callable;
import java.util.concurrent.Future;
import java.util.concurrent.Executors;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.ExecutionException;

public class ExecutorServiceExample {
    public static void main(String[] args) {
        // 创建一个ExecutorService对象
        ExecutorService executor = Executors.newCachedThreadPool();

        // 创建一个Runnable对象和两个Callable对象
        Runnable runnable1 = ()->{return;};
        Callable<String> callable1 = ()->{return "this is Callable 1.";};
        Callable<String> callable2 = ()->{return "this is Callable 2.";};

        // 一次只提交一个Runnable对象或者Callable对象
        Future future1 = executor.submit(runnable1);
        Future<String> future2 = executor.submit(callable2);

        try {
            // 一次提交多个Callable任务
            List<Future<String>> futures = executor.invokeAll(Arrays.asList(callable1, callable2));
            
            // 一次提交多个任务中的一个
            String result = executor.invokeAny(Arrays.asList(callable1, callable2));

            Thread.sleep(1000);

            System.out.println("The result of callable 2 is " + future2.get());

            System.out.println("Who has been executed? " + result);

            // 运行完毕,准备退出
            executor.shutdown();
        } catch (InterruptedException | ExecutionException ex) {}
    }
}

程序运行结果如下:

> java ExecutorServiceExample
The result of callable 2 is this is Callable 2.
Who has been executed? this is Callable 1.

在本例中,变量executor实际上引用的是一个线程池。使用线程池简化了编码的复杂度,因为,任务的生产者(main线程)无需知晓如何运行任务才能更高的利用计算资源,尽快的完成所有的任务。生产者只需专注于如何生产任务,并将任务提交给线程池即可。

ExecutorService接口还提供了shutdown()和shutdownNow()方法停止任务的运行。shutdownNow()方法会尝试立即停止任务的运行,并返回所有处于等待状态的任务对象。正在运行的任务也会被停止。但是,shutdownNow()并不能保证立即停止所有的任务。shutdownNow()仅仅是尽最大努力立即停止。而shutdown()方法不会引发立即停止。它会停止接收新的任务,并等待当前运行的任务执行完毕。

2.3 线程池ThreadPoolExecutor

java.util.concurrent.ThreadPoolExecutor是一个线程池类。它实现了ExecutorService接口。所以,ThreadPoolExecutor一次可以接收一个或者多个任务,并让其运行于线程池中。

ThreadPoolExecutor的实现比传统的线程池更加灵活。总的来说,ThreadPoolExecutor可以由三个参数来描述:corePoolSize,maximumPoolSize,和keepAliveTime。在ThreadPoolExecutor中,线程池维护着固定数量的核心线程(Core Thread)。核心线程的数量由corePoolSize决定。在繁忙时,ThreadPoolExecutor还允许额外创建辅助线程,放入线程池中。线程池中线程的最大数由maximumPoolSize确定。当所有核心线程都很忙,而且还有新的任务生成时,ThreadPoolExecutor会创建辅助线程帮助执行。但是,当任务提交的高峰期过去之后,辅助线程会空闲下来。当其空闲时间超过keepAliveTime时,该辅助线程会被停止。因此,ThreadPoolExecutor是一个动态的线程池。下面是一个使用ThreadPoolExecutor的例子。

import java.util.concurrent.ThreadPoolExecutor;

public class ThreadPoolExecutorExample {
    public static void main(String[] args) {
        ThreadPoolExecutor threadPool = new ThreadPoolExecutor(5, 10, 1000);
        Runnable runnable = ()->0;
        threadPool.submit(runnable);
    }
}

2.4 ScheduledThreadPoolExecutor

在ThreadPoolExecutor的基础上,java.util.concurrent.ScheduledThreadPoolExecutor增加了延迟任务(Delayed Task)和周期任务(Periodical Task)。延迟任务是指任务在提交之后,并不立即执行,而是需要等待一段时间之后再执行。周期任务是指被反复执行的任务。周期任务可由两种方法描述其执行计划。一种是固定频率(Fixed Rate)。这种是指当任务开始执行后,每隔一段时间,该任务被执行一次(例如,每日执行一次)。另一种描述方法是时间间隔。即当任务执行完毕之后,等待一个固定长度的时间之后,再次运行这项任务(例如,当任务完毕后,等待5分钟再次运行该任务)。如下面的代码所示:

import java.util.concurrent.ScheduledExecutorService;

public class ScheduledThreadPoolExecutorExample {
    public static void main(String[] args) {
        ScheduledExecutorService executor = new ScheduledExecutorService(5);
        Runnable task = ()->0;

        // 提交任务,并等待10秒后运行该任务
        executor.schedule(task, 10, TimeUnit.SECONDS);

        // 提交任务,并等待10秒后首次运行该任务,然后再每60秒运行一次该任务
        executor.scheduleAtFixedRate(task, 10, 60, TimeUnit.SECONDS);

        // 提交任务,并等待10秒首次运行该任务。在每次任务结束后,等待60秒,再次启动任务
        executor.scheduleWithFixedDelay(task, 10, 60, TimeUnit.SECONDS);
    }
}

2.5 Executors

java.util.concurrent.Executors是创建ExecutorService的工厂类,或者称之为帮助类。由Executors可以创建出不同种类的ExecutorService。如下面的代码所示:

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class ExecutorsExample {
    public static void main(String[] args) {
        // 创建一个ThreadPoolExecutor对象
        ExecutorService executor = Executors.newCachedThreadPool();

        // 创建一个SheduledThreadPoolExecutor对象
        ExecutorService scheduledExecutor = Executors.newScheduledThreadPool(5);
        
        // 创建只使用一个线程的ExecutorService对象
        ExecutorService singleThreadExecutor = Executors.newSingleThreadScheduledExecutor();
    }
}

4 总结

本章节介绍了Java标准库中异步执行任务的线程池概念。其中,Executor接口将如何启动运行线程和线程内部逻辑分离开。ThreadPoolExecutor类提供了灵活的线程池功能。在此基础上,ScheduledThreadPoolExecutor类提供了延迟任务和周期任务的执行。在下一章节中,我们还会继续介绍Java标准库提供的线程同步的内容。

上一章
下一章

注册用户登陆后可留言

Copyright  2019 Little Waterdrop, LLC. All Rights Reserved.