Elasticsearch中各种线程池分析

最近看完了ElasticSearch线程池模块的源码,感触颇深,然后也自不量力地借鉴ES的 EsThreadPoolExecutor 重新造了一把轮子(源码在这里),对线程池的理解又加深了一些。在继承 ThreadPoolExecutor实现自定义的线程池时,ES先重写了Runnable接口,提供了更灵活的任务运行过程中出现异常处理逻辑。简而言之,它采用回调机制实现了线程在运行过程中抛出未受检异常的统一处理逻辑,非常优美。实在忍不住把源码copy下来:

/**
* An extension to runnable.
*/
public abstract class AbstractRunnable implements Runnable { /**
* Should the runnable force its execution in case it gets rejected?
*/
public boolean isForceExecution() {
return false;
} @Override
public final void run() {
try {
doRun();
} catch (Exception t) {
onFailure(t);
} finally {
onAfter();
}
} /**
* This method is called in a finally block after successful execution
* or on a rejection.
*/
public void onAfter() {
// nothing by default
} /**
* This method is invoked for all exception thrown by {@link #doRun()}
*/
public abstract void onFailure(Exception e); /**
* This should be executed if the thread-pool executing this action rejected the execution.
* The default implementation forwards to {@link #onFailure(Exception)}
*/
public void onRejection(Exception e) {
onFailure(e);
} /**
* This method has the same semantics as {@link Runnable#run()}
* @throws InterruptedException if the run method throws an InterruptedException
*/
protected abstract void doRun() throws Exception;
}
  1. 统一的任务执行入口方法doRun(),由各个子类实现doRun()执行具体的业务逻辑

  2. try-catch中统一处理线程执行任务过程中抛出的异常,由onFailure()处理

  3. 任务执行完成(不管是正常结束还是运行过程中抛出了异常),统一由onAfter()处理

  4. isForceExecution方法,用来支持任务在提交给线程池被拒绝了,强制执行。当然了,这需要线程池的任务队列提供相关的支持。我也是受这种方式的启发,实现了一个线程在执行任务过程中抛出未受检异常时,先判断该任务是否允许强制执行isForceExecution,然后再重新提交任务运行的线程池

此外,ES内置了好几个默认实现的线程池,比如 EsThreadPoolExecutor 、QueueResizingEsThreadPoolExecutor 和 PrioritizedEsThreadPoolExecutor。

  1. QueueResizingEsThreadPoolExecutor

    在创建线程池时会指定一个任务队列(BlockingQueue),平常都是直接用 LinkedBlockingQueue,它是一个无界队列,当然也可以在构造方法中指定队列的长度。但是,ES中几乎不用 LinkedBlockingQueue 作为任务队列,而是使用 LinkedTransferQueue ,但是 LinkedTransferQueue 又是一个无界队列,于是ES又基于LinkedTransferQueue 封装了一个任务队列,类名称为 ResizableBlockingQueue,它能够限制任务队列的长度

    那么问题来了,对于一个线程池,任务队列设置为多长合适呢?

    答案就是Little's Law。在QueueResizingEsThreadPoolExecutor 线程池中重写了afterExecute()方法,里面统计了每个任务的运行时间、等待时间(入队列到执行)。所以,你想知道如何统计一个任务的运行时间吗?你想统计线程池一共提交了多少个任务,所有任务的运行时间吗?看看QueueResizingEsThreadPoolExecutor 源码就明白了。

    另外再提一个问题,为什么ES用 LinkedTransferQueue 作为任务队列而不用 LinkedBlockingQueue 呢?

    我想:很重要的一个原因是LinkedBlockingQueue 是基于重量级的锁(ReentrantLock)实现的入队操作,而LinkedTransferQueue 是基于CAS原子指令实现的入队操作。LinkedBlockingQueue#offer()当队列长度达到最大值,此时不能提交任务给队列了,直接返回false,否则通过加锁方式将任务提交给队列。LinkedTransferQueue本身是无界的,因此添加任务到LinkedTransferQueue时,通过CAS实现避免了加锁带来的上下文开销的切换,在大部分竞争情况下,是会提升性能的。

  2. PrioritizedEsThreadPoolExecutor

    优先级任务的线程池,任务提交给线程池后是在任务队列里面排队,FIFO模式。而这个线程池则允许任务定义一个优先级,优先级高的任务先执行。

  3. EsThreadPoolExecutor

    这个线程池非常像JDK里面的ThreadPoolExecutor,不过,它实现了一些拒绝处理逻辑,提交任务若被拒绝(会抛出EsRejectedExecutionException异常),则进行相关处理

        @Override
    public void execute(final Runnable command) {
    doExecute(wrapRunnable(command));
    } protected void doExecute(final Runnable command) {
    try {
    super.execute(command);
    } catch (EsRejectedExecutionException ex) {
    if (command instanceof AbstractRunnable) {
    // If we are an abstract runnable we can handle the rejection
    // directly and don't need to rethrow it.
    try {
    ((AbstractRunnable) command).onRejection(ex);
    } finally {
    ((AbstractRunnable) command).onAfter(); }
    } else {
    throw ex;
    }
    }
    }

