构造一个线程池为什么需要几个参数?如果避免线程池出现OOM?RunnableCallable的区别是什么?本文将对这些问题一一解答,同时还将给出使用线程池的常见场景和代码片段。

基础知识

Executors创建线程池

Java中创建线程池很简单,只需要调用Executors中相应的便捷方法即可,比如Executors.newFixedThreadPool(int nThreads),但是便捷不仅隐藏了复杂性,也为我们埋下了潜在的隐患(OOM,线程耗尽)。

Executors创建线程池便捷方法列表:

方法名 功能
newFixedThreadPool(int nThreads) 创建固定大小的线程池
newSingleThreadExecutor() 创建只有一个线程的线程池
newCachedThreadPool() 创建一个不限线程数上限的线程池,任何提交的任务都将立即执行

小程序使用这些快捷方法没什么问题,对于服务端需要长期运行的程序,创建线程池应该直接使用ThreadPoolExecutor的构造方法。没错,上述Executors方法创建的线程池就是ThreadPoolExecutor

ThreadPoolExecutor构造方法

Executors中创建线程池的快捷方法,实际上是调用了ThreadPoolExecutor的构造方法(定时任务使用的是ScheduledThreadPoolExecutor),该类构造方法参数列表如下:

// Java线程池的完整构造函数
public ThreadPoolExecutor(  
    int corePoolSize, // 线程池长期维持的线程数,即使线程处于Idle状态,也不会回收。  
    int maximumPoolSize, // 线程数的上限  
    long keepAliveTime, TimeUnit unit, // 超过corePoolSize的线程的idle时长,
                                       // 超过这个时间,多余的线程会被回收。
    BlockingQueue<Runnable> workQueue, // 任务的排队队列
    ThreadFactory threadFactory, // 新线程的产生方式
    RejectedExecutionHandler handler) // 拒绝策略
}

竟然有7个参数,很无奈,构造一个线程池确实需要这么多参数。这些参数中,比较容易引起问题的有corePoolSizemaximumPoolSizeworkQueue以及handler

  • corePoolSizemaximumPoolSize设置不当会影响效率,甚至耗尽线程;

  • workQueue设置不当容易导致OOM;

  • handler设置不当会导致提交任务时抛出异常。

正确的参数设置方式会在下文给出。

线程池的工作顺序

If fewer than corePoolSize threads are running, the Executor always prefers adding a new thread rather than queuing.
If corePoolSize or more threads are running, the Executor always prefers queuing a request rather than adding a new thread.
If a request cannot be queued, a new thread is created unless this would exceed maximumPoolSize, in which case, the task will be rejected.

corePoolSize -> 任务队列 -> maximumPoolSize -> 拒绝策略

Runnable和Callable

可以向线程池提交的任务有两种:RunnableCallable,二者的区别如下:

  1. 方法签名不同,void Runnable.run()V Callable.call() throws Exception

  2. 是否允许有返回值,Callable允许有返回值

  3. 是否允许抛出异常,Callable允许抛出异常。

Callable是JDK1.5时加入的接口,作为Runnable的一种补充,允许有返回值,允许抛出异常。

三种提交任务的方式:

提交方式 是否关心返回结果
Future<T> submit(Callable<T> task)
void execute(Runnable command)
Future<?> submit(Runnable task) 否,虽然返回Future,但是其get()方法总是返回null

如何正确使用线程池

避免使用无界队列

不要使用Executors.newXXXThreadPool()快捷方法创建线程池,因为这种方式会使用无界的任务队列,为避免OOM,我们应该使用ThreadPoolExecutor的构造方法手动指定队列的最大长度:

ExecutorService executorService = new ThreadPoolExecutor(2, 2, 
                0, TimeUnit.SECONDS, 
                new ArrayBlockingQueue<>(512), // 使用有界队列,避免OOM
                new ThreadPoolExecutor.DiscardPolicy());

明确拒绝任务时的行为

任务队列总有占满的时候,这是再submit()提交新的任务会怎么样呢?RejectedExecutionHandler接口为我们提供了控制方式,接口定义如下:

public interface RejectedExecutionHandler {    
   void rejectedExecution(Runnable r, ThreadPoolExecutor executor);
}

线程池给我们提供了几种常见的拒绝策略:

拒绝策略 拒绝行为
AbortPolicy 抛出RejectedExecutionException
DiscardPolicy 什么也不做,直接忽略
DiscardOldestPolicy 丢弃执行队列中最老的任务,尝试为当前提交的任务腾出位置
CallerRunsPolicy 直接由提交任务者执行这个任务

线程池默认的拒绝行为是AbortPolicy,也就是抛出RejectedExecutionHandler异常,该异常是非受检异常,很容易忘记捕获。如果不关心任务被拒绝的事件,可以将拒绝策略设置成DiscardPolicy,这样多余的任务会悄悄的被忽略。

