线程池问题与自定义扩展

线程池在我们平时开发的过程中非常常见,在开发web后端程序的时候,我们可以在日志中看到诸如http-nio-8080-exec-1的线程名,这些线程都是tomcat启动时创建的用来处理客户端请求的,http-nio指代的是tomcat中的协议,8080就是当前java进程的端口名,而1则是自动递增的序列

TaskThreadFactory tf = new TaskThreadFactory(this.getName() + "-exec-", this.daemon, this.getThreadPriority());    

我们也会在代码中定义自己的线程池,用来异步执行部分代码,一般情况下,都会直接使用Executors类中的静态方法来创建线程池

public static ExecutorService newFixedThreadPool(int nThreads)
public static ExecutorService newSingleThreadExecutor()

但是这种我们一般都不推荐,主要是因为,线程池的参数无法定制化,特别是线程的名字,因为线程的名字在我们排查问题时是很重要的。我们一般考虑使用ThreadPoolExecutor这个类来创建线程池

让我们一起更加深入的了解一下线程池吧。

1.几个问题

1.1. 线程池创建之后,会立即创建核心线程么?

这个的答案是否定的,只有在提交了任务之后,才会开始创建线程池,除非主动调用了prestartCoreThread/prestartAllCoreThreads事先启动核心线程。

在存在大量任务的系统中,我们可以考虑在创建线程池的时候,手动启用所有核心线程,毕竟线程的创建也是需要耗时的(多数时候这个耗时并不会对我们的系统造成任何影响)

1.2.核心线程会被销毁吗?

有一点需要注意,线程池中的核心线程,并非是某些固定的线程,也就是说,线程并未被打上“core”这种标签,而是当线程数为小于设置的corePoolSize时留存下来的线程,这部分的线程将被成为核心线程,换句话说,每个线程都有机会成为核心线程,并非是提前内定好的。

阅读过了源代码的同学,应该知道,核心线程正常情况下是不会被销毁的,在没有任务的时候,核心线程将会被阻塞在从阻塞队列获取任务的操作之上:

Runnable r = timed ?
                    workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
                    workQueue.take();
if (r != null)
    return r;

也就是上述代码中的workQueue.take()这个方法,此时timed为false。这段代码在一个无限循环中被调用,只要能够返回任务r,就会一直执行下去,也就是说线程将一直处于活跃状态

根据我们上边的说法,当线程池中的线程数小于等于corePoolSize,并且timed为true时,那么核心线程就是会进入workQueue.poll方法,一段时间没有获取到任务之后,将会被销毁,我们看看timed怎么来的:

 boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;

很明显,在都是核心线程的时候, wc(即工作线程数) <= corePoolSize,那么就只有allowCoreThreadTimeOut这个设置为true才可以的。

再回到上面的核心线程问题,假设allowCoreThreadTimeOut为false,只要运行到上述代码时,符合wc>corePoolSize,那么当前的这个线程则会被判断为核心之后的线程。

1.3.coreSize 等于0会怎么样呢?

提交任务之后,会先判断当前线程数是否小于corePoolSize,小于则会创建线程,否则会将任务放入到阻塞队列中,如果列队满了,则会判断当前线程池是否大于maximumPoolSize,如果大于则会拒绝任务,反之则会创建新的线程执行这个任务。

根据上述的理论,当corePoolSize == 0的时候,会将这个任务放到阻塞队列中,等到队列满了,创建核心之外的线程来执行

但在JDK1.6版本之后,事实却并非如此,这是execute方法中的代码

int c = ctl.get();
if (workerCountOf(c) < corePoolSize) {
    if (addWorker(command, true))
        return;
    c = ctl.get();
}
if (isRunning(c) && workQueue.offer(command)) {
    int recheck = ctl.get();
    if (!isRunning(recheck) && remove(command))
        reject(command);
    else if (workerCountOf(recheck) == 0)
    // 添加到等待队列成功后(workQueue.offer返回true),判断当前池内线程数是否为0,
    // 如果是则创建一个firstTask为null的worker,
    // 这个worker会从等待队列中获取任务并执行。
        addWorker(null, false);
}
else if (!addWorker(command, false))
    reject(command);

可以看到,如果corePoolSize=0,提交任务时如果线程池为空(即线程池中没有线程),则会立即创建一个线程来执行任务(非核心线程,任务会先进入队列排队再由线程获取);如果提交任务的时候,线程池不为空,则任务先在等待队列中排队,只有队列满了(workerQueue.offer返回false,并发高起来之后,队列才会慢)才会创建新线程

1.4. 如何进行异常处理呢?

线程池中提交任务的方法有两个,execute和submit,execute是异步执行的,submit我们可以拿到代表执行结果的Future对象(一般为FutureTask对象)。

  • 使用execute()提交任务,一般要在Runable任务的代码加上try-catch进行异常处理。

  • 使用submit()提交任务,一般要在主线程中,对Future.get()进行try-catch进行异常处理

