Java线程池

 

一、线程池概念

线程池:

 

  线程池从字面意思来看,是指管理一组同构工作线程的资源池。

 

  在线程池中执行任务比「为每一个任务分配一个线程」优势更多

  通过重用现有的线程而不是创建新线程,可以在处理多个请求时分摊在线程创建和销毁过程中产生的巨大开销。

 

  另外一个额外的好处是,当请求到达时,工作线程通常已经存在,因此不会由于等待创建线程而延迟任务的执行,从而提高了响应性。

 

 

new Thread的弊端:

1 new Thread(new Runnable() {
2 
3     @Override
4     public void run() {
5         // TODO Auto-generated method stub
6     }
7 }).start();

new Thread的弊端如下:

  a. 每次new Thread新建对象性能差。
  b. 线程缺乏统一管理,可能无限制新建线程,相互之间竞争,及可能占用过多系统资源导致死机或oom。
  c. 缺乏更多功能,如定时执行、定期执行、线程中断。
相比new Thread,线程池的好处在于:
  a. 降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
  b. 提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。
  c. 高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。

 

 

二、线程池参数

 

 

 

可以通过ThreadPoolExecutor来创建一个线程池:

1     public ThreadPoolExecutor(int corePoolSize,
2                               int maximumPoolSize,
3                               long keepAliveTime,
4                               TimeUnit unit,
5                               BlockingQueue<Runnable> workQueue) {
6         this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
7              Executors.defaultThreadFactory(), defaultHandler);
8     }

 

  corePoolSize(线程池的基本大小):基本大小也就是线程池的目标大小,即在没有任务执行时(初期线程并不启动,而是等到有任务提交时才启动,除非调用prestartAllCoreThreads)线程池的大小,并且只有在工作队列满了的情况下才会创建超出这个数量的线程。

  一般情况下不管有没有任务都会一直在线程池中一直存活,只有在 ThreadPoolExecutor 中的方法 allowCoreThreadTimeOut(boolean value) 设置为 true 时,闲置的核心线程会存在超时机制,如果在指定时间没有新任务来时,核心线程也会被终止,而这个时间间隔由第3个属性 keepAliveTime 指定。

 

  maximumPoolSize(线程池最大大小):线程池的最大大小表示可同时活动的线程数量的上限。如果某个线程的空闲时间超过了存活时间,那么将被标记为可回收的,并且当线程池的当前大小超过了基本大小时,这个线程将被终止。

  keepAliveTime(线程活动保持时间):线程池的工作线程空闲后,保持存活的时间。所以如果任务很多,并且每个任务执行的时间比较短,可以调大这个时间,提高线程的利用率。一般情况下用于非核心线程。也就是当线程池中线程数>corepoolsize Keeplivetime 才起作用。

 

  TimeUnit(线程活动保持时间的单位):可选的单位有天(DAYS),小时(HOURS),分钟(MINUTES),毫秒(MILLISECONDS),微秒(MICROSECONDS, 千分之一毫秒)和毫微秒(NANOSECONDS, 千分之一微秒)。

  runnableTaskQueue(任务队列):用于保存等待执行的任务的阻塞队列。

    ArrayBlockingQueue:是一个基于数组结构的有界阻塞队列,此队列按 FIFO(先进先出)原则对元素进行排序。

    LinkedBlockingQueue:一个基于链表结构的阻塞队列,此队列按FIFO (先进先出) 排序元素,吞吐量通常要高于ArrayBlockingQueue。静态工厂方法Executors.newFixedThreadPool()使用了这个队列。

    SynchronousQueue:一个不存储元素的阻塞队列。每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于LinkedBlockingQueue,静态工厂方法Executors.newCachedThreadPool使用了这个队列。

    PriorityBlockingQueue:一个具有优先级的无限阻塞队列。

  ThreadFactory:用于设置创建线程的工厂,可以通过线程工厂给每个创建出来的线程设置更有意义的名字。

  RejectedExecutionHandler(饱和策略):当队列和线程池都满了,说明线程池处于饱和状态,那么必须采取一种策略处理提交的新任务。这个策略默认情况下是AbortPolicy,表示无法处理新任务时抛出异常。

 

 

 

 

 

 

三、静态工厂方法提供的4种线程池

 

Java通过Executors提供四种线程池,分别为:
  newFixedThreadPool 创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。

  newSingleThreadExecutor 创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。

  newCachedThreadPool创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。

  newScheduledThreadPool 创建一个定长线程池,支持定时及周期性任务执行。

  他们的底层都是ThreadPoolExecutor。


newFixedThreadPool :

