Java学习day098 并发(八)(执行器:线程池、预定执行、控制任务组、Fork-Join框架、可完成Future)

使用的教材是java核心技术卷1,我将跟着这本书的章节同时配合视频资源来进行学习基础java知识。

day098   并发(八)(执行器:线程池、预定执行、控制任务组、Fork-Join框架、可完成Future)

构建一个新的线程是有一定代价的,因为涉及与操作系统的交互。如果程序中创建了大量的生命期很短的线程,应该使用线程池(thread pool)。一个线程池中包含许多准备运行的空闲线程。将Runnable对象交给线程池,就会有一个线程调用run方法。当run方法退出时,线程不会死亡,而是在池中准备为下一个请求提供服务。

另一个使用线程池的理由是减少并发线程的数目。创建大量线程会大大降低性能甚至使虚拟机崩溃。如果有一个会创建许多线程的算法,应该使用一个线程数“固定的”线程池以限制并发线程的总数。

执行器(Executor)类有许多静态工厂方法用来构建线程池,下表中对这些方法进行了汇总。


1.线程池

newCachedThreadPool方法构建了一个线程池,对于每个任务,如果有空闲线程可用,立即让它执行任务,如果没有可用的空闲线程,则创建一个新线程。newFixedThreadPool方法构建一个具有固定大小的线程池。如果提交的任务数多于空闲的线程数,那么把得不到服务的任务放置到队列中。当其他任务完成以后再运行它们。newSingleThreadExecutor是一个退化了的大小为1的线程池:由一个线程执行提交的任务,一个接着一个。这3个方法返回实现了ExecutorService接口的ThreadPoolExecutor类的对象。

可用下面的方法之一将一个Runnable对象或Callable对象提交给ExecutorService:

Future<?>submit(Runnable task)
Future<T>submit(Runnable task, T result)
Future<T>submit(Callable<T> task)

该池会在方便的时候尽早执行提交的任务。调用submit时,会得到一个Future对象,可用来查询该任务的状态。

第一个submit方法返回一个奇怪样子的Future。可以使用这样一个对象来调用isDone、cancel或isCancelled。但是,get方法在完成的时候只是简单地返回null。

第二个版本的Submit也提交一个Runnable,并且Future的get方法在完成的时候返回指定的result对象。

第三个版本的Submit提交一个Callable,并且返回的Future对象将在计算结果准备好的时候得到它。

当用完一个线程池的时候,调用shutdown。该方法启动该池的关闭序列。被关闭的执行器不再接受新的任务。当所有任务都完成以后,线程池中的线程死亡。另一种方法是调用shutdownNow。该池取消尚未开始的所有任务并试图中断正在运行的线程。下面总结了在使用连接池时应该做的事:

1)调用Executors类中静态的方法newCachedThreadPool或newFixedThreadPool。

2)调用submit提交Runnable或Callable对象。

3)如果想要取消一个任务,或如果提交Callable对象,那就要保存好返回的Future对象。

4)当不再提交任何任务时,调用shutdown。

例如,前面的程序例子产生了大量的生命期很短的线程,每个目录产生一个线程。下面的的程序使用了一个线程池来运行任务。出于信息方面的考虑,这个程序打印出执行中池中最大的线程数。但是不能通过ExecutorService这个接口得到这一信息。因此,必须将该pool对象强制转换为ThreadPoolExecutor类对象。

/**
 *@author  zzehao
 */
import java.io.*; 
import java.util.*; 
import java.util.concurrent.*;

public class ThreadPoolTest
{
	public static void main(String[] args)
	{
		try(Scanner in = new Scanner(System.in))
		{
			System.out.print("Enter base directory (e.g. /opt/jdkl.8.0/src): ");
			String directory = in.nextLine();
			System.out.print("Enter keyword (e.g. volatile): ");
			String keyword= in.nextLine();

			ExecutorService pool = Executors.newCachedThreadPool();

			MatchCounter counter = new MatchCounter(new File(directory), keyword,pool);
			Future<Integer> result = pool.submit(counter);

			try
			{
				System.out.println(result.get()+ " matching files.");
			}
			catch (ExecutionException e)
			{
				e.printStackTrace();
			}
			catch (InterruptedException e)
			{
			}

			pool.shutdown();

			int largestPoolSize = ((ThreadPoolExecutor) pool).getLargestPoolSize();
			System.out.println("largest pool size=" + largestPoolSize);
		}
	}
}

