由于本项目使用的 Apache Mina 的框架进行网络通信。当然其多线程模式也应该在 Mina 框架中体现出来。
为了理解多线程模式,首先要了解 Mina 的运作方式。
此次便于解说,假设客户端也是采用了 Mina 框架来进行,实际上本项目的客户端只是简单的使用了 windows 的 Socket 通信。在当前假设下,其通信过程如下图 16 所示。
图 16 Apache Mina 的通信过程
本项目只关注服务端,其服务端的通信过程如下:
1 、通过 SocketAcceptor 同客户端建立连接;
2 、链接建立之后 I/O 的读写交给了 I/O Processor 线程, I/O Processor 是多线程的;
3 、通过 I/O Processor 读取的数据经过 IoFilterChain 里所有配置的 IoFilter , IoFilter 进行消息的过滤,格式的转换,在这个层面可以制定一些自定义的协议;
4 、最后 IoFilter 将数据交给 Handler 进行业务处理,完成了整个读取的过程;
5 、写入过程也是类似,只是刚好倒过来,通过 IoSession.write 写出数据,然后 Handler 进行写入的业务处理,处理完成后交给 IoFilterChain ,进行消息过滤和协议的转换,最后通过 I/O Processor 将数据写出到 socket 通道。
服务端的一个简化过程如下图 17 所示:
图 17 多级线程池
在基于 SocketAcceptor 的应用程序中,运行过程中 Mina 框架本身会有两种类型的线程在运行,一种是在 SocketAcceptor 中创建的用于监听并接收来自客户端请求的线程,还有一类线程是处理客户端与服务器端 I/O 的线程,即 Processor 的处理线程。后面还有第三类,就是过滤器层的线程。这个多级的概念后面你会体会到, Acceptor 是一级线程池,而 Processor 的线程池主要通过 ExecutorFileter 进行添加,当然可以添加多个层次的线程池。下面逐层进行讲解。
1 、第一类线程池:
当调用 SocketAcceptor 的 bind 方法时,默认会创建一个名称前缀为 SocketAcceptor 的线程,该线程负责监听来自客户端的请求,如果接收到客户端的请求,它仅仅是为处理这个请求做好准备,而把具体处理请求以及 I/O 的任务代理给 SocketIoProcessor ,让它去处理请求。这个准备过程主要是为接受到的请求创建一个 IoSession ,并构建出 IoFilter 链,然后把准备好的数据以及来自客户端的请求交给 SocketIoProcessor 处理。通常 这类线程会针对每一次的 bind 调用创建一个新的线程。
请注意 “通常 ” 二字,使用这两个字说明上述的行为不是绝对的。的确这不是绝对的,这取决于 SocketAcceptor 的字段 executor 的实现,可以通过构造方法来设置字段 executor 的值, executor 字段的类型为 java.util.concurrent.Executor 。 mina 默认是用 org.apache.mina.util.NewThreadExecutor ,它会为每一个提交的任务创建一个新的线程。因为在 bind 方法中它会把实现监听客户端请求任务的 Runnable 提交到 executor 中去执行。注意千万不要使用一个不创建新线程而是在原线程中执行的 Executor ,这会使程序无法监听客户端的请求,因为程序中的唯一线程会被 Selector.get() 方法所阻塞,详情可以查看 SocketAcceptor 类的源代码。
第二类线程池:
当 SocketAcceptor 收到了来自客户端的请求,它就会把此请求丢给 SocketIoProcessor 去处理,这会创建名称以 SocketAcceptorIoProcessor 为前缀的线程, mina 框架在这类线程中处理 I/O 发布并处理事件。这一类线程的数量可以通过 SocketAcceptor 的构造函数来设置。具体的值可以根据应用的具体需求来决定。
作为 I/O 真正处理的线程,存在于服务器端和客户端,用来处理 I/O 的读写操作,线程的数量是可以配置的,默认最大数量是 CPU 个数 +1 。
在服务器端中,在创建 SocketAcceptor 的时候指定 ProcessorCount 。
SocketAcceptor acceptor =
new SocketAcceptor(Runtime.getRuntime().availableProcessors() + 1, Executors.newCachedThreadPool());
NioProcessor 虽然是多线程,但是对与一个连接的时候业务处理只会使用一个线程进行处理( Processor 线程对于一个客户端连接只使用一个线程 NioProcessor-n )如果 handler 的业务比较耗时,会导致 NioProcessor 线程堵塞 ,在 2 个客户端同时连接上来的时候会创建第 2 个(前提是第 1 个 NioProcessor 正在忙),创建的最大数量由 Acceptor 构造方法的时候指定。如果:一个客户端连接同服务器端有很多通信,并且 I/O 的开销不大,但是 Handler 处理的业务时间比较长,那么需要采用独立的线程模式,在 FilterChain 的最后增加一个 ExecutorFitler ,这个就是第三类线程池了。
第三类线程池:
上述的两类线程是 mina 框架本身所创建的,如果你的应用每次处理请求的时间较长而又希望应用能够有较好的响应性,那么最好是把处理业务逻辑的任务放到一个新的线程中去执行,而不是在 mina 框架创建的线程中去执行。 mina 框架本身提供了一个过滤器 ExecutorFilter 来完成这样的任务,它会把在此之后的过滤器以及 IoHandler 中处理业务逻辑的代码放到一个新的线程中去执行。当 mina 框架中的第二类线程执行完此过滤器后就会立即返回,可以用于处理新的请求。如果不想使用此过滤器,还可以设置 mina 的线程模型来达到相同的效果,其实线程模型也是使用 ExecutorFilter 实现的。但需要注意的是,在 mina 2.0 版本中已经废弃了线程模型。
使用类这三次线程池,性能可以得到保证了,在本项目中,主要配置了第二类线程池和第三类线程池。第二类线程池在新建 NioAcceptor 对象(以建立 TCP 监听服务器为例)时候,在其构造函数中体现,而这个数值需要多次测试来设定,其测试方法在国外网站有完整表述,请自行 Google ;第三类线程池设定在 Apache Mina 的过滤器层,一般而言只需要设置一层,设置在最消耗时间的业务前面,如比较复杂的解码,或者是数据库访问模块。
关于共享线程池问题, Apache Mina 有个官方说法:你可以想让 IoServices 和 ExecutorFilters 共享一个线程池,而不是一家一个。这个是不禁止的,但是会出现很多问题,在这种情况下,除非你为 IoServices 建立一个缓冲线程池。 本人尚未考究。