Java多线程:Executor框架

Executor框架

1、Executor框架简介

1)、Executor框架的两级调度模型

在HotSpot VM的线程模型中,Java线程被一对一映射为本地操作系统线程。Java线程启动时会创建一个本地操作系统线程;当该Java线程终止时,这个操作系统线程也会被回收。操作系统会调度所有线程并将它们分配给可用的CPU

在上层,Java多线程程序通常把应用分解为若干个任务,然后使用用户级的调度器将这些任务映射为固定数量的线程;在底层,操作系统内核将这些线程映射到硬件处理器上

在这里插入图片描述

应用程序通过Executor框架控制上层的调度,而下层的调度由操作系统内核控制,下层的调度不受应用程序的控制

2)、Executor框架的使用

在这里插入图片描述

主线程首先要创建实现Runnable或者Callable接口的任务对象。工具类Executors可以把一个Runnable对象封装为一个Callable对象(Executors.callable(Runnable task, T result)或Executors.callable(Runnable task))

然后可以把Runnable对象直接交给ExecutorService执行(ExecutorService.execute(Runnable command));或者也可以把Runnable对象或Callable对象提前交给ExecutorService执行(ExecutorService.submit(Runnable task)ExecutorService.submit(Callable<T> task)

如果执行ExecutorService.submit(…),ExecutorService将返回一个实现Future接口的对象(JDK1.8中是FutureTask对象)。由于FutureTask实现了Runnable,程序员也可以创建FutureTask,然后直接交给ExecutorService执行

最后,主线程可以执行FutureTask.get()方法来等待任务执行完成。主线程也可以执行FutureTask.cancel(boolean mayInterruptIfRunning)来取消任务的执行

2、ThreadPoolExecutor详解

ThreadPoolExecutor主要由下列4个组件构成

  • corePool:核心线程池的大小
  • maximumPool:最大线程池的大小
  • BlockingQueue:用来暂时保存任务的工作队列
  • RejectedExecutionHandler:当ThreadPoolExecutor已经关闭或ThreadPoolExecutor已经饱和时(达到了最大线程池大小且工作队列已满),execute()方法将要调用的Handler

1)、FixedThreadPool:可重用固定线程池数的线程池

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

FixedThreadPool的corePoolSize和maximumPoolSize都被设置为创建FixedThreadPool时所指定的参数nThreads

当线程池中的线程数大于corePoolSize时,keepAliveTime为多余的空闲线程等待新任务的最长时间,超过这个时间后多余的线程将被终止。这里把keepAliveTime设置为0L,意味着多余的空闲线程会被立即终止

FixedThreadPool的execute()方法运行示意图:

在这里插入图片描述

1)如果当前运行的线程数少于corePoolSize,则创建新线程来执行任务

2)在线程池完成预热之后(当前运行的线程数等于corePoolSize),将任务加入LinkedBlockingQueue

3)线程执行完1中的任务后,会在循环中反复从LinkedBlockingQueue获取任务来执行

FixedThreadPool使用无界队列LinkedBlockingQueue作为线程池的工作队列。使用无界队列作为工作队列会在线程池带来如下影响:

1)当线程池中的线程数达到corePoolSize后,新任务将在无界队列中等待,因此线程池中的线程数不会超过corePoolSize

2)由于1,使用无界队列时maximumPool参数无效

3)由于1和2,使用无界队列时keepAliveTime参数无效

4)由于使用无界队列,运行中的FixedThreadPool不会拒绝任务

**适用场景:**FixedThreadPool适用于为了满足资源管理的需求,而需要限制当前线程数量的应用场景,适用于负载比较重的服务器

2)、SingleThreadExecutor:使用单个工作线程的Executor

    public static ExecutorService newSingleThreadExecutor(ThreadFactory threadFactory) {
        return new FinalizableDelegatedExecutorService
            (new ThreadPoolExecutor(1, 1,
                                    0L, TimeUnit.MILLISECONDS,
                                    new LinkedBlockingQueue<Runnable>(),
                                    threadFactory));
    }

SingleThreadExecutor的corePoolSize和maximumPool被设置为1,使用无界队列LinkedBlockingQueue

SingleThreadExecutor的execute()方法运行示意图:

在这里插入图片描述

1)如果当前运行的线程数少于corePoolSize(即线程池中无运行的线程),则创建一个新线程来执行任务

2)在线程完成预热之后(当前线程池中有一个运行的线程),将任务加入LinkedBlockingQueue

3)线程执行完1中的任务后,会在一个无限循环中反复从LinkedBlockingQueue获取任务来执行

**适用场景:**SingleThreadExecutor适用于需要保证顺序地还行各个任务,并且在任意时间点,不会有多个线程是活动的应用场景

