分享

实现简单服务器学习多线程与Executor

 IT乐知 2020-06-27

实现简单的服务器

一个最简单的服务器实现如下图:

1单线程

这是一个单线程的实现,也能满足基本的要求,但是如果请求多起来以后就会出现问题,由于同一时刻只能处理一个请求,服务器的响应性和吞吐量会急剧下降,改进成多线程模式如下图:

2多线程

多线程模式响应性和吞吐量都有所增加,但是创建线程和线程竞争都会消耗资源,所以它反而有可能会降低响应性,同时如果并发请求太多,会创建过多的线程,还会造成内存溢出导致程序崩溃。

每个请求都是一个任务,一个任务的执行都需要一个线程,但是又要控制线程创建的数量,Java中提供了线程池来支持这种方式,而线程池作为Executor框架的一部分。

Executor框架

Executor基于生产者、消费者模式,提交任务相当于消费者,执行任务相当于消费者,所以要实现一个生产者、消费者,最简单的方式就是通过Executor,生产者、消费者模式可以对任务的提交和执行进行解耦,因为任务的提交可能在工程的任何地方,但是执行一般只在一个位置,后期如果要对任务的执行进行修改就比较简单。

根据Executor方式来优化上面的例子如下图:

3exector

通过线程池的方式可以重复利用线程,避免线程的创建和销毁所消耗的资源,同时一个新任务过来时由于线程已经创建,可以马上执行也一定的提高了响应,创建足够的线程去执行任务,也可以限制线程数量太多导致的线程竞争造成的内存消耗,所以以后new Thread(runnable).start();”这类代码都可以考虑用Executor来替换。

线程池主要有以下4种:

newFixedThreadPool:固定长度线程池,提交一个任务创建一个线程,直到达到最大数量。如果某个线程出现错误没有正常结束,会新建一个线程补充。

newCachedThreadPool:可缓存的线程池,如果线程池数量超过当前处理需求将回收空闲线程,当需求增加时可以添加新线程,线程池规模不存在任何限制;

newSingleThreadExecutor:单线程,创建单个工作线程,如果线程异常,会创建另一个替代,保证任务串行执行。

newScheduledThreadPool:固定长度线程池,可以以延迟或定时的方式来执行任务

生命周期管理ExecutorService

同时通过对executor的使用也可以实现各种调优、管理、监视、记录日志、错误报告等其他功能,不过到目前只了解了executor提交和执行,如果在执行的过程中要结束服务,当然还需要对任务的结束,ExecutorService就扩展了executor提供了这个功能,主要提供了以下两个方法:

shutdown方法:将执行平缓的关闭过程,不再接受新任务,同时等待已经提交的任务执行完成,包括还没有开始的任务。

shutdownNow方法:粗暴执行,尝试取消所有运行中的任务,并不再启动队列中未执行的任务。

任务带返回结果

Runnable不能返回一个值或者抛出一个受检查的异常,对需要有返回值的请求并不支持,在Executor框架中提供了CallableFuture接口来实现有返回值的任务。Future提供了get方法来获取返回值,如果任务没有完成get方法会阻塞直到任务完成返回。

FutureTask同时实现了RunnableFuture,所以可以把一个任务实例化成FutureTask然后交给ExecutorService去执行,然后再通过FutureTaskget方法获取各自的返回值。

局限

前面讲的例子都是基于多个请求,他们各自任务没有任何相关性,所以在多线程情况下能够显著的提高性能和响应率,不过某些情况下多线程对性能提升可能并不那么明显。

比如一个任务能够分成2个任务完成,但是其中一个任务所花费的时间是另外一个任务的10倍,那么即使是这两个任务并发的执行所花费时间总的提升并不大。

渲染网页示例

现在用Java代码去渲染一个HTML,由于HTML包含文字和图片,而图片只有url需要再去下载需要消耗大量的时间,所以可以把渲染文本和下载图片分成两个任务执行,文本渲染的时候预留图片的位置,通过ExecutorServicesubmit方法提交下载图片任务,他会返回一个Future,通过Futureget方法获取到下载的图片然后再进行图片渲染。

这里只分了两个任务,但是一般一个html图片不止一张,下载的时间很长,当所有图片下载完采取渲染,总体渲染时间提升不大。可以一张图片建一个任务,多个下载任务一起提交,然后遍历所有任务对应的Future集合,每下载一张图片就渲染一张这样总体就快了很多了,不过通过我们自己去处理比较麻烦,Java提供了一个更好的方法:完成服务(CompletionService

完成服务(CompletionService

CompletionServiceExecutorBlockingQueue的功能结合体,可以将Callable任务提交给他来执行,然后使用类似队列的poll方法来获取已完成的结果,这些结果会再完成时将被封装为FutureExecutorCompletionServiceCompletionService的实现类,它包含一个Executor和一个BlockingQueueExecutorCompletionService部分源码如下图:

4

ExecutorCompletionServicesubmit方法没有截出来,它接受的是一个Callable的参数,这个Callable最终会通过上图中的方法转变成一个QueueingFuture放到exector中去执行,QueueingFuture中重写了done方法把task放入到队列中去,所以可以执行多次submit,然后调用ExecutorCompletionServicetake或者poll方法获取执行结果,其中还有一个“poll(long timeout, TimeUnit unit)”方法还可以规定阻塞的时间。

总结

通过创建一个最简单的服务器来学习我们为什么需要线程池以及Java中对线程池的支持,以及采用线程池的优势,线程池的实现方式,带返回结果的任务实现方式,多个任务通过ExecutorBlockingQueue实现,以及Java对这个方案的实现ExecutorCompletionService

Java程序员日常学习笔记,如理解有误欢迎各位交流讨论!

    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多