Dubbo 源码分析之线程池

前言

相信学过 netty 的都知道,每一个 ChannelHandler 都是通过它的 EventLoop(I/O线程) 来处理传递给它的事件,所以至关重要的是不要阻塞这个线程,因为这会对整体的 I/O 产生负面的影响。Dubbo 亦是如此,所以 Dubbo 定义了一些线程池,来供我们异步处理事件。

Dubbo 线程池分类

Dubbo-2.6.5 版本中共有三个线程池

  • CachedThreadPool
  • FixedThreadPool
  • LimitedThreadPool

而在 GitHub 上 Dubbo 的最新源码中又提供了一个新的线程池

  • EagerThreadPool

是不是很熟悉,除了 EagerThreadPool,别的线程池好像 java.util.concurrent.Executors 都为我们提供了。只不过 Dubbo 自己又实现了一下,调整下自定义参数(对线程池不熟悉的请查看为这篇文章:JAVA线程池ThreadPoolExecutor详解

Dubbo 线程工厂

在看线程池之前先看一下 Dubbo 自定义的线程池工厂

public class NamedThreadFactory implements ThreadFactory {

	// 自增 线程池序号
    protected static final AtomicInteger POOL_SEQ = new AtomicInteger(1);
	// 线程池中 线程自增序号
    protected final AtomicInteger mThreadNum = new AtomicInteger(1);
	// 线程池名称前缀
    protected final String mPrefix;
	// 是否设置为守护线程
    protected final boolean mDaemon;
	// 所在线程组
    protected final ThreadGroup mGroup;
	// 默认非守护线程 即:用户线程
    public NamedThreadFactory() {
        this("pool-" + POOL_SEQ.getAndIncrement(), false);
    }
	
    public NamedThreadFactory(String prefix) {
        this(prefix, false);
    }

    public NamedThreadFactory(String prefix, boolean daemon) {
        mPrefix = prefix + "-thread-";
        mDaemon = daemon;
        SecurityManager s = System.getSecurityManager();
        mGroup = (s == null) ? Thread.currentThread().getThreadGroup() : s.getThreadGroup();
    }

    @Override
    public Thread newThread(Runnable runnable) {
        String name = mPrefix + mThreadNum.getAndIncrement();
        Thread ret = new Thread(mGroup, runnable, name, 0);
        ret.setDaemon(mDaemon);
        return ret;
    }

    public ThreadGroup getThreadGroup() {
        return mGroup;
    }
}

该线程工厂为我们的线程池创建了名称,为线程创建了名称,方便我们查看堆栈信息时进行 debug ,线程为守护/用户的决定权由使用者决定。
上面的线程工厂是在 Dubbo-2.6.5 版本中的源码
其实在 GitHub 上 Dubbo 的最新源码中为该线程工厂又封装了一层

public class NamedInternalThreadFactory extends NamedThreadFactory {

    public NamedInternalThreadFactory() {
        super();
    }

    public NamedInternalThreadFactory(String prefix) {
        super(prefix, false);
    }

    public NamedInternalThreadFactory(String prefix, boolean daemon) {
        super(prefix, daemon);
    }

    @Override
    public Thread newThread(Runnable runnable) {
        String name = mPrefix + mThreadNum.getAndIncrement();
        InternalThread ret = new InternalThread(mGroup, runnable, name, 0);
        ret.setDaemon(mDaemon);
        return ret;
    }
}

看着好像并没有做什么,其实细节都在 newThread 方法里,会发现我们在父类 NamedThreadFactory 中是直接使用的 new Thread ,而在这里使用的是 new InternalThread,有什么区别呢?仔细看看 InternalThread 也就是重新封装了一下 Thread ,好像什么都没做。其实作用这这里是看不出来的,看过 Netty 源码的可能知道 Netty 有个 FastThreadLocal FastThreadLocalThreadDubbo 在这里也是如此,借鉴了 Netty 源码。要说 FastThreadLocalThreadLocal 什么区别,请移步我的这篇文章Java源码之ThreadLocal,在这里我就不多废话了。

线程池拒绝策略

Dubbo 也重写了线程池拒绝策略,主要操作就是在线程池拒绝时保存堆栈信息到文件。具体可以看我分析的这篇文章 Dubbo 源码分析之自定义线程池拒绝策略,这里不再详细介绍。

下面开始看线程池

CachedThreadPool

public class CachedThreadPool implements ThreadPool {

    public Executor getExecutor(URL url) {
    	//线程池名称前缀  默认:Dubbo 
        String name = url.getParameter(Constants.THREAD_NAME_KEY, Constants.DEFAULT_THREAD_NAME);
        //核心线程数 默认:0
        int cores = url.getParameter(Constants.CORE_THREADS_KEY, Constants.DEFAULT_CORE_THREADS);
        // 最大线程数 默认:Integer.MAX_VALUE
        int threads = url.getParameter(Constants.THREADS_KEY, Integer.MAX_VALUE);
        //阻塞队列大小 默认:0
        int queues = url.getParameter(Constants.QUEUES_KEY, Constants.DEFAULT_QUEUES);
        //线程存活时间 默认:60s
        int alive = url.getParameter(Constants.ALIVE_KEY, Constants.DEFAULT_ALIVE);
        return new ThreadPoolExecutor(cores, threads, alive, TimeUnit.MILLISECONDS, 
        		queues == 0 ? new SynchronousQueue<Runnable>() : 
        			(queues < 0 ? new LinkedBlockingQueue<Runnable>() 
        					: new LinkedBlockingQueue<Runnable>(queues)),
        		new NamedThreadFactory(name, true), new AbortPolicyWithReport(name, url));
    }
}

所谓 CachedThreadPool,就是线程池会伸缩,当事件较多时创建新的线程,事件较少时,超出一定时间回收线程。

注意:其实此 CachedThreadPool 并不是正经的 cache,可能大家发现了该线程池的阻塞队列虽然默认是 SynchronousQueue,但是如果用户配置了 queues 变量,且其值较大,使用的阻塞队列就是 LinkedBlockingQueue,此时一旦 corethreads 再使用默认值0,就会导致处理事件阻塞。

FixedThreadPool

public class FixedThreadPool implements ThreadPool {

    public Executor getExecutor(URL url) {
        String name = url.getParameter(Constants.THREAD_NAME_KEY, Constants.DEFAULT_THREAD_NAME);
        int threads = url.getParameter(Constants.THREADS_KEY, Constants.DEFAULT_THREADS);
        int queues = url.getParameter(Constants.QUEUES_KEY, Constants.DEFAULT_QUEUES);
        return new ThreadPoolExecutor(threads, threads, 0, TimeUnit.MILLISECONDS, 
        		queues == 0 ? new SynchronousQueue<Runnable>() : 
        			(queues < 0 ? new LinkedBlockingQueue<Runnable>() 
        					: new LinkedBlockingQueue<Runnable>(queues)),
        		new NamedThreadFactory(name, true), new AbortPolicyWithReport(name, url));
    }

}

FixedThreadPool 表示核心线程和最大线程大小都为固定值,在 Dubbo 中的确也是这么处理的,但是这里还有要注意的地方。

注意:如果使用 fixed 线程池,默认的线程池数量为200,默认阻塞队列大小为0,默认使用的阻塞队列为SynchronousQueue。如果你的业务事件并发较高或者处理时间较长,请适当调整阻塞队列大小,即 queues 变量,否则会导致大量请求被丢弃。该线程池也是 Dubbo 默认使用的线程池,估计出事的挺多。

LimitedThreadPool

public class LimitedThreadPool implements ThreadPool {

    public Executor getExecutor(URL url) {
        String name = url.getParameter(Constants.THREAD_NAME_KEY, Constants.DEFAULT_THREAD_NAME);
        int cores = url.getParameter(Constants.CORE_THREADS_KEY, Constants.DEFAULT_CORE_THREADS);
        int threads = url.getParameter(Constants.THREADS_KEY, Constants.DEFAULT_THREADS);
        int queues = url.getParameter(Constants.QUEUES_KEY, Constants.DEFAULT_QUEUES);
        return new ThreadPoolExecutor(cores, threads, Long.MAX_VALUE, TimeUnit.MILLISECONDS, 
        		queues == 0 ? new SynchronousQueue<Runnable>() : 
        			(queues < 0 ? new LinkedBlockingQueue<Runnable>() 
        					: new LinkedBlockingQueue<Runnable>(queues)),
        		new NamedThreadFactory(name, true), new AbortPolicyWithReport(name, url));
    }

}

这个线程池就比较有意思的,该线程池中的线程数可以一直增长到上限,永不回收,其实永不回收是个伪概念,查看代码可以发现,该线程设置线程的回收时间限制为 Long.MAX_VALUE,可以理解为永久不回收。这个如果说要注意的那就只有一点,因为线程池是不回收的,所以 threads 变量即线程最大限制的值不能太大,使用默认 200 即可,避免 OOM.

EagerThreadPool

注:此线程池在dubbo-2.6.5及其以下没有实现

首先介绍一下该线程池:线程池中的所有核心线程都在忙碌时,此时如果再添加新的任务不会放入阻塞队列,而且创建新的线程,直到达到最大线程限制,此时如果还有任务,才会放入阻塞队列。

public class EagerThreadPool implements ThreadPool {

    @Override
    public Executor getExecutor(URL url) {
        String name = url.getParameter(Constants.THREAD_NAME_KEY, Constants.DEFAULT_THREAD_NAME);
        int cores = url.getParameter(Constants.CORE_THREADS_KEY, Constants.DEFAULT_CORE_THREADS);
        int threads = url.getParameter(Constants.THREADS_KEY, Integer.MAX_VALUE);
        int queues = url.getParameter(Constants.QUEUES_KEY, Constants.DEFAULT_QUEUES);
        int alive = url.getParameter(Constants.ALIVE_KEY, Constants.DEFAULT_ALIVE);

        // init queue and executor
        TaskQueue<Runnable> taskQueue = new TaskQueue<Runnable>(queues <= 0 ? 1 : queues);
        EagerThreadPoolExecutor executor = new EagerThreadPoolExecutor(cores,
                threads,
                alive,
                TimeUnit.MILLISECONDS,
                taskQueue,
                new NamedInternalThreadFactory(name, true),
                new AbortPolicyWithReport(name, url));
        taskQueue.setExecutor(executor);
        return executor;
    }
}

大致规规矩矩,和上面一样,里面好像有两个我们没有见过的类 TaskQueueEagerThreadPoolExecutor

先看一下自定义阻塞队列 TaskQueue

public class TaskQueue<R extends Runnable> extends LinkedBlockingQueue<Runnable> {

    private static final long serialVersionUID = -2635853580887179627L;

    private EagerThreadPoolExecutor executor;

    public TaskQueue(int capacity) {
        super(capacity);
    }

    public void setExecutor(EagerThreadPoolExecutor exec) {
        executor = exec;
    }
	// 重写 offer 操作
    @Override
    public boolean offer(Runnable runnable) {
        if (executor == null) {
            throw new RejectedExecutionException("The task queue does not have executor!");
        }

        int currentPoolThreadSize = executor.getPoolSize();
        // 小于核心线程数 直接调用父类offer
        if (executor.getSubmittedTaskCount() < currentPoolThreadSize) {
            return super.offer(runnable);
        }

        // 返回失败使线程池新建线程来执行任务 伪装阻塞队列已满
        if (currentPoolThreadSize < executor.getMaximumPoolSize()) {
            return false;
        }

        // currentPoolThreadSize >= max
        return super.offer(runnable);
    }

   	// 重试 offer 操作
    public boolean retryOffer(Runnable o, long timeout, TimeUnit unit) throws InterruptedException {
        if (executor.isShutdown()) {
            throw new RejectedExecutionException("Executor is shutdown!");
        }
        return super.offer(o, timeout, unit);
    }
}

最主要的逻辑就是在重写的 offer 方法里面,通过重写该方法可以在我们向线程池提交任务时,伪装阻塞队列已满,来使线程池新建线程执行任务。

贴上一段 ThreadPoolExecutor 类的 execute 代码

 public void execute(Runnable command) {
         /*如果提交的任务为null  抛出空指针异常*/
        if (command == null)
            throw new NullPointerException();

        int c = ctl.get();
        /*如果当前的任务数小于等于设置的核心线程大小,那么调用addWorker直接执行该任务*/
        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);
        }
        /*如果向队列中添加失败,那么就新开启一个线程来执行该任务*/
        //tip:最主要的就是这里 当向阻塞队列插入失败时,会直接调用 addWorker 方法,创建新的线程执行任务
        else if (!addWorker(command, false))
            reject(command);
    }