3)、CachedThreadPool:根据需要创建新线程的线程池

    public static ExecutorService newCachedThreadPool() {
        return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                      60L, TimeUnit.SECONDS,
                                      new SynchronousQueue<Runnable>());
    }

CachedThreadPool的corePoolSize被设置为0,maximumPool无界,keepAliveTime设置为60秒,意味着CachedThreadPool中的空闲线程等待新任务的最长时间为60秒,空闲线程超过60秒后将会终止

CachedThreadPool使用了没有容量的SynchronousQueue作为线程池的工作队列,但CachedThreadPool的maximumPool是无界的。这意味着,如果主线程提交任务的速度高于maximumPool中线程处理任务的速度时,CachedThreadPool会不断创建新线程。极端情况下,CachedThreadPool会因为创建多线程而耗尽CPU和内存资源

CachedThreadPool的execute()方法运行示意图:

在这里插入图片描述

1)首先执行SynchronousQueue.offer(Runnable task)。如果当前maximumPool中有空闲线程正在执行SynchronousQueue.poll(long timeout, TimeUnit unit),那么主线程执行offer操作与空闲线程执行的poll操作配对成功,主线程把任务交给空闲线程执行,execute()方法执行完成;否则执行下面的步骤2

2)当初始maximumPool为空,或者maximumPool中当前没有空闲线程时,将没有线程执行SynchronousQueue.poll(long timeout, TimeUnit unit)。这种情况下,步骤1)将失败。此时CachedThreadPool会创建一个新线程执行任务,execute()方法执行完成

3)在步骤2)中新创建的线程将任务执行完后,会执行SynchronousQueue.poll(long timeout, TimeUnit unit)。这个poll操作会让空闲线程最多在SynchronousQueue中等待60秒钟。如果60秒钟内主线程提交了一个新任务(主线程执行步骤1),那么这个空闲线程将执行主线程提交的新任务;否则,这个空闲线程将终止。由于空闲60秒的空闲线程会被禁止,因此长时间保持空闲的CachedThreadPool不会使用任何资源

SynchronousQueue是一个没有容量的阻塞队列。每个插入操作必须等待另一个线程的对应移除操作,反之亦然。CachedThreadPool使用SynchronousQueue,把主线程提交的任务传递给空闲线程执行

CachedThreadPool中任务传递的示意图:

在这里插入图片描述

3、FutureTask详解

Future接口和实现Future接口的FutureTask类,代表异步计算的结果

1)、FutureTask简介

FutureTask除了实现Future接口外,还实现了Runnable接口。因此,FutureTask可以交给Executor执行,也可以由调用线程直接执行(FutureTask.run())。根据FutureTask.run()方法被执行的时机,FutureTask可以处于下面3种状态:

  • 未启动。FutureTask.run()方法还没有被执行之前,FutureTask处于未启动状态。当创建一个FutureTask,且没有执行FutureTask.run()方法之前,这个FutureTask处于未启动状态
  • 已启动。FutureTask.run()方法被执行的过程中,FutureTask处于已启动状态
  • 已完成。FutureTask.run()方法执行完后正常结束,或被取消(FutureTask.cancel(…)),或执行FutureTask.run()方法时抛出异常而异常结束,FutureTask处于已完成状态

FutureTask的状态迁移的示意图:

在这里插入图片描述

FutureTask的get和cancel的执行示意图:

在这里插入图片描述

2)、FutureTask的使用

当一个线程需要等待另一个线程把某个任务执行完后它才能继续执行,此时可以使用FutureTask。假设有多个线程执行若干任务,每个任务最多只能被执行一次。当多个线程试图同时执行同一个任务时,只允许一个线程执行任务,其他线程需要等待这个任务执行完后才能继续执行

	private final ConcurrentMap<Object, Future<String>> taskCache = new ConcurrentHashMap<>();

	private String executionTask(final String taskName) throws InterruptedException, ExecutionException {
		while (true) {
			Future<String> future = taskCache.get(taskName);// 1.1,2.1
			if (future == null) {
				Callable<String> task = new Callable<String>() {
					@Override
					public String call() throws Exception {
						return taskName;
					}
				};
				// 1.2创建任务
				FutureTask<String> futureTask = new FutureTask<>(task);
				future = taskCache.putIfAbsent(taskName, futureTask);// 1.3
				if (future == null) {
					future = futureTask;
					futureTask.run();// 1.4执行任务
				}
			}
			try {
				return future.get();// 1.5,2.2线程自此等待任务执行完成
			} catch (CancellationException e) {
				taskCache.remove(taskName, future);
			}
		}
	}

在这里插入图片描述

当两个线程同时执行同一个任务时,如果Thread1执行1.3后Thread2执行2.1,那么接下来Thread2将在2.2等待,直到Thread1执行完1.4后Thread2才能从2.2返回

猜你喜欢

转载自blog.csdn.net/qq_40378034/article/details/87344164