前言

在 SOFA-RPC 的官方介绍里,介绍了自定义线程池,可以为指定服务设置一个独立的业务线程池,和 SOFARPC 自身的业务线程池是隔离的。多个服务可以共用一个独立的线程池。

API使用方式如下:

UserThreadPool threadPool = new UserThreadPool();
threadPool.setCorePoolSize(10);
threadPool.setMaximumPoolSize(100);
threadPool.setKeepAliveTime(200);
threadPool.setPrestartAllCoreThreads(false);
threadPool.setAllowCoreThreadTimeOut(false);
threadPool.setQueueSize(200); UserThreadPoolManager.registerUserThread("com.alipay.sofa.rpc.quickstart.HelloService", threadPool);

如上为 HelloService 服务设置了一个自定义线程池。

在 SOFABoot 中如下使用:

<bean id="customExcutor" class="com.alipay.sofa.rpc.server.UserThreadPool" init-method="init">
<property name="corePoolSize" value="10" />
<property name="maximumPoolSize" value="10" />
<property name="queueSize" value="0" />
</bean> <bean id="helloService" class="com.alipay.sofa.rpc.quickstart.HelloService"/> <sofa:service ref="helloService" interface="XXXService">
<sofa:binding.bolt>
<sofa:global-attrs thread-pool-ref="customExcutor"/>
</sofa:binding.bolt>
</sofa:service>

那么实现原理是什么呢?

一起来看看。

源码分析

关键代码:


UserThreadPoolManager.
registerUserThread("com.alipay.sofa.rpc.quickstart.HelloService", threadPool);

UserThreadPoolManager 是一个用户自定义线程池管理器。里面包含一个 Map, key 是接口名称,value 是线程池(一个 UserThreadPool对象)。

看看这个 UserThreadPool。

很简单的一个类,封装了 JDK 的线程池。并初始化了一些线程池参数,比如:

  • corePoolSize = 10
  • maximumPoolSize = 100
  • keepAliveTime = 300000(线程回收时间(毫秒))
  • queueSize = 0
  • threadPoolName = "SofaUserProcessor" 线程名字
  • boolean allowCoreThreadTimeOut 是否关闭核心线程池
  • boolean prestartAllCoreThreads 是否初始化核心线程池
  • volatile ThreadPoolExecutor executor

初始化的时候,默认参数不变,核心线程数 10,最大 100,默认不关闭核心线程池,默认不初始化线程池。默认是 SynchronousQueue 队列,此队列性能最高,也可以设置成阻塞队列,或者优先级队列。当然,这些都是可以改的。

这个线程池什么时候回起作用呢?

先说结论:当 Netty 读取数据(channelRead 方法)后,通过层层调用,会调用 RpcRequestProcessor 类的 process 方法。该方法会拿到上下文的 UserProcessor 对象(bolt 的话,实现类是 BoltServerProcessor),UserProcessor 有一个内部接口 ExecutorSelector,线程池选择器,该选择器定义了一个 select 方法,返回的是线程池,如果用户自定义了线程池,就会返回自定义线程池(方式:UserThreadPoolManager.getUserThread(service)),如果没有,返回系统线程池。

来看看具体代码。

RpcHandler 我们很熟悉了,就是 Netty 的 handler ,ChannelRead 方法中,会调用 RpcCommandHandler 的 handleCommand 方法,该方法会提交到线程池执行。任务内容是执行 process 方法。

通过调用,最后会执行 RpcRequestProcessor 的 process 方法。调用栈如下:

105 行会有如下判断:

// to check whether get executor using executor selector
if (null == userProcessor.getExecutorSelector()) {
executor = userProcessor.getExecutor();
} else {
// in case haven't deserialized in io thread
// it need to deserialize clazz and header before using executor dispath strategy
if (!deserializeRequestCommand(ctx, cmd, RpcDeserializeLevel.DESERIALIZE_HEADER)) {
return;
}
//try get executor with strategy
executor = userProcessor.getExecutorSelector().select(cmd.getRequestClass(),
cmd.getRequestHeader());
}

