[译转]Java并发性:了解线程池和执行器

举报
Amrf 发表于 2020/10/27 10:10:06 2020/10/27
【摘要】 原文:https://www.codejava.net/java-core/concurrency/java-concurrency-understanding-thread-pool-and-executors 原文作者:Nam Ha Minh该Java并发教程可帮助您开始使用java.util.concurrent 包中的高级并发API,该包提供了并发编程中通常有用的实用程序类,例如执行...

原文: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 )按顺序执行,请考虑使用此执行程序。


如果工厂方法不能满足您的需求,则可以直接将执行程序构造为ThreadPoolExecutorScheduledThreadPoolExecutor的实例,这为您提供了其他选项,例如池大小,按需构造,保持活动时间等。


要创建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();
	}
}

请注意,Futureget()方法将阻塞当前线程,直到任务完成并返回值。运行该程序,5秒钟后您将看到以下输出:

Return value = 1000

有关使用CallableFuture执行任务的更多详细信息,请参见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个时钟:


image.png


修改该程序以添加更多任务,例如增加3个时钟。重新编译并再次运行该程序,您将看到线程数与提交的任务数相等。这是缓存的线程池的关键行为:根据需要创建新线程。


5. Java固定线程池执行器示例

接下来,以固定线程池(fixed thread pool)的语句更新创建执行器:

ExecutorService pool = Executors.newFixedThreadPool(2);

在这里,我们创建一个执行器,该执行器具有最多2个并发线程池。仅将4个任务(4个时钟)提交给执行者。重新编译并运行程序,您将看到只有2个线程执行时钟:


image.png


时钟A和B首先运行,而时钟C和D在队列中等待。在A和B完成执行之后,这2个线程继续执行时钟C和D。这是固定线程池(a fixed thread pool)的关键行为:限制并发线程数并排队其他任务。


6. Java单线程池执行器示例

让我们以使用像这样的单线程执行器( a single-threaded executor)更新上面的程序:

ExecutorService pool = Executors.newSingleThreadExecutor();

重新编译并运行程序,您将看到只有一个线程按顺序执行4个时钟:

image.png

这就是单线程执行器(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时终止

  • unitkeepAliveTime参数的时间单位可以是NANOSECONDSMILLISECONDSSECONDSMINUTESHOURSDAYS

  • 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:


【版权声明】本文为华为云社区用户翻译文章,如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容, 举报邮箱:cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。