[译转]Java并发性:了解线程池和执行器
原文:https://www.codejava.net/java-core/concurrency/java-concurrency-understanding-thread-pool-and-executors 原文作者:Nam Ha Minh
该Java并发教程可帮助您开始使用java.util.concurrent 包中的高级并发API,该包提供了并发编程中通常有用的实用程序类,例如执行器(executors),线程池管理,计划任务执行,Fork / Join框架,等并发集合。
在本教程中,您将学习线程池的工作原理,以及如何通过执行器(executors)使用不同种类的线程池。
1.了解Java中的线程池
在性能方面,创建新线程是一项昂贵的操作,因为它要求操作系统为线程分配所需的资源。因此,实际上,线程池用于启动许多短期线程(short-lived threads)的大型应用程序,以便有效利用资源并提高性能。
线程池会保留许多空闲线程,这些线程准备好根据需要执行任务,而不是在新任务到达时创建新线程。线程完成任务的执行后,它不会消失。相反,它在池中保持空闲状态,等待被选择执行新的任务。
您可以限制池中一定数量的并发线程,这对于防止过载非常有用。如果所有线程都在忙于执行任务,则将新任务放入队列中,等待线程可用。
Java Concurrency API支持以下类型的线程池:
缓存线程池(Cached thread pool):保留多个活动线程并根据需要创建新线程。
固定线程池(Fixed thread pool:):限制并发线程的最大数量。其他任务在队列中排队。
单线程池(Single-threaded pool):一次只保留一个线程执行一个任务。
Fork / Join池(Fork/Join pool):一种特殊的线程池,它使用Fork / Join框架来利用多个处理器的优势,通过将工作递归分解成较小的部分来更快地执行繁重的工作。
基本上这就是线程池的工作方式。在实践中,线程池广泛用于Web服务器,其中,线程池用于服务客户端的请求。线程池还用于数据库应用程序,其中线程池维护与数据库的开放连接。
实现线程池是一项复杂的任务,但是您不必自己做。由于Java Concurrency API允许您轻松创建和使用线程池,而无需担心细节。您将在下一节中学习如何操作。
2.了解Java中的执行器(Executors)
一个执行器是一个对象,是负责管理线程和执行从客户端代码提交的Runnable任务。它使线程创建,调度等细节与任务提交脱钩,因此您可以专注于开发任务的业务逻辑,而无需关心线程管理细节。
这意味着,在最简单的情况下,而不是创建线程来执行如下任务:
Thread t = new Thread(new RunnableTask()); t.start();
您将任务提交给执行者,如下所示:
Executor executor = anExecutorImplementation; executor.execute(new RunnableTask1()); executor.execute(new RunnableTask2());
Java Concurrency API为执行者定义了以下3个基本接口:
Executor:是所有executors的超类。它仅定义了一种方法execute(Runnable)。
ExecutorService:是一个Executor,它允许通过Future对象跟踪返回值任务(Callable)的进度,并管理线程的终止。它的关键方法包括Submit()和shutdown()。
ScheduledExecutorService:是一个ExecutorService,可以安排任务在给定的延迟后执行或定期执行。它的关键方法是schedule(),scheduleAtFixedRate()和scheduleWithFixedDelay()。
您可以使用Executors实用程序类(utility class)提供的几种工厂方法之一来创建executor 程序。这里仅举几例:
newCachedThreadPool():创建一个可扩展的线程池执行程序。根据需要创建新线程,并在可用时重新使用先前构造的线程。空闲线程在池中保留一分钟。该执行程序适用于启动许多短期并发任务(short-lived concurrent tasks)的应用程序。
newFixedThreadPool(int n):在池中创建具有固定线程数的执行程序。该执行程序可确保在任何时候不超过n个并发线程。如果在所有线程都处于活动状态时提交了其他任务,则它们将在队列中等待,直到某个线程可用为止。如果任何线程由于执行过程中的失败而终止,它将被新线程替换。池中的线程将一直存在,直到明确将其关闭。如果您要限制并发线程的最大数量,请使用此执行程序。
newSingleThreadExecutor():创建一个执行程序,一次执行一个任务。确保已提交的任务按顺序执行,并且在任何时候都不会激活一个以上的任务。如果您要依次执行要排队的任务,请考虑使用此执行程序。
newScheduledThreadPool(int corePoolSize):创建一个执行程序,该执行程序可以安排任务在给定的延迟后执行或定期执行。如果要调度要并发执行的定时任务,请考虑使用此执行程序。
newSingleThreadScheduleExecutor():创建一个单线程执行器,该执行器可以安排任务在给定的延迟后执行或定期执行。如果要定时任务(schedule tasks )按顺序执行,请考虑使用此执行程序。
如果工厂方法不能满足您的需求,则可以直接将执行程序构造为ThreadPoolExecutor或ScheduledThreadPoolExecutor的实例,这为您提供了其他选项,例如池大小,按需构造,保持活动时间等。
要创建Fork / Join池,请构造ForkJoinPool类的实例。
3. Java Simple Executor和ExecutorService示例
让我们看几个简单的示例,这些示例显示如何创建执行器(executor )来执行Runnable任务和Callable任务。
以下程序为您展示了一个由单线程执行器(single-threaded executor)执行任务的简单示例:
import java.util.concurrent.*; /** * SimpleExecutorExample.java * This program demonstrates how to create a single-threaded executor * to execute a Runnable task. * @author www.codejava.net */ public class SimpleExecutorExample { public static void main(String[] args) { ExecutorService pool = Executors.newSingleThreadExecutor(); Runnable task = new Runnable() { public void run() { System.out.println(Thread.currentThread().getName()); } }; pool.execute(task); pool.shutdown(); } }
如您所见,Runnable任务是使用匿名类语法创建的。该任务仅打印线程名称并终止。编译并运行该程序,您将看到如下输出:
pool-1-thread-1
请注意,线程完成执行后,应调用shutdown()销毁执行程序。否则,程序随后仍将运行。您可以通过注释关闭请求来观察(observe)此行为。
以下程序展示了如何向执行器提交Callable任务。一个可调用(Callable)任务返回完成后的值,我们使用Future对象获取值。下面是代码:
import java.util.concurrent.*; /** * SimpleExecutorServiceExample.java * This program demonstrates how to create a single-threaded executor * to execute a Callable task. * @author www.codejava.net */ public class SimpleExecutorServiceExample { public static void main(String[] args) { ExecutorService pool = Executors.newSingleThreadExecutor(); Callable<Integer> task = new Callable<Integer>() { public Integer call() { try { // fake computation time Thread.sleep(5000); } catch (InterruptedException ex) { ex.printStackTrace(); } return 1000; } }; Future<Integer> result = pool.submit(task); try { Integer returnValue = result.get(); System.out.println("Return value = " + returnValue); } catch (InterruptedException | ExecutionException ex) { ex.printStackTrace(); } pool.shutdown(); } }
请注意,Future的get()方法将阻塞当前线程,直到任务完成并返回值。运行该程序,5秒钟后您将看到以下输出:
Return value = 1000
有关使用Callable和Future执行任务的更多详细信息,请参见Java并发:使用Callable和Future执行返回值的任务。
让我们看一个更复杂的示例,其中向您展示如何使用不同类型的执行器执行多个任务。
4. Java缓存线程池执行器示例
以下示例显示了如何创建缓存的线程池(cached thread pool )以同时执行一些任务。给定以下类:
/** * CountDownClock.java * This class represents a coutdown clock. * @author www.codejava.net */ public class CountDownClock extends Thread { private String clockName; public CountDownClock(String clockName) { this.clockName = clockName; } public void run() { String threadName = Thread.currentThread().getName(); for (int i = 5; i >= 0; i--) { System.out.printf("%s -> %s: %d\n", threadName, clockName, i); try { Thread.sleep(1000); } catch (InterruptedException ex) { ex.printStackTrace(); } } } }
此类表示一个倒计时时钟,该时钟将从5向下计数到0,并在每次计数后暂停1秒。运行时,它将打印当前线程名称,后跟时钟名称和计数编号。
让我们创建一个带有缓存线程池的执行器,以同时执行4个时钟。下面是代码:
import java.util.concurrent.*; /** * MultipleTasksExecutorExample.java * This program demonstrates how to execute multiple tasks * with different kinds of executors. * @author www.codejava.net */ public class MultipleTasksExecutorExample { public static void main(String[] args) { ExecutorService pool = Executors.newCachedThreadPool(); pool.execute(new CountDownClock("A")); pool.execute(new CountDownClock("B")); pool.execute(new CountDownClock("C")); pool.execute(new CountDownClock("D")); pool.shutdown(); } }
编译并运行该程序,您将看到有4个线程同时执行4个时钟:
修改该程序以添加更多任务,例如增加3个时钟。重新编译并再次运行该程序,您将看到线程数与提交的任务数相等。这是缓存的线程池的关键行为:根据需要创建新线程。
5. Java固定线程池执行器示例
接下来,以固定线程池(fixed thread pool)的语句更新创建执行器:
ExecutorService pool = Executors.newFixedThreadPool(2);
在这里,我们创建一个执行器,该执行器具有最多2个并发线程池。仅将4个任务(4个时钟)提交给执行者。重新编译并运行程序,您将看到只有2个线程执行时钟:
时钟A和B首先运行,而时钟C和D在队列中等待。在A和B完成执行之后,这2个线程继续执行时钟C和D。这是固定线程池(a fixed thread pool)的关键行为:限制并发线程数并排队其他任务。
6. Java单线程池执行器示例
让我们以使用像这样的单线程执行器( a single-threaded executor)更新上面的程序:
ExecutorService pool = Executors.newSingleThreadExecutor();
重新编译并运行程序,您将看到只有一个线程按顺序执行4个时钟:
这就是单线程执行器(a single-threaded executor)的关键行为:将任务依次依次排队执行。
7.创建一个自定义线程池执行器
如果希望更好地控制线程池的行为,可以直接从ThreadPoolExecutor类而不是Executors实用程序类的工厂方法创建线程池执行器(a thread pool executor)。
例如,ThreadPoolExecutor具有如下通用构造函数:
public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue)
只要您真正了解参数的含义,就可以对其进行调整:
corePoolSize:要保留在池中的线程数。
maximumPoolSize:池中允许的最大线程数。
keepAliveTime:如果当前池中有多个corePoolSize线程,则多余的线程将在空闲状态超过keepAliveTime时终止。
unit:keepAliveTime参数的时间单位。可以是NANOSECONDS,MILLISECONDS,SECONDS,MINUTES,HOURS和DAYS。
workQueue:用于在执行任务之前保留任务的队列。默认选择是对于多线程池为SynchronousQueue,对于单线程池为LinkedBlockingQueue。
让我们来看一个例子。以下代码创建一个缓存的线程池(a cached thread pool),该线程池至少保留10个线程,最多允许1,000个线程,并且空闲线程在该池中保留120秒:
int corePoolSize = 10; int maxPoolSize = 1000; int keepAliveTime = 120; BlockingQueue<Runnable> workQueue = new SynchronousQueue<Runnable>(); ThreadPoolExecutor pool = new ThreadPoolExecutor(corePoolSize, maxPoolSize, keepAliveTime, TimeUnit.SECONDS, workQueue); pool.execute(new RunnableTask());
您可以看到,当corePoolSize = maxPoolSize = 1时,我们得到一个单线程池执行器。
API References:
Related Tutorials:
Other Java Concurrency Tutorials:
How to use Threads in Java (create, start, pause, interrupt and join)
Understanding Deadlock, Livelock and Starvation with Code Examples in Java
- 点赞
- 收藏
- 关注作者
评论(0)