想具体了解细节的可以查看我的这篇博客 线程池(ThreadPoolExecutor)源码分析之如何保证核心线程不被销毁的
通过 ThreadPoolExecutor 源码可以发现,当向阻塞队列插入失败时,会直接调用 addWorker 方法,创建新的线程执行任务。现在就和重写的 TaskQueueoffer 方法对上了。

那就继续看 EagerThreadPoolExecutor 类。

// 继承ThreadPoolExecutor
public class EagerThreadPoolExecutor extends ThreadPoolExecutor {

   	// 任务数量
    private final AtomicInteger submittedTaskCount = new AtomicInteger(0);

    public EagerThreadPoolExecutor(int corePoolSize,
                                   int maximumPoolSize,
                                   long keepAliveTime,
                                   TimeUnit unit, TaskQueue<Runnable> workQueue,
                                   ThreadFactory threadFactory,
                                   RejectedExecutionHandler handler) {
        super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory, handler);
    }

    // 返回正在执行的任务数量
    public int getSubmittedTaskCount() {
        return submittedTaskCount.get();
    }

	// ThreadPoolExecutor 模版方法 任务执行完成之后会调用
    @Override
    protected void afterExecute(Runnable r, Throwable t) {
        submittedTaskCount.decrementAndGet();
    }

	// 重写 execute 方法
    @Override
    public void execute(Runnable command) {
        if (command == null) {
            throw new NullPointerException();
        }
        // do not increment in method beforeExecute!
        submittedTaskCount.incrementAndGet();
        try {
            super.execute(command);
        } catch (RejectedExecutionException rx) {
            // 提交任务拒绝时 直接放入阻塞队列
            final TaskQueue queue = (TaskQueue) super.getQueue();
            try {
                if (!queue.retryOffer(command, 0, TimeUnit.MILLISECONDS)) {
                    submittedTaskCount.decrementAndGet();
                    throw new RejectedExecutionException("Queue capacity is full.", rx);
                }
            } catch (InterruptedException x) {
                submittedTaskCount.decrementAndGet();
                throw new RejectedExecutionException(x);
            }
        } catch (Throwable t) {
            // decrease any way
            submittedTaskCount.decrementAndGet();
            throw t;
        }
    }
}

重写的线程池也很简单,提供一个当前任务数量的接口,供 TaskQueue 使用。execute 方法也很好理解。可能有的小伙伴要问了,为什么要在 execute 方法中捕获 RejectedExecutionException 异常呢。这个还要回到 ThreadPoolExecutor 源码中

//省略无关代码
 else if (!addWorker(command, false))
            reject(command);
//省略无关代码

这句代码,在 addWorker 失败时,会抛出 RejectedExecutionException 异常。在未重写的线程池中抛出该异常的条件是:阻塞队列已满,线程数已达到最大限制。重写后的 TaskQueue 阻塞队列中 offer 还会在线程池中线程数量小于最大限制时返回 false.即使返回 false 新的阻塞队列也不代表已经满了,所以需要 retryOffer

//省略无关代码
   if (currentPoolThreadSize < executor.getMaximumPoolSize()) {
            return false;
   }
//省略无关代码

线程池分析结束了,看了下提交记录 EagerThreadPool 好像还是当初我们公司的前师兄写的。目前好像是 DubboPPMC。厉害的很啊,大家一起努力学习啊。coding

展开阅读全文
©️2020 CSDN 皮肤主题: 像素格子 设计师: CSDN官方博客 返回首页
实付0元
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值