上述两种方式都是可以的,下面介绍几种其他的方式:

  • 如果是execute的方式,我们可以考虑自定义线程池,重载ThreadPoolExecutor中的钩子函数afterExecute(Runnable r, Throwable t),这里的参数t就是执行任务过程中产生的异常,我们在此处可以对异常进行统一的处理

  • 实现Thread.UncaughtExceptionHandler接口,实现void uncaughtException(Thread t, Throwable e)方法,并且在创建线程的时候将其设置到线程中,这里的e就是执行过程中抛出的异常,这个uncaughtException方法是由JVM调用的

  • 注意,afterExecute和UncaughtExceptionHandler都不适用submit。在submit中,执行任务的实体是FutureTask,而FutureTask对Throwable进行了try-catch,封装到了outcome属性,所以在ThreadPoolExecutor层是拿不到异常信息

try {
    task.run();
} catch (RuntimeException x) {
    thrown = x; throw x;
} catch (Error x) {
    thrown = x; throw x;
} catch (Throwable x) {
    thrown = x; throw new Error(x);
} finally {
    afterExecute(task, thrown);
}

1.5.线程池关闭的问题

关闭线程池,主要是为了回收一些资源,降低资源的损耗,一般来讲,线程池的生命周期和主体服务是一致的。在开源软件中,都会显式地调用线程池的shutdown方法

2.可扩展的地方

2.1. 任务重放线程池

在利用线程池(由于线程池并无法感知任务执行的成功还是失败)做异步任务的时候,可能会出现任务失败的情况,比如任务抛出了异常或者说从业务的角度说并未成功,这时候需要将这个任务重新投递到线程池中,那么可行的补偿方案就是通过定时任务轮询任务状态,再将任务放入到线程池中(或者直接由定时任务执行)。这种方案我们首先需要对任务进行落库,同时还需要定时任务进行数据库的轮询。

于是便有了下面这个线程池的设计。

我们先看下线程池中任务执行相关的代码:

try {
    beforeExecute(wt, task);
    Throwable thrown = null;
    try {
        task.run();
    } catch (RuntimeException x) {
        thrown = x; throw x;
    } catch (Error x) {
        thrown = x; throw x;
    } catch (Throwable x) {
        thrown = x; throw new Error(x);
    } finally {
        afterExecute(task, thrown);
    }
} finally {
    task = null;
    w.completedTasks++;
    w.unlock();
}

且看代码的第16行,这里将task置为null,旨在对这个任务进行垃圾回收,也就是说线程池只管执行这个任务,对于这个任务的执行结果并不关注。

对于我们的场景,我们需要知道这个任务执行的情况,于是只能对Runnable进行相关的改造了,我们需要让Runnable变得有状态,这个状态即是任务十分执行成功

abstract class AbstractStatefulRunnable implements Runnable {

        private boolean isSuccess = true;

        public boolean isSuccess() {
            return isSuccess;
        }

        @Override
        public void run() {
            isSuccess = doRun();
        }

        /**
         * run
         *
         * @return true means the task execute successfully
         */
        protected abstract boolean doRun();
    }

我们只需要继承AbstractStatefulRunnable类,重写doRun方法即可

而对应的线程池实现则是通过ThreadPoolExecutor的afterExecute这个钩子函数实现的:

public class RecycleExecutor extends ThreadPoolExecutor {

    @Override
    protected void afterExecute(Runnable r, Throwable t) {
        if (r instanceof AbstractStatefulRunnable) {
            AbstractStatefulRunnable runnable = (AbstractStatefulRunnable) r;
// 任务执行失败,或者存在异常,则将任务重新放到队列中去执行
            if (!runnable.isSuccess() || t != null) {
                this.getQueue().offer(runnable);
            }
        }

    }

    public RecycleExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue) {
        super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue);
    }
}

需要注意几个问题

  1. 第9行的offer方法,在队列满的时候可能会无法将任务重新放回队列,此时可能仍旧需要补偿策略

  1. 可考虑的补偿策略是,若无法将任务放入到队列中,则放入到临时队列中,当临时队列满了或者达到大小的二分之一的时候,将临时队列中的任务放入到线程池阻塞队列中

  1. 说到临时队列,这里又有一种类似于两阶段提交的一种思路,也是要借助于临时队列,任务开始执行就是prepare操作,在线程池的beforeExecute方法中,将任务放到临时队列中;当任务执行失败,就执行rollback,将这个任务从临时队列中移除,重新放回线程池队列;当任务执行成功时,就执行commit操作,将这个任务从临时队列中移除。注意,rollback和commit都是在afterExecute中执行的。这个思路来自于RocketMQ中的消息消费的实现。

  1. 出于对系统资源的保护,任务的重试是需要一个阈值的,超过了一定的阈值,就不能再继续重试了。

2.2.任务积压线程池

