Java IO编程全解(三)——伪异步IO编程

  转载请注明出处:http://www.cnblogs.com/Joanna-Yan/p/7723174.html

  前面讲到:Java IO编程全解(二)——传统的BIO编程

  为了解决同步阻塞I/O面临的一个链路需要一个线程处理的问题,后来有人对它的线程模型进行了优化,后端通过一个线程池来处理多个客户端的请求接入,形成客户端个数M:线程池最大线程数N的比例关系,其中M可以远远大于N,通过线程池可以灵活的调配线程资源,设置线程的最大值,防止由于海量并发接入导致线程耗尽。

  下面,我们结合连接模型图和源码,对伪异步I/O进行分析,看它是否能够解决同步阻塞I/O面临的问题。

1. 伪异步I/O模型图

  采用线程池和任务队列可以实现一种叫做伪异步的I/O通信框架,它的模型图如下所示。

  当有新的客户端接入的时候,将客户端的Socket封装成一个Task(该任务实现java.lang.Runnable接口)投递到后端的线程池中进行处理,JDK的线程池维护一个消息队列和N个活跃线程对消息队列中的任务进行处理。由于线程池可以设置消息队列的大小和最大线程数。因此,它的资源占用是可控的,无论多少个客户端并发访问,都不会导致资源的耗尽和宕机。

图1 伪异步I/O服务端通信模型(M:N)

  下面我们依然采用时间服务器程序,将其改造成为伪异步I/O时间服务器,然后通过对代码进行分析,找出其弊端。

2.伪异步式I/O创建的TimeServer源码分析

package joanna.yan.poio;

import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
/**
 * 伪异步式I/O
 * @author Joanna.Yan
 * @date 2017年10月24日上午10:16:10
 */
public class TimeServer {

