伪异步IO编程

BIO主要的问题在于每当有一个新的客户端请求接入时,服务端必须创建一个新的线程处理新接入的客户端链路,一个线程只能处理一个客户端连接。在高性能服务器应用领域,往往需要面向成千上万个客户端的并发连接,这种模型显然无法满足高性能、高并发接入的场景。为了改进一线程一连接模型,后来又演进出了一种通过线程池或者消息队列实现1个或者多个线程处理N个客户端的模型,由于它的底层通信机制依然使用同步阻塞I/O,所以被称为“伪异步”。

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

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

服务端示例:

import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
public class TimeServer {
//首先创建一个时间服务器处理类的线程池,当接收到新的客户端连接的时候,将请求Socket封装成一个Task,
//然后调用线程池的execute方法执行,从而避免了每个请求接入都创建一个新的线程。
public static void main(String[] args) throws IOException {
int port = 8080;
if (args != null && args.length > 0) {
try {
port = Integer.valueOf(args[0]);
} catch (NumberFormatException 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();
singleExecutor.execute(new TimeServerHandler(socket));
}
} finally {
if (server != null) {
System.out.println("The time server close");
server.close();
server = null;
}
}
}
}
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;
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);
currentTime = "QUERY TIME ORDER".equalsIgnoreCase(body) ? new java.util.Date(
System.currentTimeMillis()).toString() : "BAD ORDER";
out.println(currentTime);
} } catch (Exception e) {
if (in != null) {
try {
in.close();
} catch (IOException e1) {
e1.printStackTrace();
}
}
if (out != null) {
out.close();
out = null;
}
if (this.socket != null) {
try {
this.socket.close();
} catch (IOException e1) {
e1.printStackTrace();
}
this.socket = null;
}
}
}
}
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
public class TimeServerHandlerExecutePool { private ExecutorService executor; public TimeServerHandlerExecutePool(int maxPoolSize, int queueSize) {
executor = new ThreadPoolExecutor(Runtime.getRuntime()
.availableProcessors(), maxPoolSize, 120L, TimeUnit.SECONDS,
new ArrayBlockingQueue(queueSize));
} public void execute(java.lang.Runnable task) {
executor.execute(task);
}
}

由于线程池和消息队列都是有界的,因此,无论客户端并发连接数多大,它都不会导致线程个数过于膨胀或者内存溢出,相比于传统的一连接一线程模型,是一种改良。客户端代码并没有改变,详见BIO的类。

伪异步I/O弊端分析

I/O类库输入流InputStreamAPI说明:

当对Socket的输入流进行读取操作的时候,它会一直阻塞下去,直到发生如下三种事件。

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

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

I/O类库输出流OutputStream的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)由于几乎所有的连接都超时,调用者会认为系统已经崩溃,无法接收新的请求消息。