1 public static ExecutorService newFixedThreadPool(int nThreads) {
2         return new ThreadPoolExecutor(nThreads, nThreads,
3                                       0L, TimeUnit.MILLISECONDS,
4                                       new LinkedBlockingQueue<Runnable>());
5     }

  corePoolSize和maximumPoolSize的大小是一样的,因为如果使用无界queue的话maximumPoolSize参数是没有意义的

  BlockingQueue选择了LinkedBlockingQueue,该queue有一个特点,他是无界的。

  创建固定大小的线程池,每次提交一个任务就创建一个线程,直到线程达到线程池的最大大小,线程池的大小一旦达到最大值就会保持不变,如果某个线程因为执行异常而结束,那么线程池会补充一个新线程。

 

 1 ExecutorService fixedThreadPool = Executors.newFixedThreadPool(3);
 2 for (int i = 0; i < 10; i++) {
 3     final int index = i;
 4     fixedThreadPool.execute(new Runnable() {
 5 
 6         @Override
 7         public void run() {
 8             try {
 9                 System.out.println(index);
10                 Thread.sleep(2000);
11             } catch (InterruptedException e) {
12                 // TODO Auto-generated catch block
13                 e.printStackTrace();
14             }
15         }
16     });
17 }

  因为线程池大小为3,每个任务输出index后sleep 2秒,所以每两秒打印3个数字。

  定长线程池的大小最好根据系统资源进行设置。如Runtime.getRuntime().availableProcessors()。

 

newSingleThreadExecutor:

1 public static ExecutorService newSingleThreadExecutor() {
2         return new FinalizableDelegatedExecutorService
3             (new ThreadPoolExecutor(1, 1,
4                                     0L, TimeUnit.MILLISECONDS,
5                                     new LinkedBlockingQueue<Runnable>()));
6     }

  相当于newFixedThreadPool(1)

  创建是一个单线程池,也就是该线程池只有一个线程在工作,所有的任务是串行执行的,如果这个唯一的线程因为异常结束,那么会有一个新的线程来替代它,此线程池保证所有任务的执行顺序按照任务的提交顺序执行。

 

 1 ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();
 2 for (int i = 0; i < 10; i++) {
 3     final int index = i;
 4     singleThreadExecutor.execute(new Runnable() {
 5 
 6         @Override
 7         public void run() {
 8             try {
 9                 System.out.println(index);
10                 Thread.sleep(2000);
11             } catch (InterruptedException e) {
12                 // TODO Auto-generated catch block
13                 e.printStackTrace();
14             }
15         }
16     });
17 }

  结果依次输出,相当于顺序执行各个任务。

  现行大多数GUI程序都是单线程的。

 

newCachedThreadPool:

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

  首先是无界的线程池,所以我们可以发现maximumPoolSize为max。其次BlockingQueue的选择上使用SynchronousQueue:每个插入操作必须等待另一个线程的对应移除操作。

  比如,我先添加一个元素,接下来如果继续想尝试添加则会阻塞,直到另一个线程取走一个元素,反之亦然。(就是缓冲区为1的生产者消费者模式)

   根据用户的任务数创建相应的线程数来处理,该线程池不会对线程数加以限制,完全依赖于 JVM 能创建线程的数量,可能引起内存不足。

  这是一个可根据需要创建新线程的线程池,但是在以前构造的线程可用时将重用它们。对于执行很多短期异步任务的程序而言,这些线程池通常可提高程序性能。调用 execute() 将重用以前构造的线程(如果线程可用)。如果现有线程没有可用的,则创建一个新线程并添加到池中。终止并从缓存中移除那些已有 60 秒钟未被使用的线程。因此,长时间保持空闲的线程池不会使用任何资源。

 


 

 

 1 ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
 2 for (int i = 0; i < 10; i++) {
 3     final int index = i;
 4     try {
 5         Thread.sleep(index * 1000);
 6     } catch (InterruptedException e) {
 7         e.printStackTrace();
 8     }
 9 
10     cachedThreadPool.execute(new Runnable() {
11 
12         @Override
13         public void run() {
14             System.out.println(index);
15         }
16     });
17 }

  线程池为无限大,当执行第二个任务时第一个任务已经完成,会复用执行第一个任务的线程,而不用每次新建线程。

 

newScheduledThreadPool:

1     public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
2         return new ScheduledThreadPoolExecutor(corePoolSize);
3     }
4 
5     public ScheduledThreadPoolExecutor(int corePoolSize) {
6         super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
7               new DelayedWorkQueue());
8     }

 

