不同形态的线程池

102 阅读3分钟

开始之前,我们先对之前在任务堆积线程池中提到的,任务重复消费的问题,做一个简单的概括。

我们提到了使用偏移量来解决这个问题,以RocketMQ为例,在集群(CLURTERING)消费模式下,偏移量是存在于broker端的,并且这个偏移量是需要落地的,因为服务停止之后,这个偏移量可能就消失了,实际的情况就是存在一个consumerOffset.json文件中的,那么我们的情况需不需要存呢?实际上可以存也可以不存:

  • 不存的话,我们需要将处理完成的任务设置为已处理状态,系统启动时,自动找到第一条尚未处理的任务,从这个偏移量开始寻找数据
  • 存的话,就需要一个定时任务或者JVM的钩子函数,定时任务定时将偏移量落地,而钩子函数则是在JVM进程正常退出的时候将进程落地;二者之间相辅相成,能够应对大部分情况

而发号器则需要线程安全的,可以考虑直接使用AtomicLong。

下面我们再接触几种有意识的扩展

Tomcat中的线程池

在之前的文章中,我们提到了tomcat中的线程池,这里单独拿出来说是因为它里面有一些很有意思的特性。我们先看下创建线程池的代码:

public void createExecutor() {
    internalExecutor = true;
    TaskQueue taskqueue = new TaskQueue();
    TaskThreadFactory tf = new TaskThreadFactory(getName() + "-exec-", daemon, getThreadPriority());
    executor = new ThreadPoolExecutor(getMinSpareThreads(), getMaxThreads(), 60, TimeUnit.SECONDS,taskqueue, tf);
    taskqueue.setParent( (ThreadPoolExecutor) executor);
}

前面几行代码还好,可最后一行代码是怎么回事呢?居然将executor设置到了队列中。 而我们说的有意思的地方就在这里了。

这里的ThreadPoolExecutor其实并不是jdk里面的,而是tomcat自定义的,我们看下其中的一个构造函数:


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

可以看到我们上篇文章的提前产生核心线程,tomcat还是很注重性能的。

我们再看一下execute的代码:

public void execute(Runnable command) {
    execute(command,0,TimeUnit.MILLISECONDS);
}
public void execute(Runnable command, long timeout, TimeUnit unit) {
    submittedCount.incrementAndGet();
    try {
        super.execute(command);
    } catch (RejectedExecutionException rx) {
        if (super.getQueue() instanceof TaskQueue) {
            final TaskQueue queue = (TaskQueue)super.getQueue();
            try {
                if (!queue.force(command, timeout, unit)) {
                    submittedCount.decrementAndGet();
                    throw new RejectedExecutionException(sm.getString("threadPoolExecutor.queueFull"));
                }
            } catch (InterruptedException x) {
                submittedCount.decrementAndGet();
                throw new RejectedExecutionException(x);
            }
        } else {
            submittedCount.decrementAndGet();
            throw rx;
        }

    }
}

可以看到,在线程池队列满了之后,还会尝试继续将任务塞到队列中。

我们回到这里面的队列,它似乎有一个ThreadPoolExecutor类型成员变量,那么这个有什么用呢?

我们顺着这个变量的使用轨迹,扒一下这个队列的源代码,这个队列实现了LinkedBlockingQueue,对其中的一些方法做了一些增强,顺着线程池变量的使用规则,我们先看下offer这个方法,而这个方法则正是这个队列的精髓所在了:

@Override
public boolean offer(Runnable o) {
  //we can't do any checks
    if (parent==null) return super.offer(o);
    //we are maxed out on threads, simply queue the object
    if (parent.getPoolSize() == parent.getMaximumPoolSize()) return super.offer(o);
    //we have idle threads, just add it to the queue
    if (parent.getSubmittedCount()<=(parent.getPoolSize())) return super.offer(o);
    //if we have less threads than maximum force creation of a new thread
    if (parent.getPoolSize()<parent.getMaximumPoolSize()) return false;
    //if we reached here, we need to add it to the queue
    return super.offer(o);
}

这个方法是在干嘛呢?乍一看,好像基本上都是在调用super的方法,但是又有一些不同的地方,比如,return false的那一行,它的语义是,当前队列中的线程数小于maximum,就返回false,在线程池的场景中,调用offer的地方只有一个,就是在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)
        addWorker(null, false);
}
else if (!addWorker(command, false))
    reject(command);

而如果这个offer返回false,那就进入到了addWorker(command, false)的流程了,这个是在干嘛呢?对了,就是在创建核心之外的线程,那么我们现在串起来一下。 当线程池中的线程数大于核心线程时:

  • 正常的流程中,会将这个任务入队列,并且这一步是会成功的(队列的offer方法会返回true)
  • 当前队列中的线程数小于maximum的时候,并且队列的offer方法返回false时,则会进行核心之外线程的场景流程

这样子的话,不会等到队列满了,再去创建核心之外线程,这样子话任务排队的概率更加低了,整个线程池的并发性会更高了,能够及时处理更加多的请求了。

这种线程池在dubbo中也有用到的,是dubbo的服务发布方用来处理客户端请求时的一种线程模型。

任务有执行顺序的线程池

任务需要有执行顺序,那么就需要在队列中按照一定的条件进行排序,队列排序大家又想到什么数据结构吗?没错,正是优先队列,,它也是堆的经典实现,对于第K个大/小的问题,使用优先队列解题也是很不错的选择。

而对应到线程池的场景就是PriorityBlockingQueue,它会根据一定的规则来对队列中的元素进行“排队”。我们队列中的元素是Runnable,它是没办法排序的啊,那就需要进行Runnable的扩展了,Runnable是线程池执行的基本元素,核心的方法是run,但是这并不妨碍我们给它加上一些其他的特性呀。 我们需要给定一个用来排序的变量,比如priority,而runnable的设计可以如下:

public abstract class PrioritizedRunnable implements Runnable, Comparable<PrioritizedRunnable>

感兴趣的小伙伴可以看下ES的源代码,里面有相关的具体实现,即PrioritizedEsThreadPoolExecutor