    public static void main(String[] args) {
        int port=9090;
        if(args!=null&&args.length>0){
            try {
                port=Integer.valueOf(args[0]);
            } catch (Exception e) {
                // 采用默认值
            }
        }

        ServerSocket server=null;
        try {
            server=new ServerSocket(port);
            System.out.println("The time server is start in port :"+ port);
            Socket socket=null;
            //创建一个时间服务器类的线程池
            TimeServerHandlerExecutePool singleExecutor=new
                    TimeServerHandlerExecutePool(50, 10000);//创建I/O任务

            while(true){
                socket=server.accept();
                //当接收到新的客户端连接时,将请求Socket封装成一个Task,然后调用execute方法执行。从而避免了每个请求接入都创建一个新的线程。
                singleExecutor.execute(new TimeServerHandler(socket));
            }
        } catch (IOException e) {
            e.printStackTrace();
        }finally{
            if(server!=null){
                try {
                    System.out.println("The time server close");
                    server=null;
                    server.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}
package joanna.yan.poio;

import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
/**
 * 由于线程池和消息队列都是有界的,因此,无论客户端并发连接数多大,它都不会导致线程个数过于膨胀或者内存溢出,
 * 相比于传统的一连接一线程模型,是一种改良。
 * @author Joanna.Yan
 * @date 2017年10月24日下午2:39:49
 */
public class TimeServerHandlerExecutePool {
    private ExecutorService executor;

    public TimeServerHandlerExecutePool(int maxPoolSize,int queueSize){
        executor=new ThreadPoolExecutor(Runtime.getRuntime().availableProcessors(),
                maxPoolSize, 120L, TimeUnit.SECONDS,
                new ArrayBlockingQueue<java.lang.Runnable>(queueSize));

    }

    public void execute(java.lang.Runnable task){
        executor.execute(task);;
    }
}
package joanna.yan.poio;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;
import java.util.Date;

public class TimeServerHandler implements Runnable{
    private Socket socket;

    public TimeServerHandler(Socket socket) {
        this.socket = socket;
    }

    @Override
    public void run() {
        BufferedReader in=null;
        PrintWriter out=null;

        try {
            in=new BufferedReader(new InputStreamReader(this.socket.getInputStream()));
            out=new PrintWriter(this.socket.getOutputStream(), true);
            String currentTime=null;
            String body=null;
            while(true){
                body=in.readLine();
                if(body==null){
                    break;
                }
                System.out.println("The time server receive order:"+body);
                //如果请求消息为查询时间的指令"QUERY TIME ORDER"则获取当前最新的系统时间。
                currentTime="QUERY TIME ORDER".equalsIgnoreCase(body) ?
                        new Date(System.currentTimeMillis()).toString() : "BAD ORDER";
                out.println(currentTime);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }finally{
            if(in!=null){
                try {
                    in.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if(out!=null){
                out.close();
                out=null;
            }
            if(this.socket!=null){
                try {
                    this.socket.close();
                    this.socket=null;
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

  伪异步I/O通信框架采用了线程池实现,因此避免了为每个请求都创建一个独立线程造成的线程资源耗尽问题。但是由于它底层的通信依然采用同步阻塞模型,因此无法从根本上解决问题。

3.伪异步I/O弊端分析

  要对伪异步I/O的弊端进行深入分析,首先我们看两个Java同步I/O的API说明。随后我们结合代码进行详细分析。

  请注意加粗斜体字部分的API说明,当对Socket的输入流进行读取操作的时候,它会一直阻塞下去,直到发生如下三种事件。

  • 有数据可读;
  • 可用数据已经读取完毕;
  • 发生空指针或者I/O异常。

  这意味着当对方发送请求或者应答消息比较缓慢、或者网络传输较慢时,读取输入流一方的通信线程将被长时间阻塞,如果对方要60s才能够将数据发送完成,读取一方的I/O线程也将会被同步阻塞60s,在此期间,其他接入消息只能在消息队列中排队。

  下面我们接着对输出流进行分析,还是看JDK I/O类库输出流的API文档,然后结合文档说明进行故障分析。

  当调用OutputStream的write方法写输出流的时候,它将会被阻塞,直到要发送的字节全部写入完毕,或者发生异常。学习过TCP/IP相关知识的人都知道,当消息的接收方处理缓慢的时候,将不能及时地从TCP缓冲区读取数据,这将会导致发送方的TCP window size不断减小,直到为0,双方处于Keep-Alive状态,消息发送方将不能再向TCP缓冲区写入消息,这是如果采用的是同步阻塞I/O,write操作将会被无限期阻塞,直到TCP window size大于0或者发生I/O异常。

  通过对输入和输出流的API文档进行分析,我们了解到读和写操作都是同步阻塞的,阻塞的时间取决于对方I/O线程的处理速度和网络I/O传输速度。本质上来讲,我们无法保证生产环境的网络状况和对端的应用程序能够足够快,如果我们的应用程序依赖对方的处理速度,它的可靠性就非常差。

  伪异步I/O实际上仅仅只是对之前I/O线程模型的一个简单优化,它无法从根本上解决同步I/O导致的通信线程阻塞问题。下面我们就简单分析下如果通信对方返回应答时间过长,会引起的级联故障。

  1.  服务端处理缓慢,返回应答消息耗费60s,平时只需要10ms。
  2. 采用伪异步I/O的线程正在读取故障服务节点的响应,由于读取输入流是阻塞的,因此,它将会被同步阻塞60s。
  3. 假如所有的可用线程都被故障服务器阻塞,那后续所有的I/O消息都将在队里中排队。
  4. 由于线程池采用阻塞队里实现,当队列积满之后,后续入队的操作将被阻塞。
  5. 由于前端只有一个Accptor线程接收客户端接入,它被阻塞在线程池的同步阻塞队列之后,新的客户端请求消息将被拒绝,      客户端会发生大量的连接超时。
  6. 由于几乎所有的连接都超时,调用者会认为系统已经崩溃,无法接收新的请求消息。

  那么这个问题如何解决?后面的NIO将给出答案。

Java IO编程全解(四)——NIO编程

如果此文对您有帮助,微信打赏我一下吧~

时间: 2024-11-03 22:38:47

Java IO编程全解(三)——伪异步IO编程的相关文章

Java IO编程全解(六)——4种I/O的对比与选型

转载请注明出处:http://www.cnblogs.com/Joanna-Yan/p/7804185.html 前面讲到:Java IO编程全解(五)--AIO编程 为了防止由于对一些技术概念和术语的理解或者叫法不一致而引起歧义,这里对涉及到的专业术语或者技术用语做下声明:如果它们与其他一些地方的称呼不一致,请以本解释为准. 异步非阻塞I/O 很多人喜欢将JDK1.4提供的NIO框架成为异步非阻塞I/O,但是,如果严格按照UNIX网络编程模型和JDK的实现进行区分,实际上它只能被称为非阻塞I/

javaNIO原理(含代码)及与 同步阻塞IO 、伪异步IO比较

一.同步阻塞IO BIO就是阻塞式的IO,网络通信中对于多客户端的连入,服务器端总是与客户端数量一致的线程去处理每个客户端任务,即,客户端与线程数1:1,并且进行读写操作室阻塞的,当有你成千上完的客户端进行连接,就导致服务器不断的建立新的线程,最后导致低通资源不足,后面的客户端不能连接服务器,并且连接入的客户端并不是总是在于服务器进行交互,很可能就只是占用着资源而已. 二.伪异步IO 伪异步IO对同步IO进行了优化,后端通过一个线程池和任务队列去处理所有客户端的请求,当用完后在归还给线程池,线程

伪异步IO理解

伪异步IO实在阻塞IO的基础上将每一个客户端发送过来的请求由新创建的线程来处理改进为用线程池来处理,因此避免了为每一个客户端请求创建一个新线程造成的资源耗尽问题. 来看一下伪异步IO的服务端代码: 线程池类 import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.ExecutorService; import java.util.concurrent.ThreadPoolExecutor; impor

PHP漏洞全解(三)-客户端脚本植入

本文主要介绍针对PHP网站的客户端脚本植入攻击方式.所谓客户端脚本植入攻击,是指将可以执行的脚本插入到表单.图片.动画或超链接文字等对象内.当用户打开这些对象后,黑客所植入的脚本就会被执行,进而开始攻击. 客户端脚本植入(Script Insertion),是指将可以执行的脚本插入到表单.图片.动画或超链接文字等对象内.当用户打开这些对象后,攻击者所植入的脚本就会被执行,进而开始攻击. 可以被用作脚本植入的HTML标签一般包括以下几种: 1.<script>标签标记的javascript和vb

理论铺垫:阻塞IO、非阻塞IO、IO多路复用/事件驱动IO(单线程高并发原理)、异步IO

完全来自:http://www.cnblogs.com/alex3714/articles/5876749.html 同步IO和异步IO,阻塞IO和非阻塞IO分别是什么,到底有什么区别?不同的人在不同的上下文下给出的答案是不同的.所以先限定一下本文的上下文. 本文讨论的背景是Linux环境下的network IO. 一 概念说明 在进行解释之前,首先要说明几个概念:- 用户空间和内核空间- 进程切换- 进程的阻塞- 文件描述符- 缓存 I/O 用户空间与内核空间 现在操作系统都是采用虚拟存储器,

Java网络编程基础(三)---基于UDP编程

前面在介绍TCP/IP协议的时候我们已经提到,在TCP/IP协议的传输层除了TCP协议外还有一个UDP协议,相比UDP的应用不如TCP广泛,但是随着计算机网络的发展UDP协议正越来越显示出及其威力,尤其是在需要很强的实时交互性的场合,例如网络游戏和视频会议等,UDP更是显示出极强的威力. UDP采用Datagram(数据报)传输,数据包是一种尽力而为的传送数据的方式,它只是 把数据的目的地记录在数据包中,然后就直接放在网络上,系统不保证数据是否能安全到达,或者什么时候可以送到,它并不保证传送质量

Objective-C 编程全解-第04章 对象的类型和动态绑定

第04章 对象的类型和动态绑定 Objective-C的一个重要特征就是动态性,本章将对Objective-C的动态类型(dynamic typing)和动态绑定(dynamic binding)进行说明. 4.1 动态绑定 4.1.1 什么是动态绑定 Objective-C中的消息是在运行时才去绑定的.运行时系统首先会确定接受者的类型(动态类型识别),然后根据消息名在类的方法列表里选择相应的方法执行,如果没有找到就到父类中去继续寻找,假如一直找到NSObject也没有找到就要调用的方法,就会报

伪异步IO

概念 客户端: 1 package bhz.bio2; 2 3 import java.io.BufferedReader; 4 import java.io.IOException; 5 import java.io.InputStreamReader; 6 import java.io.PrintWriter; 7 import java.net.Socket; 8 import java.net.UnknownHostException; 9 10 public class Client

Jsp—01—Java server page 全解

一.Jsp的原理 tomacat 还是只认识servlet,我们自己写的jsp通过 JspServlet引擎转换成了一个servlet.java文件;我们在.jsp文件中,写的都是<html><font>之类的,然后在JspServlet引擎的作用下, 都转换成了resp.getWriter().write("<html>")resp.getWriter().write("<font>")这就非常方便我们了, 我们可以