//This task counts the files in a directory and its subdirectories that contain a given keyword.
class MatchCounter implements Callable<Integer>
{
	private File directory;
	private String keyword;
	private ExecutorService pool;
	private int count;

	//Constructs a MatchCounter.
	public MatchCounter(File directory, String keyword, ExecutorService pool)
	{
		this.directory = directory;
		this.keyword = keyword;
		this.pool = pool;
	}

	public Integer call()
	{
		int count = 0;
		try
		{
			File[] files =directory.listFiles();
			List<Future<Integer>> results = new ArrayList<>();

			for (File file : files)
			{
				if (file.isDirectory())
				{
					MatchCounter counter = new MatchCounter(file, keyword,pool);
					Future<Integer> result = pool.submit(counter);
					results.add(result);
				}
				else
				{
					if (search(file))
						count++;
				}
			}

			for (Future<Integer> result : results)
			{
				try
				{
					count += result.get();
				}
				catch (ExecutionException e)
				{
					e.printStackTrace();
				}
			}
		}
		catch (InterruptedException e)
		{
		}
		return count;
	}

	//Searches a file for a given keyword.
	public boolean search(File file)
	{
		try
		{
			try (Scanner in = new Scanner(file, "UTF-8"))
			{
				boolean found = false;
				while (!found && in.hasNextLine())
				{
					String line = in.nextLine();
					if (line.contains(keyword)) 
						found = true;
				}
				return found;
			}
		}
		catch (IOException e)
		{
			return false;
		}
	}
}

运行的结果是(自行输入):


2.预定执行

ScheduledExecutorService接口具有为预定执行(ScheduledExecution)或重复执行任务而设计的方法。它是一种允许使用线程池机制的java.util.Timer的泛化。Executors类的newScheduledThreadPool和newSingleThreadScheduledExecutor方法将返回实现了Scheduled¬ExecutorService接口的对象。

可以预定Runnable或Callable在初始的延迟之后只运行一次。也可以预定一个Runnable对象周期性地运行。详细内容见API文档。


3.控制任务组

了解了如何将一个执行器服务作为线程池使用,以提高执行任务的效率。有时,使用执行器有更有实际意义的原因,控制一组相关任务。例如,可以在执行器中使用shutdownNow方法取消所有的任务。

invokeAny方法提交所有对象到一个Callable对象的集合中,并返回某个已经完成了的任务的结果。无法知道返回的究竟是哪个任务的结果,也许是最先完成的那个任务的结果。对于搜索问题,如果你愿意接受任何一种解决方案的话,你就可以使用这个方法。例如,假定你需要对一个大整数进行因数分解计算来解码RSA密码。可以提交很多任务,每一个任务使用不同范围内的数来进行分解。只要其中一个任务得到了答案,计算就可以停止了。

invokeAll方法提交所有对象到一个Callable对象的集合中,并返回一个Future对象的列表,代表所有任务的解决方案。当计算结果可获得时,可以像下面这样对结果进行处理:

List<Callab1e<T>> tasks = . ..;
List<Future<T>> results = executor.invokeAll(tasks);
for (Future<T> result : results)
processFurther(result.get());

这个方法的缺点是如果第一个任务恰巧花去了很多时间,则可能不得不进行等待。将结果按可获得的顺序保存起来更有实际意义。可以用ExecutorCompletionService来进行排列。

用常规的方法获得一个执行器。然后,构建一个ExecutorCompletionService,提交任务给完成服务(completionservice)。该服务管理Future对象的阻塞队列,其中包含已经提交的任务的执行结果(当这些结果成为可用时)。这样一来,相比前面的计算,一个更有效的组织形式如下:

ExecutorCompletionService<T> service = new ExecutorCompletionServiceo(executor):
for (Callable<T> task : tasks) service.submit(task);
    for (int i = 0; i < tasks.size();i++)
        processFurther(service.take().get());


