线程模型

首先我们来谈一谈java中常见的几种IO线程模型

我们知道一般io(socket)都是由accept,read,write,close几种状态组成

####同步阻塞(bio)
在read时需要无限等待直到消息到达,就是阻塞,同步则指的是每一步都需要等待上一步完成然后被调用
如果图片失效,请邮件联系作者补图

####同步非阻塞()
同步阻塞和同步非阻塞的区别就在于,在read时无论是否有数据,立刻返回。那么或许有人会问了,这样有什么意义,还需要自己写while循环包裹来促使其不断访问直到数据到达。其实针对这一点,如果一个程序在底层进入了阻塞状态,也就意味着我们失去了对其控制,对于socket来说,我们只能通过close来使其断开连接离开阻塞状态,而如果我们是非阻塞的情况下,我们发现read数据未到达,可以先允许该线程去做其他工作,过一会再来read一次检测下消息是否到达,同时我们也可以通过标记位来控制其后续行为
如果图片失效,请邮件联系作者补图

####io多路复用(nio)
其实nio也被算作同步非阻塞,但是在使用时也可以成为异步非阻塞,不过我们不必拘泥于这些分类,在发展中,是先有的模型,后来才被分类,所以很多情况下分类是模棱两可的。
io多路复用跟之前说的同步非阻塞有点关系,io多路复用的read也是非阻塞的,跟之前的最大区别在于,他采用了Selector选择器负责监听每一个socket的各种行为,当该行为被激活的时候,通知后续线程去处理。
我们可以想象,此时有巨量的socket链接进来,我们需要为每一个socket创建一个线程来read(即使是使用线程池减少了创建线程的消耗,那么大量的线程也依旧会在while(){//read}上浪费掉),此时我们就需要一个方案来解放这些线程无意义的循环read
一个管理者,来管理所有的Socket,这也就诞生了Selector选择器,由Selector负责检测是否有accept,read,write行为,并且通知其他线程来处理,这样我们可以节约大量线程,配合线程池我们就可以用有限的资源处理大量的连接
假设我们将流程分类为,io监听和io接收,业务处理三部分,那么nio的核心就是在于将io监听给提取出来单独管理
如果图片失效,请邮件联系作者补图

####异步
说到异步,阻塞与非阻塞的界限更为模糊。
下面让我们来看一段代码,这段代码并不是异步,他只是一个回调雏形,后面我会谈到

public interface CallBack {
    void callback();
}

public class Main {
    public void work(CallBack callBack){
        //业务代码省略...
        callBack.callback();
    }
    public static void main(String[] args) {
        Main main = new Main();
        System.out.println(1);
        main.work(new CallBack() {
            @Override
            public void callback() {
                System.out.println(2);
            }
        });
        System.out.println(3);
    }
}

如果你对代理模式比较熟悉,那么这里你肯定会产生疑问:这不就是代理模式么?
嗯,没错,这个东西在我眼里就是代理模式,只不过我们一般使用的代理模式的代码是写死在代理类中,而这里我们传入了自定义的代码,这就是回调的雏形

你肯定会问,这有什么用?还不如直接在一个方法里从头到尾写下来。
这是因为我们还没有引入其他的模型,假设我们引入多线程,那么我们的代码就成了这样

public interface CallBack {
    void callback();
}

public class Main {
    public void work(CallBack callBack){
        //业务代码
        callBack.callback();
    }
    public static void main(String[] args) {
        Main main = new Main();
        System.out.println(1);
        new Thread(new Runnable() {
            @Override
            public void run() {
                main.work(new CallBack() {
                    @Override
                    public void callback() {
                        System.out.println(2);
                    }
                });
            }
        }).start();
        System.out.println(3);
    }
}

通过对比,我们发现,引入了线程的概念后,他的意义就完全改变,变成了一种近似异步(不必在乎这些概念,你重点关注的应该是是否对于性能有真正的提升)的实现。
假设我们面对这样一个场景(此处我们先以非阻塞为例,否则引入自变量过的多不宜于理解),两个socket AB互相长期通信,且每次通信在业务上(我们先将流程简单的分为为io,业务两部分)所需要耗费的时间是不确定的,假若说我们采用同步的方式,每一次A发往B,因B只有一根线程,需要顺序的处理读io,业务操作,写io后才可继续处理A的后续请求。
而现在,我们将双方模型改为异步,A只要有请求就向B发送,无需等待B响应,当B读取完消息后(你可能会问A一直在发送消息,B怎么知道A是发送到一个请求还是两个请求,这一点你可以去了解粘包拆包的问题),将消息封装为一个任务,递交给线程池执行(执行完毕后会将执行回调函数来决定接下来的操作,由于任务耗时的不确定性,如果返回消息的话,消息的先后顺序也是不确定的,所以A在请求时需要附带消息的序列号),并立刻返回A一条消息表示自己已经接收到了请求。

到这里你会觉得一切豁然开朗,你仿佛明白了同步异步,阻塞非阻塞,感觉自己成为了大佬。但是,我刚才做的将同步改为异步的操作,真的提高了性能么,假设我线程池只设置一根线程,那么性能跟io和业务在同一根线程有区别么,这真的是异步带来的福利,还是仅仅是多线程带来的福利?我只不过是让A提前知道了,B已经接收到了来自A的消息,但是实际如果线程池只有一根线程的话,业务处理时间是不会改变的。那么异步的意义何在?仅仅是为了利用起多线程并发处理业务这个效果么?



答案:
 异步确实起到了利用多线程的作用,这里的异步我们要明确,异步并非是一个确切的概念,而是一个抽象宏观的概念,是针对于观察点而不断变化的,例如在当前这个场景中,如果A只有当收到B的处理结果才会继续发送,那么B的异步还算是真正的异步吗?我们当然可以说B是异步的,但是对于整体来说,他又是同步的,B在此时的是无法体现其性能优势的。假如说在这个基础上,有许许多多的A连接同一个B向其发送消息,此时B针对每一个连接起一个io线程(这里当然可以用Selector选择器配合io线程池),接到消息后扔到线程池(即使线程池只有一根线程,但是由于io是并发的,省去了io时间)去处理,这时候B又能体现他的性能优势了

那么接下来我们抛开异步同步阻塞非阻塞这个问题,从性能方面总结一下,之前我们提到的线程模型,变化繁多,那么他们为了性能所做的改变都有什么共同点呢?
将职责精细划分,对于每一部分职责分别进行深度优化,使得每一部分职责成为一个组件,各组件之间相互通信,以避免某一组件因为另一组件的原因而造成无意义的等待
在并发量低的环境下,由于我们机器可以开足够的线程来处理消息,即使义务处理因为io产生了等待,其他的消息也可以选择其他线程去处理。而当并发量增高,此时如果我们线程随之增高的话,会产生大量的线程上下文切换开销,所以我们不得不把控线程的数目,转而通过技巧来充分利用起每一条线程(例如线程池,组件功能划分等方式),这也就是这些线程模型存在并逐渐演化的原因

拓展:
{% post_link Tomcat源码笔记 Tomcat源码笔记 %}最尾处的Tomcat线程模型

待续

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值