常用的线程池模式以及不同线程池的使用场景

答案

1. 基础知识

Java中最基础的线程池服务接口和实现类是 ExecutorServiceThreadPoolExecutor,使用方式如下:

ExecutorService service = new ThreadPoolExecutor(
  coreSize,maxSize,keepAliveTime,TimeUnit,BlockingQueue);

serivce.execute(new Runnable(){ 
  public void run(){
  }
});
  • coreSize是线程池的基本线程数,也可以理解为最小保留的线程数;

    注意 即使设置了coreSize>0, 线程池中最初也没有任何线程(默认),有任务时才会增加新线程,这称为On-demand construction;

  • 如果高峰期任务量比较大,超过了coreSize,那么超过的任务会被放入阻塞队列BlockingQueue缓冲起来,排队等待执行;

  • maxSize是线程池的最大线程数,当任务非常密集,不仅超过了基本线程数,并且连Queue都放满了(Queue有界时),那么线程池会新建一些线程来救急,新建的线程数+基本线程数不会超过maxSize;

  • 这些新建的线程在任务高峰期过去后,一般没必要保留,让它们结束就好,因此会有keepAliveTime来决定它们在任务结束后会保留多长时间,TimeUnit是时间单位。

ExecutorService 还有一个扩展接口: ScheduledExecutorService,它对应的实现类是 ScheduledThreadPoolExecutor,用来执行哪些需要延迟或反复执行的任务,例如将某次任务延迟10秒执行:

ScheduledExecutorService service = new ScheduledThreadPoolExecutor(5);    

service.schedule(new Runnable() {
  public void run() {
  }
}, 10, TimeUnit.SECONDS);

ScheduledThreadPoolExecutor 中的coreSize线程数是由构造方法传入,并且采用了无界队列,多于coreSize的任务都要排队执行,线程数不会增长总是固定的。

2. 线程池常见模式

JDK 为了方便线程池的使用,提供了一系列工厂方法来创建线程池:

1)Executors.newCachedThreadPool()

它的实现其实是:

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

其中SynchronousQueue 实现特点是,没有排队功能,要添加新任务,必须等上一个任务被取走。所以整个线程池表现为线程数会根据任务量不断增长,没有上限,当任务执行完毕,空闲1分钟后释放线程。

评价 适用于耗时较短,但比较密集的任务

2)Executors.newFixedThreadPool(nThreads)

它的实现其实是:

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

整个线程池表现为:线程数固定,任务数多于线程数时,会放入无界队列排队。任务执行完毕,这些线程也不会被释放。

评价 适用于任务量已知,相对耗时的任务

3)Executors.newSingleThreadExecutor()

它的实现其实是:

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

整个线程池表现为:线程数固定为1,任务数多于1时,会放入无界队列排队。任务执行完毕,这唯一的线程也不会被释放。

评价 相当于单线程串行执行任务,区别时如果线程池中的线程因为任务执行失败而终止,还会新建一个线程,保证池的正常工作。

4)Executors.newScheduledThreadPool(corePoolSize)

它的实现其实是:

public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
  return new ScheduledThreadPoolExecutor(corePoolSize);
}

整个线程池表现为:线程数固定,任务数多于线程数时,会放入无界队列排队。任务执行完毕,这些线程也不会被释放。用来执行延迟或反复执行的任务

3. 线程池的使用原则

首先达成几个共识:

  • 能同时执行的线程,与你的CPU个数和核数有关
  • CPU不总是处于繁忙状态,例如,当你执行业务计算时,这时候会使用CPU资源,但当你执行IO操作时、远程RPC调用时,包括进行数据库操作时,这时候CPU就闲下来了,你可以利用多线程提高它的利用率。
  • 当你要执行的CPU密集型的计算时,更多的线程不能提高效率
  • CPU在不同的线程之间切换是有开销的,如果切换的过于频繁,反而会降低效率

一个合理使用线程池的例子: 构建web服务器(例如tomcat)时,web服务的特点是请求任务量大但是处理时间短,如果为每次请求新建一个线程,线程新建的开销甚至超过了任务处理的开销。 利用线程池,就可以让线程实现重用,轮流处理多个请求任务。


results matching ""

    No results matching ""