1 ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(5);
2 scheduledThreadPool.schedule(new Runnable() {
3 
4     @Override
5     public void run() {
6         System.out.println("delay 3 seconds");
7     }
8 }, 3, TimeUnit.SECONDS);

  表示延迟3秒执行。

scheduledThreadPool.scheduleAtFixedRate(new Runnable() {

    @Override
    public void run() {
        System.out.println("delay 1 seconds, and excute every 3 seconds");
    }
}, 1, 3, TimeUnit.SECONDS);

  表示延迟1秒后每3秒执行一次。

  ScheduledExecutorService比Timer更安全,功能更强大。

 

四、任务队列

  ThreadPoolExecutor允许提供一个BlockingQueue来保存等待执行的任务。 
  基本的任务排队方法有3种:无界队列(unbounded queue,),有界队列(bounded queue,)和同步移交(synchronous handoff)。 

  newFixedThreadPoolnewSingleThreadExecutor在默认情况下将使用一个无界的LinkedBlockingQueue

  如果所有工作者线程都处于忙碌状态,那么任务将在队列中等候。如果任务持续地达到,并且超过了线程池处理它们的速度,那么队列将无限制地增加。  

 

  一种更稳妥的资源管理策略是使用有界队列,例如ArrayBlockingQueue,有界的LinkedBlockingQueue,PriorityBlockingQueue。 

  有界队列有助于避免资源耗尽的情况发生,但队列填满后,由饱和策略解决。

 

  在newCachedThreadPool工厂方法中使用了SynchronousQueue。 

  对于非常大的或者无界的线程池,可以通过使用SynchronousQueue来避免任务排队,以及直接将任务从生产者移交给工作者线程。 

  SynchronousQueue不是一个真正的队列,而是一种在线程之间移交的机制。 

  要将一个元素放入SynchronousQueue中,必须有另一个线程正在等待接受这个元素。如果没有线程正在等待,并且线程池的当前大小小于最大值,那么TrheadPoolExecutor将创建一个新的线程,否则根据饱和策略,这个任务将被拒绝。  使用直接移交将更高效,因为任务会直接移交给执行它的线程,而不是首先放在队列中,然后由工作者线程从队列中提取该任务。 

  只有当线程池是无界的或者可以拒绝任务时,SynchronousQueue才有实际价值。

 

  对于Executor,newCachedThreadPool工厂方法是一种很好的默认选择,他能提供比固定大小的线程更好的排队性能(由于使用了SynchronousQueue而不是LinkedBlockingQueue)。 
  当需要限制当前任务的数量以满足资源管理需求时,可以选择固定大小的线程池,就像在接受网络用户请求的服务器应用程序中,如果不进行限制,容易发生过载问题。

 

五、饱和策略

  当有界队列被填满后,饱和策略开始发挥作用。 
  ThreadPoolExecutor的饱和策略可以通过调用setRejectedExecutionHandler来修改。(如果某个任务被提交到一个已被关闭的Executor时,也会用到饱和策略) 

 

  AbortPolicy:“中止(Abort)策略”是默认的饱和策略,该策略将抛出未检查的Rejected-ExecutionException。调用这可以捕获这个异常,然后根据需求编写自己的处理代码。

  DiscardPolicy:“抛弃(Discard)策略”会抛弃超出队列的任务。

  DiscardOldestPolicy:“抛弃最旧策略“则会抛弃下个将被执行任务,然后尝试重新提交新的任务。(如果工作队列是一个优先队列,那么抛弃最旧策略将导致抛弃优先级最高的任务,因此最好不要将抛弃最旧饱和策略和优先级队列一起使用)  

  CallerRunsPolicy:“调用者运行策略“实现了一种调节机制。该策略不会抛弃任务,也不会抛出异常,而是将某些任务回退到调用者,从而减低新任务的流量。  它不会在线程池的某个线程中执行新提交的任务,新任务会在调用execute时在主线程中执行。

 

六、关闭线程池

 

ExecutorService提供了两种方法关闭方法:

 

  shutdown: 平缓的关闭过程,即不再接受新的任务,而是要等所有任务缓存队列中的任务都执行完后才终止;

 

  shutdownNow: 立刻关闭所有任务,并尝试打断正在执行的任务,并且清空任务缓存队列,返回尚未执行的任务;

 

 

 

七、Callable

 

  Executor以Runnable的形式描述任务,但是Runnable有很大的局限性:

 

    没有返回值,只是执行任务;

 

    不能处理被抛出的异常;

 

  为了弥补以上的问题,Java中设计了另一种接口Callable。

 

 

 

  Callable支持任务有返回值,并支持异常的抛出。如果希望获得子线程的执行结果,那Callable将比Runnable更为合适。

 

  无论是Callable还是Runnable都是对于任务的抽象描述,即表明任务的范围:有明确的起点,并且都会在一定条件下终止。

 

 

