开始之前,我们先对之前在任务堆积线程池中提到的,任务重复消费的问题,做一个简单的概括。
我们提到了使用偏移量来解决这个问题,以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