4.Fork-Join框架

有些应用使用了大量线程,但其中大多数都是空闲的。举例来说,一个Web服务器可能会为每个连接分别使用一个线程。另外一些应用可能对每个处理器内核分别使用一个线程,来完成计算密集型任务,如图像或视频处理。JavaSE7中新引入了fork-join框架,专门用来支持后一类应用。假设有一个处理任务,它可以很自然地分解为子任务,如下所示:

if (problemSize < threshold)
    solve problem directly
else
{
    break problem into subproblems
    recursively solveeach subproblem
    combine the results
}

图像处理就是这样一个例子。要增强一个图像,可以变换上半部分和下部部分。如果有足够多空闲的处理器,这些操作可以并行运行。(除了分解为两部分外,还需要做一些额外的工作,不过这属于技术细节,我们不做讨论)。

在这里,我们将讨论一个更简单的例子。假设想统计一个数组中有多少个元素满足某个特定的属性。可以将这个数组一分为二,分别对这两部分进行统计,再将结果相加。

要采用框架可用的一种方式完成这种递归计算,需要提供一个扩展RecursiveTask<T>的类(如果计算会生成一个类型为T的结果)或者提供一个扩展RecursiveActicm的类(如果不生成任何结果)。再覆盖compute方法来生成并调用子任务,然后合并其结果。

public Counter(double[] values,int from,int to,DoublePredicate filter)
	{
		this.values = values;
		this.from = from;
		this.to = to;
		this.filter = filter;
	}
	protected Integer compute()
	{
		if(to - from < THRESHOLD)
		{
			int count = 0;
			for (int i = from;i < to;i++)
			{
				if (filter.test(values[i])) 
					count++;
			}
			return count;
		}
		else
		{
			int mid = (from + to)/2;
			Counter first = new Counter(values, from, mid, filter);
			Counter second = new Counter(values, mid, to, filter);
			invokeAll(first, second);
			return first.join()+second.join();
		}
	}

在这里,invokeAll方法接收到很多任务并阻塞,直到所有这些任务都已经完成。join方法将生成结果。我们对每个子任务应用了join,并返回其总和。

下面是完整的代码。在后台,fork-join框架使用了一种有效的智能方法来平衡可用线程的工作负载,这种方法称为工作密取(workstealing)。每个工作线程都有一个双端队列(deque)来完成任务。一个工作线程将子任务压人其双端队列的队头。(只有一个线程可以访问队头,所以不需要加锁。)一个工作线程空闲时,它会从另一个双端队列的队尾“密取”一个任务。由于大的子任务都在队尾,这种密取很少出现。

/**
 *@author  zzehao
 */
import java.util.concurrent.*;
import java.util.function.*;

//This program demonstrates the fork-join framework.
public class ForkJoinTest
{
	public static void main(String[] args)
	{
		final int SIZE = 10000000;
		double[] numbers = new double[SIZE];
		for(int i=0;i < SIZE;i++)
			numbers[i] = Math.random();
		Counter counter = new Counter(numbers,0,numbers.length,x -> x > 0.5);
		ForkJoinPool pool = new ForkJoinPool();
		pool.invoke(counter);
		System.out.println(counter.join());
	}
}

class Counter extends RecursiveTask<Integer>
{
	public static final int THRESHOLD = 1000;
	private double[] values;
	private int from;
	private int to;
	private DoublePredicate filter;
	
	public Counter(double[] values,int from,int to,DoublePredicate filter)
	{
		this.values = values;
		this.from = from;
		this.to = to;
		this.filter = filter;
	}
	protected Integer compute()
	{
		if(to - from < THRESHOLD)
		{
			int count = 0;
			for (int i = from;i < to;i++)
			{
				if (filter.test(values[i])) 
					count++;
			}
			return count;
		}
		else
		{
			int mid = (from + to)/2;
			Counter first = new Counter(values, from, mid, filter);
			Counter second = new Counter(values, mid, to, filter);
			invokeAll(first, second);
			return first.join()+second.join();
		}
	}
}

运行的结果是:


5.可完成Future