我们都知道消息队列可以积压消息,原因在于它将这部分消息持久化到硬盘了,那么我们线程池也能利用此特性来做到任务的积压。需要说明的一点是:这里提供的是一种思路,至于这种做法在生产等环境中能不能使用需要经过测试才行。

那么我们现在的问题是任务积压怎么集成到线程池中呢?

我们看下向显现出提交任务的方法,当队列满了,并且无法在创建新的线程(活跃线程数等于max)时,会调用reject方法进行任务的拒绝操作。

涉及到的类是RejectedExecutionHandler这个接口,默认实现有四种:分别是抛出异常、调用者直接调用(即提交任务的线程直接执行任务)、直接丢弃该任务、丢弃任务队列中最老的任务。

显然这些handler都无法实现任务堆积的目的,那么我们可以考虑自己实现一个handler,并且在创建线程池的时候,指定这个handler,而这个handler的实现就是将任务进行落库。

我们可以考虑将其存在数据库中,也可以考虑其他的存储介质——可以是kafka,甚至可以是本地文件(当然这种实现起来的难度大一点,但是却没有了网络调用的开销)。

好了,现在我们已经把数据持久化了,那么如何去获取这部分的数据呢?

我们先想一下,线程是如何去获取任务的。没错,就是下面的这段代码

Runnable r = timed ?
                    workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
                    workQueue.take();

可是这是写死在jdk源码中的内容,我们没有办法更改,那该怎么办呢?不要忘了,这个workQeuue是我们自己传入的,那么它的poll和take方法我们就可以做文章了。我们基于装饰者设计模式创建一个和数据库等交互的queue,不同之处在于传统的queue都是从内存中获取数据,而我们这个则包含了从数据库等获取数据的逻辑。

还有一个就是为什么需要装饰者模式呢?因为我们有一部分数据是在内存中,我们是内存中放不下去才会放在数据库中的,不能遗忘了这部分数据。

class PersistBlockingQueue<T> implements BlockingQueue<T> {

        private BlockingQueue<T> delegate;

        public PersistBlockingQueue(BlockingQueue<T> delegate) {
            this.delegate = delegate;
        }
    ....
}

我们重写take和poll方法,尝试从数据库中获取数据。

注意take和poll实现的不同之处:

  • take需要可考虑先从数据库中获取任务,然后在从队列中获取任务,即super.take()方法,因为我们需要依赖delegate阻塞队列的take方法来阻塞线程池的线程

  • poll对于二者的顺序没有要求,建议先从队列获取,及super.poll方法

注意:

  1. 对于PersistBlockingQueue的实现,我们需要先看看线程池中使用到了它的那些方法,然后再去根据情况重写,而没有用到的,则直接交给delegate的对应方法即可。

  1. 需要区分使用场景,在任务的实时性,并发性要求不高,但要求可靠性的时候,这个思路是可取的,当然数据持久化的场景性能就会差,这只是相比较于内存而言的,对于现在的存储技术来说,能够做大持久化数据的读写与内存数据的读写相差无异。

  1. 可行性有待商榷,主要是想提供一种思路。

线程池其他的扩展远不止这些,大家感兴趣可以评论区交流,另外本人水平有限,如有错误还望指出,谢谢!

今天就先写到这里,明天再更新余下的内容,祝大家新年快乐!

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
线程池自定义线程池工厂指的是我们可以通过自定义工厂类来创建线程池。在Java中,我们可以通过实现ThreadFactory接口来自定义线程池工厂。通过自定义工厂类,我们可以定制线程的创建方式,例如给线程设置特定的命名规则、设置线程的优先级等。 自定义线程池工厂的步骤如下: 1. 创建一个实现ThreadFactory接口的自定义工厂类,并实现其`newThread(Runnable r)`方法。 2. 在`newThread`方法中,我们可以通过`Thread`类的构造方法来创建线程,并进行一些定制化的操作,比如设置线程的名称、优先级等。 3. 自定义线程池工厂类的实例化后,我们可以将其作为参数传递给线程池创建方法中,以便使用自定义线程池工厂来创建线程池。 举个例子,假设我们需要自定义线程池工厂来创建线程池,可以按照以下步骤进行: 1. 创建一个自定义线程池工厂类,例如`CustomThreadFactory`,并实现ThreadFactory接口。 2. 在`CustomThreadFactory`类中,实现`newThread(Runnable r)`方法,并在该方法中创建线程,并设置线程的名称。 3. 在使用线程池的地方,例如`Executors.newFixedThreadPool()`方法中,将`CustomThreadFactory`类的实例传递给`newFixedThreadPool()`方法,以使用自定义线程池工厂来创建线程池。 通过自定义线程池工厂,我们可以更加灵活地控制线程的创建过程,并根据实际需求进行定制化操作。这样可以提高线程池的灵活性和可扩展性,使其更好地适用于各种场景的需求。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值