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. Java关于日期的计算持续汇总~

      /** * 00 * 描述:传入Date date.转为 String yyyyMMdd. * [时间 2019-04-18 15:41:12 作者 陶攀峰] */ public static Str ...

    2. iBatis第五章:事务管理

      ---------------------------- 1.什么是事务 ------------------------------ 什么是事务? 需要注意的是,事务的概念不是针对某个特定的数据库的 ...

    3. char在C语言一个字节表示的数据范围

      #include <stdio.h> //char类型数据范围 [-128,127] // ......-132 -131 -130 -129 -128 .. 127 128 129 13 ...

    4. RuntimeException和Exception区别

      1.java将所有的错误封装为一个对象,其根本父类为Throwable, Throwable有两个子类:Error和Exception. 2.Error是Throwable 的子类,用于指示合理的应用 ...

    5. Linux/Ubuntu 16.04 安装编辑器 Sublime Text 3

      在ubuntu 16.04 系统上使用Sublime Text 3 编辑文本还是不错的, 先到官网下载安装包,链接:http://www.sublimetext.com/3 ,下载对应的版本,64位或 ...

    6. Shell 全局变量、环境变量和局部变量

      Shell 变量的作用域(Scope),就是 Shell 变量的有效范围(可以使用的范围). 在不同的作用域中,同名的变量不会相互干涉,就好像 A 班有个叫小明的同学,B 班也有个叫小明的同学,虽然他 ...

    7. 毕业设计(1)基于MicroPython的大棚监测控制系统的程序设计与模型设计

      智慧农业就是将物联网技术运用到传统农业中去,运用传感器和软件通过移动平台或者电脑平台对农业生产进行控制,使传统农业更具有“智慧”.除了精准感知.控制与决策管理外,从广泛意义上讲,智慧农业还包括农业电子 ...

    8. Python--day14(迭代器)

      今日主要内容 1.  带参装饰器 (了了解) 2.  迭代器(*****) 可迭代对象 迭代器对象 for迭代器 枚举对象 1.  带参装饰器 1.  通常,装饰器为被装饰的函数添加新功能,需要外界的 ...

    9. ginput函数用法

      1.ginput函数:获取指定点坐标值 2.用法说明 (1)[x,y] = ginput(n) 函数从当前的坐标轴上选择n个点,并返回这n个点相应的坐标值(x,y).这n个点可由鼠标定位.用户可以按下 ...

    10. UnityEditorWindow自建窗口扩展

      这里主要记录UnityEditorWindow的创建,以及常用的API接口样式 1,创建UnityEditorWindow 在Unity目录中,创建一个名为Editor的文件夹(任何位置),然后创建如 ...