处理非阻塞调用的传统方法是使用事件处理器,程序员为任务完成之后要出现的动作注册一个处理器。当然,如果下一个动作也是异步的,在它之后的下一个动作会在一个不同的事件处理器中。尽管程序员会认为“先做步骤1,然后是步骤2,再完成步骤3”,但实际上程序逻辑会分散到不同的处理器中。如果必须增加错误处理,情况会更糟糕。假设步骤2是“用户登录”。可能需要重复这个步骤,因为用户输入凭据时可能会出错。要尝试在一组事件处理器中实现这样一个控制流,或者想要理解所实现的这样一组事件处理器,会很有难度。

JavaSE8的CompletableFuture类提供了一种候选方法。与事件处理器不同,“可完成future"可以“组合”(composed)。

例如,假设我们希望从一个Web页面抽取所有链接来建立一个网络爬虫。下面假设有这样一个方法:

public void CorapletableFuture<String> readPage(URL url)

Web页面可用时这会生成这个页面的文本。如果方法:

public static List<URL> getLinks(String page)

生成一个HTML页面中的URL,可以调度当页面可用时再调用这个方法:

ConipletableFuture<String> contents=readPage(url);

CompletableFuture<List<URL>> links = contents.thenApply(Parser::getLinks);

thenApply方法不会阻塞。它会返回另一个fiiture。第一个fiiture完成时,其结果会提供给getLinks方法,这个方法的返回值就是最终的结果。

利用可完成fiiture,可以指定你希望做什么,以及希望以什么顺序执行这些工作。当然,这不会立即发生,不过重要的是所有代码都放在一处。

从概念上讲,CompletableFuture是一个简单API,不过有很多不同方法来组合可完成fiiture。下面先来看处理单个fiiture的方法(如表所示)。(对于这里所示的每个方法,还有两个Async形式,不过这里没有给出,其中一种形式使用一个共享ForkJoinPool,另一种形式有一个Executor参数)。在这个表中,使用了简写记法来表示复杂的函数式接口,这里会把Function写为T->U。当然这并不是真正的Java类型。

你已经见过thenApply方法。以下调用:

CompletableFuture<U> future.thenApply(f);

CompletableFuture<U> future.thenApplyAsync(f);

会返回一个future,可用时会对future的结果应用f。第二个调用会在另一个线程中运行f。

thenCompose方法没有取函数T->U,而是取函数T->CompletableFuture。这听上去相当抽象,不过实际上也很A然。考虑从一个给定URL读取一个Web页面的动作。不用提供方法:

public String blockingReadPage(URL url)

更精巧的做法是让方法返回一个future:

public CompletableFuture<String> readPage(URL url)

现在,假设我们还有一个方法可以从用户输入得到URL,这可能从一个对话框得到,而在用户点击OK按钮之前不会得到答案。这也是将来的一个事件:

public CompletableFuture<URL> getURLInput(Stringprompt)

这里我们有两个函数T->CompletableFuture<U>和U->CompletableFuture<V>。显然,如果第二个函数在第一个函数完成时调用,它们就可以组合为一个函数T->CompletableFuture<V>。这正是thenCompose所做的。

表中的第3个方法强调了目前为止一直忽略的另一个方面:失败(failure)。CompletableFuture中拋出一个异常时,会捕获这个异常并在调用get方法时包装在一个受查异常ExecutionException中。不过,可能get永远也不会被调用。要处理异常,可以使用handle方法。调用指定的函数时要提供结果(如果没有则为null)和异常(如果没有则为null),这种情况下就有意义了。

其余的方法结果都为void,通常用在处理管线的最后。下面来看组合多个future的方法。

前3个方法并行运行一个CompletableFuture<T>和一个CompletableFuture<U>动作,并组合结果。

接下来3个方法并行运行两个CompletableFuture<T>动作。一旦其中一个动作完成,就传递它的结果,并忽略另一个结果。

最后的静态allOf和anyOf方法取一组可完成fiiture(数目可变),并生成一个CompletableFuture<Void>,它会在所有这些fiiture都完成时或者其中任意一个future完成时结束。不会传递任何结果。


 

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值