讲完了ES中常用的三个线程池实现,还想结合JDK源码,记录一下线程在执行任务过程中抛出运行时异常,是如何处理的。我觉得有二种方式(或者说有2个地方)来处理运行时异常。一种方式是:java.util.concurrent.ThreadPoolExecutor#afterExecute方法,另一种方式是:java.lang.Thread.UncaughtExceptionHandler#uncaughtException

  1. afterExecute

    看ThreadPoolExecutor#afterExecute(Runnable r, Throwable t) 的源码注释:

    Method invoked upon completion of execution of the given Runnable.This method is invoked by the thread that executed the task. If non-null, the Throwable is the uncaught RuntimeException or Error that caused execution to terminate abruptly.

    提交给线程池的任务,执行完(不管是正常结束,还是执行过程中出现了异常)后都会自动调用afterExecute()方法。如果执行过程中出现了异常,那么Throwable t 就不为null,并且导致执行终止(terminate abruptly.)。

    This implementation does nothing, but may be customized in subclasses. Note: To properly nest multiple overridings, subclasses should generally invoke super.afterExecute at the beginning of this method.

    默认的afterExecute(Runnable r, Throwable t) 方法是一个空实现,什么也没有。因此,在继承ThreadPoolExecutor实现自己的线程池时,如果重写该方法,则要记住:先调用 super.afterExecute

    比如说这样干:

     @Override
    protected void afterExecute(Runnable r, Throwable t) {
    super.afterExecute(r, t);
    if (t != null) {
    //出现了异常
    if (r instanceof AbstractRunnable && ((AbstractRunnable)r).isForceExecution()) {
    //AbstractRunnable 设置为强制执行时重新拉起任务
    execute(r);
    logger.error("AbstractRunnable task run time error:{}, restarted", t.getMessage());
    }
    }
    }

    看,重写afterExecute方法,当 Throwable 不为null时,表明线程执行任务过程中出现了异常,这时就重新提交任务。

    有个时候,在实现 Kafka 消费者线程的时候(while true循环),经常因为解析消息出错导致线程抛出异常,就会导致 Kafka消费者线程挂掉,这样就永久丢失了一个消费者了。而通过这种方式,当消费者线程挂了时,可重新拉起一个新任务。

  2. uncaughtException

    创建 ThreadPoolExecutor时,要传入ThreadFactory 作为参数,在而创建ThreadFactory 对象时,就可以设置线程的异常处理器java.lang.Thread.UncaughtExceptionHandler。

    在用Google Guava包的时候,一般这么干:

    //先 new  Thread.UncaughtExceptionHandler对象 exceptionHandler
    private ThreadFactory threadFactory = new ThreadFactoryBuilder().setNameFormat("thread_name-%d").setUncaughtExceptionHandler(exceptionHandler).build();

    在线程执行任务过程中,如果抛出了异常,就会由JVM调用 Thread.UncaughtExceptionHandler 中实现的异常处理逻辑。看Thread.UncaughtExceptionHandler的JDK源码注释:

    Interface for handlers invoked when a Thread abruptly. terminates due to an uncaught exception.

    When a thread is about to terminate due to an uncaught exception the Java Virtual Machine will query the thread for its UncaughtExceptionHandler using getUncaughtExceptionHandler and will invoke the handler's uncaughtException method, passing the thread and the exception as arguments.

    其大意就是:如果线程在执行Runnable任务过程因为 uncaught exception 而终止了,那么 JVM 就会调用getUncaughtExceptionHandler 方法查找是否设置了异常处理器,如果设置了,那就就会调用异常处理器的java.lang.Thread.UncaughtExceptionHandler#uncaughtException方法,这样我们就可以在这个方法里面定义异常处理逻辑了。