尝试获取线线程池选择器,如果是 null, 使用系统线程池,如果不是 null,调用选择器的 select 方法得到线程池,随后,使用这个线程执行任务。

// Till now, if executor still null, then try default
if (executor == null) {
executor = (this.getExecutor() == null ? defaultExecutor : this.getExecutor());
} // use the final executor dispatch process task
executor.execute(new ProcessTask(ctx, cmd));

那么这个 select 方法是如何实现的呢?目前仅有一个实现,BoltServerProcessor 的内部类 UserThreadPoolSelector。该方法逻辑如下:

从 Header 中获取服务名称,根据服务名称调用 UserThreadPoolManager.getUserThread(service) ,如果返回值不是 null ,说明用户设置自定义线程池了,就返回该线程池。如果是 null,返回系统线程池。

而 BoltServerProcessor 的 getExecutorSelector 判断规则如下:

    @Override
public ExecutorSelector getExecutorSelector() {
return UserThreadPoolManager.hasUserThread() ? executorSelector : null;
} public static boolean hasUserThread() {
return userThreadMap != null && userThreadMap.size() > 0;
} public BoltServerProcessor(BoltServer boltServer) {
this.boltServer = boltServer;
this.executorSelector = new UserThreadPoolSelector(); // 支持自定义业务线程池
}

可以看到,BoltServerProcessor 默认就会创建一个内部类对象,只要 UserThreadPoolManager 里面的 Map 不是空,就会尝试调用 select 方法,如果通过服务名称找到缓存中的自定义线程池,就直接返回了。非常完美。

需要注意一点,系统线程池只有一个,默认核心线程池大小 20 ,最大 200。貌似这也是 tomcat 的默认配置,因此,并发很高的时候,可能就需要用户使用自定义线程池了,能够显著提高并发量。

总结

好了,关于自定线程池的原理探究的差不多了,这个功能挺有用的,当系统并发很高的时候,或者某个服务很慢,不能让该服务影响其他服务,就可以使用自定线程池,将这些慢服务和其他服务隔离开。

原理则是通过 UserThreadPoolManager 与 Server 进行交互,当 Server 执行任务的时候,会从当前的上下文中,找到与调用服务对应的线程池,如果有的话,就返回 UserThreadPoolManager 管理的线程池,如果没有,返回框架线程池。

具体判断的代码在 Bolt 模块 com.alipay.remoting.rpc.protocol.RpcRequestProcessor 的 process 方法中。

bye!!!

SOFA 源码分析 — 自定义线程池原理的更多相关文章

  1. 深入源码分析Java线程池的实现原理

    程序的运行,其本质上,是对系统资源(CPU.内存.磁盘.网络等等)的使用.如何高效的使用这些资源是我们编程优化演进的一个方向.今天说的线程池就是一种对CPU利用的优化手段. 通过学习线程池原理,明白所 ...

  2. 源码分析—ThreadPoolExecutor线程池三大问题及改进方案

    前言 在一次聚会中,我和一个腾讯大佬聊起了池化技术,提及到java的线程池实现问题,我说这个我懂啊,然后巴拉巴拉说了一大堆,然后腾讯大佬问我说,那你知道线程池有什么缺陷吗?我顿时哑口无言,甘拜下风,所 ...

  3. 从JDK源码角度看线程池原理

    "池"技术对我们来说是非常熟悉的一个概念,它的引入是为了在某些场景下提高系统某些关键节点性能,最典型的例子就是数据库连接池,JDBC是一种服务供应接口(SPI),具体的数据库连接实 ...

  4. SOFA 源码分析— 自定义路由寻址

    前言 SOFA-RPC 中对服务地址的选择也抽象为了一条处理链,由每一个 Router 进行处理.同 Filter 一样, SOFA-RPC 对 Router 提供了同样的扩展能力. 那么就看看 SO ...

  5. SOFA 源码分析 —— 服务引用过程

    前言 在前面的 SOFA 源码分析 -- 服务发布过程 文章中,我们分析了 SOFA 的服务发布过程,一个完整的 RPC 除了发布服务,当然还需要引用服务. So,今天就一起来看看 SOFA 是如何引 ...

  6. netty源码分析 - Recycler 对象池的设计

    目录 一.为什么需要对象池 二.使用姿势 2.1 同线程创建回收对象 2.2 异线程创建回收对象 三.数据结构 3.1 物理数据结构图 3.2 逻辑数据结构图(重要) 四.源码分析 4.2.同线程获取 ...

  7. jQuery 2.0.3 源码分析Sizzle引擎解析原理

    jQuery 2.0.3 源码分析Sizzle引擎 - 解析原理 声明:本文为原创文章,如需转载,请注明来源并保留原文链接Aaron,谢谢! 先来回答博友的提问: 如何解析 div > p + ...

  8. Memcached源码分析之线程模型

    作者:Calix 一)模型分析 memcached到底是如何处理我们的网络连接的? memcached通过epoll(使用libevent,下面具体再讲)实现异步的服务器,但仍然使用多线程,主要有两种 ...

  9. SOFA 源码分析 — 调用方式

    前言 SOFARPC 提供了多种调用方式满足不同的场景. 例如,同步阻塞调用:异步 future 调用,Callback 回调调用,Oneway 调用. 每种调用模式都有对应的场景.类似于单进程中的调 ...

