文章目录
  1. 1. Executor框架

Executor框架

concurrent包是java提供的并发编程工具,从Java 5开始引入了Executor框架辅助实现并发编程,通过它控制线程的启动、执行和关闭,通常比使用Thread的start方法好。
Executor框架包括:线程池,Executor,Executors,ExecutorService,CompletionService,Future,Callable等。
使用它而不是Thread的好处是,它已经实现了一个线程池,可以以更节省资源的方式使用线程,它设计的接口可以较方便地管理线程。
Executor接口之中只定义了一个方法,即void execute(Runnable command)方法,大量注释用于描述它存在的意义和设计思想。ExecutorService继承自它,提供了更丰富的多线程接口,例如提供了关闭自己的方法shutdown,调用它可以平滑地关闭ExecutorService,所谓平滑就是调用方法后ExecutorService不再接受新的任务,并等待已经提交的任务(正在执行的和尚未开始的)完成,然后关闭ExecutorService。一般使用这个接口来实现和管理多线程。

ExecutorService的生命周期包括三种状态:运行,关闭,终止。创建后就进入运行状态,除非调用shutdown方法否则一直会处于运行状态。在调用shutdown方法之后会进入关闭状态,此时不再接受新任务,当已经提交的任务完成后就进入终止状态。Executors是一个工厂方法和工具方法集合,提供了创建线程池的工厂方法,可以返回实现了ExecutorSerivce接口的线程池,具体见Executors.java代码。下面介绍四个典型类型的线程池:

public static ExecutorService newFixedThreadPool(int nThreads)
// 创建固定线程数目的线程池

public static ExecutorService newCachedThreadPool()
// 创建可缓存的线程池。调用execute方法时会重用以前构造的可用的线程,如果没有则创建新的并加入线程池中,同时终止并移除线程池中60秒没有使用的线程

public static ExecutorService newSingleThreadExecutor()
// 创建一个单线程化的Executor,等同于newFixedThreadPool(1)

public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize)
// 创建一个支持定时和周期性执行任务的线程池,多数情况下可以替代Timer类

先看CachedThreadPool,在缓存型线程池中,使用时先看池中有没有以前建立的线程,如果有就reuse,否则创建一个新的线程并加入池中。它通常用来执行一些生存期很短的异步任务,是Executor的首选。但不适宜面向连接的daemon型Server。
能reuse的线程必须要是 timeout IDLE (空闲超时)内的池中线程,缺省 timeout是60s(创建时keepAliveTime为60s),IDLE超过这个时长,线程实例会被终止并移出线程池。因此不必担心它是否会结束,它会自动结束。

FixedThreadPool和缓存型差不多,在任务数超过固定线程数后能reuse的会reuse,不同在于不能随时创建新的线程,任一时间点最多只能有固定数目的活动线程存在,如果此时要创建新的线程,只能在队列中等待直到当前池中某个线程终止被移出。它没有IDLE机制(创建时keepAliveTime为0),所以适用于有稳定且固定数量并发任务的场景,如服务器。

SingleThreadExecutor是线程池版单例模式,实现和CachedThreadPool、FixedThreadPool一样。

ScheduledThreadPool里的线程可以按schedule依次delay执行,或周期执行。

从上面分析知道,CachedThreadPool的设计会尽量让创建的线程数目与需要的相同,而且它会回收旧线程重用,因此理论上是大多数需要用到线程池的场景时的首选。《Thinking in Java》中说只有当这种方式存在问题时,比如需要大量长时间面向连接的线程时,才需要考虑用FixedThreadPool。

Executors还有一些别的工厂方法,可以看源码。调用工厂方法创建了ExecutorService的实例后,就可以调用实例的execute接口传入Runnable。该方法自动在某个线程上执行Runnable。
ExecutorService还可以执行Callable,通过调用submit(Callable task),它与Runnable不同的地方在于Runnable没有返回值,而Callable有返回值。调用submit方法后会返回一个Future类型的对象表示任务等待完成。Future的get()方法会获取返回值,它会阻塞直到任务完成。
shutdown和shutdownNow都能关闭线程池,不同的是前者会设置状态为SHUTDOWN,中断所有没有在执行任务的线程,等待已提交任务完成,后者会设置状态为STOP,中断所有没有在执行任务的线程并试图中断正在执行任务或暂停任务的线程,返回等待执行的任务列表。在SHUTDOWN状态下线程池可能还没有挂壁,只有当处于TERMINATED状态才说明线程池关闭成功。