总结

ES的ThreadPool 模块是学习线程池的非常好的一个示例,实践出真知。它告诉你如何自定义线程池(用什么任务队列?cpu核数、任务队列长度等参数如何配置?)。在实现自定义任务队列过程中,也进一步理解了CAS操作的原理,如何巧妙地使用CAS?是失败重试呢?还是直接返回?。我想,这也是CAS与synchronized锁、ReentrantLock锁的一个最重要应用区别:多个线程在竞执行 synchronized锁 或者 ReentrantLock锁 锁住的代码(术语叫临界区)时,未抢到锁的进程会被挂起,会伴随上下文切换,而若可以把临界区中的代码逻辑基于CAS原子指令来实现,如果某个线程执行CAS操作失败了,它可以选择继续重试,还是执行其它的处理逻辑,还是sleep若干毫秒。因此,它把线程执行的主动权交回给了程序员。比如基于CAS实现自增操作,失败时继续重试(这里自增操作逻辑本身要求"失败重试直到加1成功"),直到加1成功,代码是这样的:

do{
v = value.get();
}while(v!=value.compareAndSwap(v,v+1));

有个时候,代码里面CAS失败,并不一定就需要立即重试,因为,CAS失败了,意味着此时有其他线程也在竞争,说明资源的竞争较激烈,那我们是不是可以先 sleep 一下再重试呢?这样是不是更好?

  • 线程在执行Runnable任务过程中抛出了异常如何处理?这里提到了Thread.UncaughtExceptionHandler#uncaughtException 和 ThreadPoolExecutor#afterExecute。前者是由JVM自动调用的,后者则是在每个任务执行结束后都会被调用。

  • Thread.UncaughtExceptionHandler#uncaughtException 和 RejectedExecutionHandler#rejectedExecution 是不同的。RejectedExecutionHandler 用来处理任务在提交的时候,被线程池拒绝了,该怎么办的问题,默认是AbortPolicy,即:直接丢弃。

  • 等下次有时间,好好地写一篇分析ElasticSearch6.3.2的线程池模块。

    自定义 ThreadPoolExecutor 处理线程运行时异常的更多相关文章

    1. Java中运行时异常和非运行时异常什么鬼?

      Java中的异常分类 RuntimeException(也称unchecked exceptions,运行时异常) 就是我们在开发中测试功能时程序终止,控制台出现的异常.(一般来说,出现运行时异常基本 ...

    2. android 运行时异常捕获

      1,将运行时异常捕获并存到手机SD卡上 可以直接使用logcat 命令Runtime.getRuntime().exec("logcat -f "+ file.getAbsolut ...

    3. Java软件工程师面试题:Java运行时异常与一般异常有什么不一样?

      异常表示程序运行过程中可能出现的非正常状态,运行时异常表示虚拟机的通常操作中可能遇到的异常,是一种常见运行错误.java编译器要求方法必须声明抛出可能发生的非运行时异常,但是并不要求必须声明抛出未被捕 ...

    4. Effective Java 第三版——70. 对可恢复条件使用检查异常,对编程错误使用运行时异常

      Tips 书中的源代码地址:https://github.com/jbloch/effective-java-3e-source-code 注意,书中的有些代码里方法是基于Java 9 API中的,所 ...

    5. java运行时异常与一般异常有何异同?

      转自: http://blog.csdn.net/rainminism/article/details/51208572 Throwable是所有Java程序中错误处理的父类,有两种资类:Error和 ...

    6. Java checked 异常 和 RuntimeException(运行时异常)

      目录 一.运行时异常 1.什么是RuntimeExceptioin 2.运行时异常的特点 3.如何运用运行时异常 二.运行时异常和ckecked异常的区别 1.机制上 2.逻辑上 一.运行时异常 1. ...

    7. 【Java面试题】20 运行时异常和一般异常有何区别

      Throwable 是所有 Java 程序中错误处理的父类 ,有两种资类: Error 和 Exception . Error :表示由 JVM 所侦测到的无法预期的错误,由于这是属于 JVM 层次的 ...

    8. Java运行时异常和非运行时异常

      1.Java异常机制 Java把异常当做对象来处理,并定义一个基类java.lang.Throwable作为所有异常的超类.Java中的异常分为两大类:错误Error和异常Exception,Java ...

    9. Java运行时异常与一般异常以及错误的异同

      Java提供了两类主要的异常:runtime exception和checked exception.checked 异常也就是我们经常遇到的IO异常,以及SQL异常都是这种异常.对于这种异常,JAV ...

    随机推荐

    1. hbase snapshot 表备份/恢复

      snapshot其实就是一组metadata信息的集合,它可以让管理员将表恢复到以前的一个状态.snapshot并不是一份拷贝,它只是一个文件名的列表,并不拷贝数据.一个全的snapshot恢复以为着 ...

    2. 2019年3月8日_CCF-A类文章分享

      3月8日下午3点开始,王利朋老师主持实验室会议,实验室全体人员参加.首先王利朋老师就基全本子存在的问题进行了再一次的说明以及如何修改部分内容, 其次,王利朋老师对近段时间做了相关工作总结和下阶段任务分 ...

    3. C# web项目利用docx文档作为模板~为打印专做的解决方案

      还是那句话:十年河东,十年河西,莫欺少年穷. 目前,web端打印技术有很多,有收费的专业web打印控件,大家可以参考我的上一篇博客.当然,很多公司不愿意出钱,那么今天咱们就探讨下怎么做免费的打印. w ...

    4. 利用unittest+ddt进行接口测试(一):简单demo

      一般进行接口测试时,每个接口的传参都不止一种情况,一般会考虑正向.逆向等多种组合.所以在测试一个接口时通常会编写多条case,而这些case除了传参不同外,其实并没什么区别. 这个时候就可以利用ddt ...

    5. Python-包-65

      包 包是一种通过使用‘.模块名’来组织python模块名称空间的方式. 1. 无论是import形式还是from...import形式,凡是在导入语句中(而不是在使用时)遇到带点的,都要第一时间提高警 ...

    6. DevOps时代,企业数字化转型需要强大的工具链

      伴随时代的飞速进步,中国的人口红利带来了互联网业务的快速发展,巨大的流量也带动了技术的不断革新,研发的模式也在不断变化.传统企业纷纷效仿互联网的做法,结合DevOps进行数字化的转型. 通常提到Dev ...

    7. keras04 - 阿狗阿猫识别 面向对象编程

      参考:https://www.bilibili.com/video/av32756783

    8. 使用py,根据日志记录自动生成周报

      日志格式如下,思路是如果检测到文件中的内容为5位或者8位,即12.11或18.12.11,同时存在.即认为当前行为日期数据仅作为方便查看日志使用,生成脚本时过滤此行.每次读取到空白行的时候则认为下一条 ...

    9. Linux(Ubuntu)使用日记------markdown文档转化为word文档

      Linux(Ubuntu)使用日记------markdown文档转化为word文档

    10. Vue.js 2.x笔记:路由Vue Router(6)

      1. Vue Router简介与安装 1.1 Vue Router简介 Vue Router 是 Vue.js 官方的路由管理器.它和 Vue.js 的核心深度集成,构建单页面应用. Vue Rout ...