八、Future

 

 

  Future类表示任务生命周期状态,并提供了相应的方法来判断是否已经完成或取消,以及获取任务的结果和取消任务等,其命名体现了任务的生命周期只能向前不能后退。

 

  Future类提供方法查询任务状态外,还提供get方法获得任务的返回值,但是get方法的行为取决于任务状态:

 

    如果任务已经完成,get方法则会立刻返回;

 

    如果任务还在执行中,get方法则会拥塞直到任务完成;

 

    如果任务在执行的过程中抛出异常,get方法会将该异常封装为ExecutionException中,并可以通过getCase方法获得具体异常原因;

 

  如果将一个Callable对象提交给ExecutorService,submit方法就会返回一个Future对象,通过这个Future对象就可以在主线程中获得该任务的状态,并获得返回值。

 

  除此之外,可以显式地把Runnable和Callable对象封装成FutureTask对象,FutureTask不光继承了Future接口,也继承Runnable接口,所以可以直接调用run方法执行。

 

 

 

Future 接口:

 1 public interface Future<V> {
 2 
 3     boolean cancel(boolean mayInterruptIfRunning);
 4 
 5     boolean isCancelled();
 6 
 7     boolean isDone();
 8 
 9     V get() throws InterruptedException, ExecutionException;
10 
11     V get(long timeout, TimeUnit unit)
12         throws InterruptedException, ExecutionException, TimeoutException;
13 }

 

Future 定义了5个方法:

1)boolean cancel(boolean mayInterruptIfRunning):试图取消对此任务的执行。如果任务已完成、或已取消,或者由于某些其他原因而无法取消,则此尝试将失败。当调用 cancel() 时,如果调用成功,而此任务尚未启动,则此任务将永不运行。如果任务已经启动,则 mayInterruptIfRunning 参数确定是否应该以试图停止任务的方式来中断执行此任务的线程。此方法返回后,对 isDone() 的后续调用将始终返回 true。如果此方法返回 true,则对 isCancelled() 的后续调用将始终返回 true。


2)boolean isCancelled():如果在任务正常完成前将其取消,则返回 true。


3)boolean isDone():如果任务已完成,则返回 true。 可能由于正常终止、异常或取消而完成,在所有这些情况中,此方法都将返回 true。


4)V get()throws InterruptedException,ExecutionException:如有必要,等待计算完成,然后获取其结果。


5)V get(long timeout,TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException: 如有必要,最多等待为使计算完成所给定的时间之后,获取其结果(如果结果可用)。

 

1 public class FutureTask<V> implements RunnableFuture<V> {
2     ...
3 }
4 
5 public interface RunnableFuture<V> extends Runnable, Future<V> {
6     void run();
7 }

  FutureTask 实现了 Runnable 和 Future,所以兼顾两者优点,既可以在 Thread 中使用,又可以在 ExecutorService 中使用。

 

 1 Callable<String> callable = new Callable<String>() {
 2     @Override
 3     public String call() throws Exception {
 4         return "abc";
 5     }
 6 };
 7 
 8 FutureTask<String> task = new FutureTask<String>(callable);
 9 
10 Thread t = new Thread(task);
11 t.start(); // 启动线程
12 task.cancel(true); // 取消线程

 

 

  使用 FutureTask 的好处是 FutureTask 是为了弥补 Thread 的不足而设计的,它可以让程序员准确地知道线程什么时候执行完成并获得到线程执行完成后返回的结果。FutureTask 是一种可以取消的异步的计算任务,它的计算是通过 Callable 实现的,它等价于可以携带结果的 Runnable,并且有三个状态:等待、运行和完成。完成包括所有计算以任意的方式结束,包括正常结束、取消和异常。
 
ExecutorService:
 1 public interface Executor {
 2     void execute(Runnable command);
 3 }
 4 
 5 public interface ExecutorService extends Executor {
 6     void shutdown();
 7     List<Runnable> shutdownNow();
 8     
 9     boolean isShutdown();
10     boolean isTerminated();
11     
12     <T> Future<T> submit(Callable<T> task);
13     <T> Future<T> submit(Runnable task, T result);
14     Future<?> submit(Runnable task);
15     ...
16 }

 

 

  

 

posted @ 2017-12-31 21:09  __Meng  阅读(363)  评论(0编辑  收藏  举报