img

上面介绍了Executors提供的工厂方法和特点,源码可以看到底层都是同样的数据结构来实现的,那么是什么样的数据结构,现在就来看看。创建线程池最终都是调用的ThreadPoolExecutor类的构造方法ThreadPoolExecutor(corePoolSize, maximumPoolSize, keepAliveTime, milliseconds,runnableTaskQueue, threadFactory, handler),依次是线程池基本大小、线程池最大大小、线程池中线程活动时间限制、时间单位、任务队列、线程工厂、饱和回调。
提交一个任务到线程池时,线程池会创建一个线程来执行任务,即使此时有其他基本线程可用,当提交的任务数大于线程池基本大小时不再创建而是尝试复用已有线程。newCachedThreadPool指定的这个基本大小是0,即始终尝试复用线程,而FixedThreadPool中这个值与maximumPoolSize相等,即优先考虑创建新线程。如果调用了prestartAllCoreThreads方法,线程池会提前创建并启动所有基本线程。任意时间线程池内的活动线程数目不超过maximumPoolSize。
任务队列是用来保存等待执行的任务的阻塞队列(BlockingQueue),可以从以下几种中选择,理论上应该也可以自己实现:

  • ArrayBlockingQueue:基于数组结构的有界阻塞队列,元素顺序FIFO。
  • LinkedBlockingQueue:基于链表的阻塞队列,FIFO,吞吐量通常高于ArrayBlockingQueue,在newFixedThreadPool中就会用这个队列。
  • SynchronousQueue:一个不存储元素的阻塞队列。每个插入操作必须等到前一个移除操作完成,否则插入操作一直处于阻塞状态。吞吐量通常要高于LinkedBlockingQueue,newCachedThreadPool使用了这个队列。
  • PriorityBlockingQueue:具有优先级的无阻塞队列,按优先级排序。

线程工厂用来创建线程,给线程命名等,可以通过实现自己的线程工厂来给线程设置更有意义的名字。
RejectedExecutionHandler饱和策略,当线程池和队列都处于饱和状态时必须采取一种策略处理新提交的任务,这个策略默认情况下是AbortPolicy,它会直接抛出RejectedExecutionException异常。Java提供了以下几种策略,你也可以自己实现RejectedExectionHandler:

  • AbortPolicy:直接抛出异常
  • CallerRunsPolicy:使用调用者所在线程来运行任务
  • DiscardOldestPolicy:抛弃队列里最老的一个任务并执行当前任务
  • DiscardPolicy:直接丢弃

线程活动时间指的是工作线程空闲后的存活时间。如果任务多并且每个任务执行时间短,可以调大这个时间提高线程利用率。时间单位在TimeUnit里有定义,从天到纳秒。

线程池的存在和其实现原理提示了我们它的使用场景,可以从以下几个角度考虑:

  1. 任务的性质:CPU密集型任务,IO密集型任务和混合型任务。
  2. 任务的优先级:高,中和低。
  3. 任务的执行时间:长,中和短。
  4. 任务的依赖性:是否依赖其他系统资源,如数据库连接。

线程池提供了beforeExecute和afterExecute接口用于在线程执行前后进行别的操作,例如Profile。
executorframework
看了线程池对于Android有什么帮助呢?首先当然是我们知道有这么个东西,以后遇到并发任务的时候可以考虑用Executor来做,其次实际上Android有一个东西叫做AsyncTask,它也有execute方法,看看它的源码就会发现它就是用的ThreadPool,不谈它的实现的演变,它里面有一个Executor用来创建线程池和线程,默认使用LinkedBlockingQueue作为任务队列,固定了线程池基本线程、最大线程数量和空闲超时。
在Java里面有个注解GuardedBy,它与锁有关,事实上正是它让我想要看Java并发这一块内容的。

文章目录
  1. 1. Executor框架