ExecutorService executorService = new ThreadPoolExecutor(2, 2,
               0, TimeUnit.SECONDS,
               new ArrayBlockingQueue<>(512),
               new ThreadPoolExecutor.DiscardPolicy());// 指定拒绝策略

获取处理结果和异常

线程池的处理结果、以及处理过程中的异常都被包装到Future中,并在调用Future.get()方法时获取,执行过程中的异常会被包装成ExecutionExceptionsubmit()方法本身不会传递结果和任务执行过程中的异常。获取执行结果的代码可以这样写:

ExecutorService executorService = Executors.newFixedThreadPool(4);
Future<Object> future = executorService.submit(new Callable<Object>() {
        @Override
        public Object call() throws Exception {
            throw new RuntimeException("exception in call~");// 该异常会在调用Future.get()时传递给调用者
        }
    }); try {
  Object result = future.get();
} catch (InterruptedException e) {
  // interrupt
} catch (ExecutionException e) {
  // exception in Callable.call()
  e.printStackTrace();
}

上述代码输出类似如下:

线程池的常用场景

正确构造线程池

int poolSize = Runtime.getRuntime().availableProcessors() * 2;
BlockingQueue<Runnable> queue = new ArrayBlockingQueue<>(512);
RejectedExecutionHandler policy = new ThreadPoolExecutor.DiscardPolicy();
executorService = new ThreadPoolExecutor(poolSize, poolSize,    0, TimeUnit.SECONDS,
           queue,
           policy);

获取单个结果

submit()向线程池提交任务后会返回一个Future,调用V Future.get()方法能够阻塞等待执行结果,V get(long timeout, TimeUnit unit)方法可以指定等待的超时时间。

获取多个结果

如果向线程池提交了多个任务,要获取这些任务的执行结果,可以依次调用Future.get()获得。但对于这种场景,我们更应该使用ExecutorCompletionService,该类的take()方法总是阻塞等待某一个任务完成,然后返回该任务的Future对象。向CompletionService批量提交任务后,只需调用相同次数的CompletionService.take()方法,就能获取所有任务的执行结果,获取顺序是任意的,取决于任务的完成顺序:

void solve(Executor executor, Collection<Callable<Result>> solvers)
   throws InterruptedException, ExecutionException {    CompletionService<Result> ecs = new ExecutorCompletionService<Result>(executor);// 构造器    for (Callable<Result> s : solvers)// 提交所有任务
       ecs.submit(s);    int n = solvers.size();
   for (int i = 0; i < n; ++i) {// 获取每一个完成的任务
       Result r = ecs.take().get();
       if (r != null)
           use(r);
   }
}

单个任务的超时时间

V Future.get(long timeout, TimeUnit unit)方法可以指定等待的超时时间,超时未完成会抛出TimeoutException

多个任务的超时时间

等待多个任务完成,并设置最大等待时间,可以通过CountDownLatch完成:

public void testLatch(ExecutorService executorService, List<Runnable> tasks) 
    throws InterruptedException{     CountDownLatch latch = new CountDownLatch(tasks.size());
      for(Runnable r : tasks){
          executorService.submit(new Runnable() {
              @Override
              public void run() {
                  try{
                      r.run();
                  }finally {
                      latch.countDown();// countDown
                  }
              }
          });
      }
      latch.await(10, TimeUnit.SECONDS); // 指定超时时间
  }

线程池和装修公司

以运营一家装修公司做个比喻。公司在办公地点等待客户来提交装修请求;公司有固定数量的正式工以维持运转;旺季业务较多时,新来的客户请求会被排期,比如接单后告诉用户一个月后才能开始装修;当排期太多时,为避免用户等太久,公司会通过某些渠道(比如人才市场、熟人介绍等)雇佣一些临时工(注意,招聘临时工是在排期排满之后);如果临时工也忙不过来,公司将决定不再接收新的客户,直接拒单。

线程池就是程序中的“装修公司”,代劳各种脏活累活。上面的过程对应到线程池上:

// Java线程池的完整构造函数public ThreadPoolExecutor(  
   int corePoolSize, // 正式工数量  
   int maximumPoolSize, // 工人数量上限,包括正式工和临时工  long                          keepAliveTime, TimeUnit unit, // 临时工游手好闲的最长时间,超过这个时间将被解雇
   BlockingQueue<Runnable> workQueue, // 排期队列
   ThreadFactory threadFactory, // 招人渠道
   RejectedExecutionHandler handler) // 拒单方式

总结

Executors为我们提供了构造线程池的便捷方法,对于服务器程序我们应该杜绝使用这些便捷方法,而是直接使用线程池ThreadPoolExecutor的构造方法,避免无界队列可能导致的OOM以及线程个数限制不当导致的线程数耗尽等问题。ExecutorCompletionService提供了等待所有任务执行结束的有效方式,如果要设置等待的超时时间,则可以通过CountDownLatch完成。

