上一篇章介绍了下NIO的基础,并且也给出了一个简单的代码示例,但是如果想享受NIO带来的高速的快感,就得使用多线程编程了。那么在使用多线程编程之前,有一些关于多线程的东西想分享下:
一、关于多线程
- 分享点一:在单核CPU上,多线程不一定能比单线程更好。为什么要使用多线程?很多人也许未必去考虑过,我个人认为,使用多线程是为了减少CPU等待的时间,最大化的利用CPU的性能。那为什么说多线程未必比单线程好呢?我简单举个案例。你要扫地和洗马桶,扫地要10分钟,洗马桶要10分钟,你一个人完成这两件事要多少时间?使用单线程工作方式是20分钟,而你如果使用多线程,扫一半地跑过去洗马桶再回过来扫地再回去去洗马桶,那么完成这两件事必定要超过20分钟了。但是如果你要做的事是煮饭和扫地,那么使用多线程就体现出优势了。
- 分享点二:选择多少个线程能充分利用服务器的性能呢?有很多理论是服务器用的是几核CPU就使用几个线程,这是一个比较通用的配置方式,仅限当你线程中执行的代码无阻塞时。要服务器性能越高,就是等同于让CPU处理业务逻辑的时间越长,所以开几个多线程就是考虑能充分利用这些CPU,但是注意不要过度开多线程,线程间的调度本身会消耗CPU,开越多,调度的代价越高。
多线程的开发要比单线程复杂很多,所以在进行多线程编程的情况下,要特别的小心。
二、网络模型
Reactor模式是我在接触netty之后才知道的一个模式,关于这个模式,在Netty实现原理浅析的网络模型里面有详尽的描述,当然作者文章也说,是参考Doug Lea的Scalable IO in Java这位大牛的文章,想要理解这个模型的,可以先去看上述两篇文章,接下去的内容,是基于我对于这些模式的理解的记录,顺便会根据Doug Lea文章的内容稍微翻译摘出来点:
一个最简单的网络模型拥有的基本模型大概是这样的:
- 读取数据
- 对数据解码
- 处理数据
- 对数据编码
- 发送数据
模型一:单线程的reactor模型:
读取数据,对数据解码,处理数据,对数据编码和发送都是在一个线程中的。此时的Reactor可以认为是服务器端的主线程,负责接受客户端的链接,链接建立完毕后将此链接负责分派到一个handler线程去处理剩下的事情。伪代码如下:
- /**
- * 代码来自Doug Lea
- **/
- Class Server implements Runnable{
- public void run(){
- try{
- ServerSocket ss = new ServerSocket(port);
- while(!Thread.interrupted()){
- new Thread(new Handler(ss.accept())).start();
- }
- }
- }
- }
- static class Handler implements Runnable{
- final Socket socket;
- Handler(Socket s){
- socket = s;
- }
- public void run(){
- try{
- byte[] input = new byte[MAX_INPUT];
- socket.getInputStream().read(input);
- byte[] output = process(input);
- socket.getOutputStream().write(output);
- }catch(IOException ex){
- /*...*/
- }
- }
- }
这个模式有什么优缺点?我一个单词一个单词的查询翻译了下Doug Lea的文章,也貌似没明确说,但在Netty原理浅析里面是这样阐述的:
该模型 适用于处理器链中业务处理组件能快速完成的场景。不过,这种单线程模型不能充分利用多核资源,所以实际使用的不多。
那怎么样才算能充分利用多核CPU资源呢?难道开了多个Handler操作就只是用到了一个CPU核么?想想也不可能,
我用下面代码测试:
- public class Test {
- public static void main(String[] ben){
- Thread work1 = new Work();
- work1.start();
- Thread work2 = new Work();
- work2.start();
- }
- }
- class Work extends Thread{
- public void run(){
- Long sum=0l;
- int i=0;
- while(i<100000){
- sum+=i;
- }
- System.out.println("end");
- }
- }
我CPU是双核,两个核立马彪满:
所以说上面的这个模型不能充分利用多核CPU,我是持保留意见滴。那么怎样的情况不能利用多核CPU呢?
假设当前服务器为四核CPU,当前只有一个handler线程,handler线程里面执行的业务处理很庞大,比如像上面我的示例,1到100万相加总和这样的计算,在这种情况下,如果把1到100万相加的handler再拆成四个线程去计算,各计算四分之一的数据,那么就可以充分利用四核CPU了(但是如果执行很快,比如计算1到10,拆了反而更慢,因为CPU还要调度的)。
但是又假设如果当前已经有四个handler线程甚至更多,就算不拆也已经用到四核了。
但是真的如果handler的业务逻辑执行很慢(非阻塞,计算量大的情况),要不要拆呢,因为在服务器端我相信同一个时间用户的访问并发应该会超过CPU的核数?所以个人认为不到需要极端优化性能的前提下还是没必要去拆,太复杂。
所以说,该模型适用于能快速执行的业务逻辑的系统或者多核CPU系统,在某种场景或者程度上是可以这么理解。
题外话,假设handler线程里面出现堵塞,会不会让CPU处于空闲呢?回答这个问题钱这,我又回去翻阅了Think in java中线程的一章,首先看线程的四个状态:
- 新(New):线程对象已经创建,但尚未启动,所以不可运行
-
可运行(Runnable):意味着一旦时间分片机制有空闲的CPU周期提供给一个线程,那这个线程便可立即运行。因此线程可能在、也可能不在运行当众,但一旦条件许可,没有什么能阻止它的运行------它既没有“死”掉,也未被"堵塞"。
-
死(Dead):从自己的run()方法中返回后,一个线程便已经"死"掉。亦可调用stop()令其死掉。.
- 堵塞(Blocked):线程可以运行,但有种东西阻碍了它。若线程处于堵塞状态,调度机制可以简单的跳过它,不给他分配任何CPU时间。除非线程再次进入"可运行"状态,否则不会采取任何操作
那么何为线程堵塞:
-
调用sleep(毫秒数),使线程进入"睡眠"状态。在规定的时间内,这个线程是不会运行的。
-
用suspend()暂定了线程的执行。除非线程收到resume()消息,否则不会返回"可运行"状态。
- 用wait()暂停了线程的执行。除非线程收到notify()或者notifyAll()消息,否则不可变成"可运行".
- 线程正在等候一些IO(输入输出)操作完成。
- 线程实体调用另一个对象的"同步"方法,但那个对象处于锁定状态,暂时无法使用.
所以如果handler线程里面有堵塞操作,那么就多开几个线程让CPU去切换执行即可,至于开多少个合适,看堵塞的时间而定。这个淘宝内部有公式的,但是堵塞时间不一定,所以还是需要通过TPS测试才能确定.
模型二:多线程处理的Reactor模式
所以说上面那个模型也不能说不好,继续跟着Doug Lea的文章继续前行了。Doug Lea提出了几点模型可扩展性的目标(Scalability Goals).
- 优雅的流量降级(当客户端连接持续增大到很多时,能避免系统被压垮)
- 能够持续的控制资源的使用增长(CPU,memory,disk,bandwidth)
- 能满足一些性能方面的目标,包括低延迟,满足高峰需求,服务质量的保证。
- 分而自治(Divide-and-conquer)通常是最好的方式.其实就是模块化吧。
Doug Lea首先介绍了Divide-and-conquer,其核心是将任务划分成小任务来处理,并且是非堵塞的小任务.我猜应该是支持了我上述的理论,将较大的任务分拆成小任务来处理,可能可以更好的利用多核CPU的性能,所以才有了下面这个模型:
这个模型把原来handler(这个大handler也可叫worker线程)中的对数据解码,数据处理,对数据编码抽离出来行程一个小handler,用过netty的都知道,netty可以添加多个handler并顺序执行,不知道大家有没有思考过,handler是处理业务逻辑的,我为什么要写多个handler?我一个handler就可以写完。如果放到这个模型里面来,就可以理解了,netty希望我们自己将大的handler分解成一个一个小的handler进行执行,只不过默认情况下这些handler都是在一个线程里面顺序执行(其实就跟一个handler是一样的概念,所以默认情况下netty在worker的选择上是模型一)的,或者更多默认情况下,我们只写了一个handler而已。关于此模型,暂时认为讲到这里即可,暂时还不需要深入.
模型三:多Reator线程的Reator模式 (好怪的名字,将就下)
上述两个模型都有一个通病,Reactor的Acceptor中,首先是建立链接,然后再把链接分配给指定的handler线程去执行,那么我们知道,建立链接是个阻塞动作,操作很慢,那么Doug Lea为了匹配CPU和IO的速率(Use to match CPU and IO rates)提出了下面这个模型:
Doug Lea的所谓匹配IO和CPU的速率,其实所得直白点是因为只有一个Reator情况下,因为Reator只能一个一个处理创建连接这个请求,如果客户端连接数量一大,就会在Reator形成一个单线程操作,并且创建链接又是一个阻塞动作,只有前一个客户的链接创建完毕才会处理下一个客户的链接请求,性能直线下降。所以这个模型就有了mainReator和subReactor这两个reactor。mainReator负责将用户请求的链接提交给subReactor去执行,subReactor则负责建立链接,并将建立后的链接发送到具体的work线程去执行.
下一篇章:会先写netty所使用的reactor模型与其根据netty源码简化后的代码。.