随机推荐

  1. 【一天一道LeetCode】#98. Validate Binary Search Tree

    一天一道LeetCode 本系列文章已全部上传至我的github,地址:ZeeCoder's Github 欢迎大家关注我的新浪微博,我的新浪微博 欢迎转载,转载请注明出处 (一)题目 Given a ...

  2. 《.NET最佳实践》与Ext JS/Touch的团队开发

    概述 持续集成 编码规范 测试 小结 概述 有不少开发人员都问过我,Ext JS/Touch是否支持团队开发?对于这个问题,我可以毫不犹豫的回答:支持.原因是在Sencha官网博客中客户示例中,有不少 ...

  3. C++对象模型(五):The Semantics of Data Data语义学

    本文是<Inside the C++ Object Model>第三章的读书笔记.主要讨论C++ data member的内存布局.这里的data member 包含了class有虚函数时 ...

  4. Linux IPC实践(2) --匿名PIPE

    管道概念 管道是Unix中最古老的进程间通信的形式,我们把从一个进程连接到另一个进程的一个数据流称为一个"管道", 管道的本质是固定大小的内核缓冲区; 如:ps aux | gre ...

  5. mpi中程序在集群中的分发

    我们在开发mpi程序时,由于其是分布式程序,我们在单个节点上完成编码后,需要将代码拷贝到整个集群进行测试.集群之间的文件拷贝可以通过scp命令完成.但是scp命令是针对两个节点之间文件互传设计,为了将 ...

  6. 仿百度壁纸客户端(六)——完结篇之Gallery画廊实现壁纸预览已经项目细节优化

    仿百度壁纸客户端(六)--完结篇之Gallery画廊实现壁纸预览已经项目细节优化 百度壁纸系列 仿百度壁纸客户端(一)--主框架搭建,自定义Tab + ViewPager + Fragment 仿百度 ...

  7. The type java.lang.Object cannot be resolved. It is indirectly referenced from required .class files

    The type java.lang.Object cannot be resolved.It is indirectly referenced from required .class files ...

  8. Testbench(转)

    本来还打算自己写下对Testbench的理解,后来发现百度百科名片解释得很好,所以就直接转了. 原文百度百科链接:http://baike.baidu.com/link?url=dxzsOAs32IE ...

  9. MASM中3中文本宏的使用与区别

    = 宏 格式 : name = exp 其中,exp只能为32位整数值,且用=宏定义的符号名称可以重定义: EQU 宏 格式1:name EQU exp exp为有效整数值,可以重定义: 格式2:na ...

  10. 十分钟搞定mongodb副本集

    mongodb副本集配置 最近项目中用到了mongodb,由于是用mongodb来记录一些程序的日志信息和日常的统计,为了增加应用的可靠性,一直在找mongodb集群的一些资料,下面是对最近做的一个小 ...