Java线程池详解,看这篇就够了!的更多相关文章

  1. Java NIO全面详解(看这篇就够了)

    很多技术框架都使用NIO技术,学习和掌握Java NIO技术对于高性能.高并发网络的应用是非常关键的@mikechen NIO简介 NIO 中的 N 可以理解为 Non-blocking,不单纯是 N ...

  2. Java线程池详解(二)

    一.前言 在总结了线程池的一些原理及实现细节之后,产出了一篇文章:Java线程池详解(一),后面的(一)是在本文出现之后加上的,而本文就成了(二).因为在写完第一篇关于java线程池的文章之后,越发觉 ...

  3. Java线程池详解

    一.线程池初探 所谓线程池,就是将多个线程放在一个池子里面(所谓池化技术),然后需要线程的时候不是创建一个线程,而是从线程池里面获取一个可用的线程,然后执行我们的任务.线程池的关键在于它为我们管理了多 ...

  4. 【java线程系列】java线程系列之java线程池详解

    一线程池的概念及为何需要线程池: 我们知道当我们自己创建一个线程时如果该线程执行完任务后就进入死亡状态,这样如果我们需要在次使用一个线程时得重新创建一个线程,但是线程的创建是要付出一定的代价的,如果在 ...

  5. Java线程池详解(一)

    一.线程池初探 所谓线程池,就是将多个线程放在一个池子里面(所谓池化技术),然后需要线程的时候不是创建一个线程,而是从线程池里面获取一个可用的线程,然后执行我们的任务.线程池的关键在于它为我们管理了多 ...

  6. Java线程池详解及实例

    版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明.本文链接:https://blog.csdn.net/aa1215018028/article/ ...

  7. Java线程池 详解(图解)

    来源:www.jianshu.com/p/098819be088c 拓展: 手动创建 new ThreadPoolExecutor 的使用: https://segmentfault.com/a/11 ...

  8. Java 线程池详解

    Executors创建线程池 Java中创建线程池很简单,只需要调用Executors中相应的便捷方法即可,比如Executors.newFixedThreadPool(int nThreads),但 ...

  9. kafka3.x原理详解看这篇就够了

    一.概述 (一).kafka的定义 1.定义 1)kafka传统的定义:kafka是一个分布式的基于发布/订阅模式的消息队列,主要用于大数据实时处理领域 2)kafka最新的定义:kafka是一个开源 ...

随机推荐

  1. 数据分析与挖掘 - R语言:贝叶斯分类算法(案例一)

    一个简单的例子!环境:CentOS6.5Hadoop集群.Hive.R.RHive,具体安装及调试方法见博客内文档. 名词解释: 先验概率:由以往的数据分析得到的概率, 叫做先验概率. 后验概率:而在 ...

  2. Linux性能优化gprof使用

    gprof用于分析函数调用耗时,可用之抓出最耗时的函数,以便优化程序. gcc链接时也一定要加-pg参数,以使程序运行结束后生成gmon.out文件,供gprof分析. gprof默认不支持多线程程序 ...

  3. Sql之left join(左关联)、right join(右关联)、inner join(自关联)的区别

    参考:https://blog.csdn.net/hj7jay/article/details/51749863

  4. Hibernate框架第一天

    **框架和CRM项目的整体介绍** 1. 什么是CRM * CRM(Customer Relationship Management)客户关系管理,是利用相应的信息技术以及互联网技术来协调企业与顾客间 ...

  5. python base64加密文本内容(1)

    仅仅使用base64加密安全系数太低了,我们还可以自定义base64加密后的文本进行一些变化,提高安全系数,在解密时再回复某些变化 1,先实现一个base64加密 import base64 impo ...

  6. Vue系列之 => 使用第三方animated.css动画

    <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8&quo ...

  7. Sitecore 9有什么新功能

    在这个新版本中有很多值得爱的东西.每个人都会有自己喜欢的新功能,但是,我想与你分享一些地雷: xConnect 正如我们在Sitecore的官方Sitecore 9新闻稿中所见的那样,“新的xConn ...

  8. 【转】Loadrunder场景设计篇——添加windows Resource计数器和指标说明

    转至:https://www.cnblogs.com/langhuagungun/p/8488270.html Loadrunder场景设计篇——添加windows Resource计数器和指标说明 ...

  9. Java 内存分配

    静态储存区:全局变量,static 内存在编译的时候就已经分配好了,并且这块内存在程序运行期间都存在. 栈储存区:1,局部变量.2,,保存类的实例,即堆区对象的引用.也可以用来保存加载方法时的帧.函数 ...

  10. 小米note3的开发者选项在哪里?怎么进入开发者模式?如何显示布局边界?

    小米note3的开发者选项在哪里?小米note3怎么进入开发者模式1.找到[设置],打开2.点击[我的设备]3.点击[全部参数]4.连续点击[MIUI版本]5次5.之后就会看见提示 “进入到开发者模式 ...