JDK 伪异步编程(线程池)的更多相关文章

  1. JAVA并行异步编程,线程池+FutureTask

    java 在JDK1.5中引入一个新的并发包java.util.concurrent 该包专门为java处理并发而书写. 在java中熟悉的使用多线程的方式为两种?继续Thread类,实现Runnal ...

  2. Java编发编程 - 线程池的认识(一)

    每逢面试都会询问道线程池的概念和使用,但是工作中真正的又有多少场景使用呢?相信大家都会有这样的疑问:面试选拔造汽车,实际进公司就是拧螺丝!但是真正要把这颗螺丝拧紧,拧牢,没有这些最底层的知识做铺垫你可 ...

  3. PAIP.并发编程 多核编程 线程池 ExecutorService的判断线程结束

    PAIP.并发编程 多核编程 线程池 ExecutorService的判断线程结束 ExecutorService并没有提供什么 isDone()或者isComplete()之类的方法. 作者Atti ...

  4. 并发编程-线程池&J.U.C

    8. 共享模型之工具 8.1 线程池 池化技术相比大家已经屡见不鲜了,线程池.数据库连接池.Http 连接池等等都是对这个思想的应用.池化技术的思想主要是为了减少每次获取资源的消耗,提高对资源的利用率 ...

  5. UNIX网络编程——线程池模式比较(ICE线程池模型和L/F领导者跟随者模式)

    程池模式一般分为两种:L/F领导者与跟随者模式.HS/HA半同步/半异步模式. HS/HA 半同步/ 半异步模式 :分为三层,同步层.队列层.异步层,又称为生产者消费者模式,主线程处理I/O事件并解析 ...

  6. Java 并发编程 | 线程池详解

    原文: https://chenmingyu.top/concurrent-threadpool/ 线程池 线程池用来处理异步任务或者并发执行的任务 优点: 重复利用已创建的线程,减少创建和销毁线程造 ...

  7. Java异步、线程池解决方案

    一.ThreadPoolExecutor------线程池 private static final ThreadPoolExecutor threadPoolExecutor = new Threa ...

  8. Linux下简单的多线程编程--线程池的实现

    /* 写在前面的话: 今天刚“开原”,选择了一篇关于线程池的文件与大家分享,希望能对您学习有所帮助,也希望能与大家共同学习! 选择在这个特殊的时候注册并发文章也是有一些我个人特殊的意义的,看我的id( ...

  9. 《java学习三》并发编程 -------线程池原理剖析

    阻塞队列与非阻塞队 阻塞队列与普通队列的区别在于,当队列是空的时,从队列中获取元素的操作将会被阻塞,或者当队列是满时,往队列里添加元素的操作会被阻塞.试图从空的阻塞队列中获取元素的线程将会被阻塞,直到 ...

随机推荐

  1. xmpp笔记2(客户端到服务器的例子)--xml

    xmpp( 客户端到服务器的例子 ) 1 步:客户端初始流给服务器: <stream:stream xmlns='jabber:client' xmlns:stream='http://ethe ...

  2. C++ 类的静态成员及静态成员函数

    对象与对象之间的成员变量是相互独立的.要想共用数据,则需要使用静态成员和静态方法. 只要在类中声明静态成员变量,即使不定义对象,也可以为静态成员变量分配空间,进而可以使用静态成员变量.(因为静态成员变 ...

  3. NYOJ之Fibonacci数

    aaarticlea/png;base64,iVBORw0KGgoAAAANSUhEUgAAAskAAAJwCAIAAAD0kmsHAAAgAElEQVR4nO3dvXLbOMM24O8k3PtA3E

  4. Ionic2 Tutorial

    build your first app Now that you have Ionic and its dependencies installed, you can build your firs ...

  5. Java -- 找不到或无法加载主类

    原文:http://wenku.baidu.com/link?url=5nS1GEaePn-hmtAg6xXdJvtt9Z89JQsakhqSv8fambaJY2t9nKPtf3hXFpjW-BtD9 ...

  6. MVC学习笔记---各种上下文context

    0  前言 AspNet MVC中比较重要的上下文,有如下: 核心的上下文有HttpContext(请求上下文),ControllerContext(控制器上下文) 过滤器有关有五个的上下文Actio ...

  7. 在多台服务器上简单实现Redis的数据主从复制(3)(转载)

    转载地址:http://www.cnblogs.com/liping13599168/archive/2011/04/14/2016226.html Redis的主从复制功能非常强大,一个master ...

  8. Delphi编程建议遵守的规范1---缩进、各种语句的用法

    在编程时候,尤其是在一个大的团队里面,遵守统一的编程规范是极其重要的.为所有的开发人员制定一个源代码书写标准,以及程序和文件的命名标准,使他们在编程时有一致的格式,这样,每个编程人员编写的代码能够被其 ...

  9. 一个简单的Promise 实现

    用了这么长时间的promise,也看了很多关于promise 的文章博客,对promise 算是些了解.但是要更深的理解promise,最好的办法还是自己实现一个. 我大概清楚promise 是对异步 ...

  10. 玩转SSRS第九篇---匿名访问的一个间接方法

    SSRS是一个功能丰富的报表平台,我们可以在这个平台上实现各种不同需求的报表应用,所以这个平台也吸引了很多.net框架之外的技术,希望能在应用中引入SSRS的报表,比如JSP或者PHP页面,这个时候系 ...