文档章节

IO的阻塞与非阻塞、同步与异步以及Java网络IO交互方式

Zero零_度
 Zero零_度
发布于 2016/02/22 11:50
字数 3630
阅读 106
收藏 10

最近工作中,接触到了Java网络编程方面的东西:Socket、NIO、MongoDB等,也看了tomcat的源码,也加强了线程方面的知识,也使用了MINA这样的框架。感觉获益良多,原本技术上的薄弱环节也在慢慢提高,很多想写的东西,也在慢慢规划整理。无奈最近在筹备婚礼的事情,显得有些耽搁。

  想了很久,决定先写写IO中经常被提到的概念——“同步与异步、阻塞与非阻塞”以及在Java网络编程中的简单运用。

  想达到的目的有两个:

  1。深入的理解同步与异步、阻塞与非阻塞,这看似烂大街的词汇很多人已经习惯不停的说,但却说不出其中的所以然,包括我。

  2。理解各种IO模型在Java网络IO中的运用,能够根据不同的应用场景选择合适的交互方式。了解不同的交互方式对IO性能的影响。

前提

  首先先强调上下文:下面提到了同步与异步、阻塞与非阻塞的概念都是在IO的场合下。它们在其它场合下有着不同的含义,比如操作系统中,通信技术上。

  然后借鉴下《Unix网络编程卷》中的理论:

  IO操作中涉及的2个主要对象为程序进程、系统内核。以读操作为例,当一个IO读操作发生时,通常经历两个步骤:

  1,等待数据准备

  2,将数据从系统内核拷贝到操作进程中

  例如,在socket上的读操作,步骤1会等到网络数据包到达,到达后会拷贝到系统内核的缓冲区;步骤2会将数据包从内核缓冲区拷贝到程序进程的缓冲区中。

阻塞(blocking)与非阻塞(non-blocking)IO

  IO的阻塞、非阻塞主要表现在一个IO操作过程中,如果有些操作很慢,比如读操作时需要准备数据,那么当前IO进程是否等待操作完成,还是得知暂时不能操作后先去做别的事情?一直等待下去,什么事也不做直到完成,这就是阻塞。抽空做些别的事情,这是非阻塞。

  非阻塞IO会在发出IO请求后立即得到回应,即使数据包没有准备好,也会返回一个错误标识,使得操作进程不会阻塞在那里。操作进程会通过多次请求的方式直到数据准备好,返回成功的标识。

  想象一下下面两种场景:

  A 小明和小刚两个人都很耿直内向,一天小明来找小刚借书:“小刚啊,你那本XXX借我看看”。 于是小刚就去找书,小明就等着,找了半天找到了,把书给了小明。

  B 小明和小刚两个人都很活泼外向,一天小明来找小刚借书:“嘿小刚,你那本XXX借我看看”。 小刚说:“我得找一会”,小明就去打球去了。过会又来,这次书找到了,把书给了小明。

  结论:A是阻塞的,B是非阻塞的。

  从CPU角度可以看出非阻塞明显提高了CPU的利用率,进程不会一直在那等待。但是同样也带来了线程切换的增加。增加的 CPU 使用时间能不能补偿系统的切换成本需要好好评估。

同步(synchronous)与异步(asynchronous)IO

  先来看看正式点的定义,POSIX标准将IO模型分为了两种:同步IO和异步IO,Richard Stevens在《Unix网络编程卷》中也总结道:

A synchronous I/O operation causes the requesting process to be blocked until that I/O operation completes;
An asynchronous I/O operation does not cause the requesting process to be blocked;

  可以看出,判断同步和异步的标准在于:一个IO操作直到完成,是否导致程序进程的阻塞。如果阻塞就是同步的,没有阻塞就是异步的。这里的IO操作指的是真实的IO操作,也就是数据从内核拷贝到系统进程(读)的过程。

  继续前面借书的例子,异步借书是这样的:

  C 小明很懒,一天小明来找小刚借书:“嘿小刚,你那本XXX借我看看”。 小刚说:“我得找一会”,小明就出去打球了并且让小刚如果找到了就把书拿给他。小刚是个负责任的人,找到了书送到了小明手上。

  A和B的借书方式都是同步的,有人要问了B不是非阻塞嘛,怎么还是同步?

  前面说了IO操作的2个步骤:准备数据和把数据从内核中拷贝到程序进程。映射到这个例子,书即是准备的数据,小刚是内核,小明是程序进程,小刚把书给小明这是拷贝数据。在B方式中,小刚找书这段时间小明的确是没闲着,该干嘛干嘛,但是小刚找到书把书给小明的这个过程也就是拷贝数据这个步骤,小明还是得乖乖的回来候着小刚把书递手上。所以这里就阻塞了,根据上面的定义,所以是同步。

  在涉及到 IO 处理时通常都会遇到一个是同步还是异步的处理方式的选择问题。同步能够保证程序的可靠性,而异步可以提升程序的性能。小明自己去取书不管等着不等着迟早拿到书,指望小刚找到了送来,万一小刚忘了或者有急事忙别的了,那书就没了。

讨论

  说实话,网上关于同步与异步、阻塞与非阻塞的文章多之又多,大部分是拷贝的,也有些写的非常好的。参考了许多,也借鉴了许多,也经过自己的思考。

  同步与异步、阻塞与非阻塞之间确实有很多相似的地方,很容易混淆。wiki更是把异步与非阻塞画上了等号,更多的人还是认为他们是不同的。原因可能有很多,每个人的知识背景不同,设定的上下文也不同。

  我的看法是:在IO中,根据上面同步异步的概念,也可以看出来同步与异步往往是通过阻塞非阻塞的形式来表达的,并且是通过一种中间处理机制来达到异步的效果。同步与异步往往是IO操作请求者和回应者之间在IO实际操作阶段的协作方式,而阻塞非阻塞更确切的说是一种自身状态,当前进程或者线程的状态。

  在发出IO读请求后,阻塞IO会一直等待有数据可读,当有数据可读时,会等待数据从内核拷贝至系统进程;而非阻塞IO都会立即返回至于数据怎么处理是程序进程自己的事情,无关同步和异步。 

两种方式的组合

  组合的方式当然有四种,分别是:同步阻塞、同步非阻塞、异步阻塞、异步非阻塞。

Java网络IO实现和IO模型

  不同的操作系统上有不同的IO模型,《Unix网络编程卷》将unix上的IO模型分为5类:blocking I/O、nonblocking I/O、I/O multiplexing (select and poll)、signal driven I/O (SIGIO)以及asynchronous I/O (the POSIX aio_functions)。具体可参考Unix网络编程卷1》6.2章节

  在windows上IO模型也是有5种:select 、WSAAsyncSelect、WSAEventSelect、Overlapped I/O 事件通知以及IOCP。具体可参考windows五种IO模型
  Java是平台无关的语言,在不同的平台上会调用底层操作系统的不同的IO实现,下面就来说一下Java提供的网络IO的工具和实现,为了扩大阻塞非阻塞的直观感受,我都使用了长连接。

阻塞IO

  同步阻塞最常用的一种用法,使用也是最简单的,但是 I/O 性能一般很差,CPU 大部分在空闲状态。下面是一个简单的基于TCP的同步阻塞的Socket服务端例子:

复制代码

 1     @Test 2     public void testJIoSocket() throws Exception 3     { 4         ServerSocket serverSocket = new ServerSocket(10002); 5         Socket socket = null; 6         try 7         { 8             while (true) 9             {10                 socket = serverSocket.accept();11                 System.out.println("socket连接:" + socket.getRemoteSocketAddress().toString());12                 BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));13                 while(true)14                 {15                     String readLine = in.readLine();16                     System.out.println("收到消息" + readLine);17                     if("end".equals(readLine))18                     {19                         break;20                     }21                     //客户端断开连接22                     socket.sendUrgentData(0xFF);23                 }24             }25         }26         catch (SocketException se)27         {28             System.out.println("客户端断开连接");29         }30         catch (IOException e)31         {32             e.printStackTrace();33         }34         finally35         {36             System.out.println("socket关闭:" + socket.getRemoteSocketAddress().toString());37             socket.close();38         }39     }

复制代码

  使用SocketTest作为客户端工具进行测试,同时开启2个客户端连接Server端并发送消息,如下图:

  再看下后台的打印

socket连接:/127.0.0.1:54080收到消息hello!收到消息my name is client1

   由于服务器端是单线程的,在第一个连接的客户端阻塞了线程后,第二个客户端必须等待第一个断开后才能连接。当输入“end”字符串断开客户端1,这时候看到后台继续打印:

复制代码

socket连接:/127.0.0.1:54080收到消息hello!收到消息my name is client1收到消息end
socket关闭:/127.0.0.1:54080
socket连接:/127.0.0.1:54091
收到消息hello!
收到消息my name is client2

复制代码

   所有的客户端连接在请求服务端时都会阻塞住,等待前面的完成。即使是使用短连接,数据在写入 OutputStream 或者从 InputStream 读取时都有可能会阻塞。这在大规模的访问量或者系统对性能有要求的时候是不能接受的。

阻塞IO + 每个请求创建线程/线程池

  通常解决这个问题的方法是使用多线程技术,一个客户端一个处理线程,出现阻塞时只是一个线程阻塞而不会影响其它线程工作;为了减少系统线程的开销,采用线程池的办法来减少线程创建和回收的成本,模式如下图:

  

  简单的实现例子如下,使用一个线程(Accptor)接收客户端请求,为每个客户端新建线程进行处理(Processor),线程池的我就不弄了:

复制代码

public class MultithreadJIoSocketTest
{
    @Test    public void testMultithreadJIoSocket() throws Exception
    {
        ServerSocket serverSocket = new ServerSocket(10002);
        Thread thread = new Thread(new Accptor(serverSocket));
        thread.start();
        
        Scanner scanner = new Scanner(System.in);
        scanner.next();
    }    
    public class Accptor implements Runnable
    {        private ServerSocket serverSocket;        
        public Accptor(ServerSocket serverSocket)
        {            this.serverSocket = serverSocket;
        }        public void run()
        {            while (true)
            {
                Socket socket = null;                try
                {
                    socket = serverSocket.accept();                    if(socket != null)
                    {
                        System.out.println("收到了socket:" + socket.getRemoteSocketAddress().toString());
                        Thread thread = new Thread(new Processor(socket));
                        thread.start();
                    }
                }                catch (IOException e)
                {
                    e.printStackTrace();
                }
            }
        }
    }    
    public class Processor implements Runnable
    {        private Socket socket;        
        public Processor(Socket socket)
        {            this.socket = socket;
        }
        
        @Override        public void run()
        {            try
            {
                BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
                String readLine;                while(true)
                {
                    readLine = in.readLine();
                    System.out.println("收到消息" + readLine);                    if("end".equals(readLine))
                    {                        break;
                    }                    //客户端断开连接
                    socket.sendUrgentData(0xFF);
                    Thread.sleep(5000);
                }
            }            catch (InterruptedException e)
            {
               e.printStackTrace();
            }            catch (SocketException se)
            {
                System.out.println("客户端断开连接");
            }            catch (IOException e)
            {
                e.printStackTrace();
            }            finally {                try
                {
                    socket.close();
                }                catch (IOException e)
                {
                    e.printStackTrace();
                }
            }
        }
        
    }
}

复制代码

   使用2个客户端连接,这次没有阻塞,成功的收到了2个客户端的消息。

收到了socket:/127.0.0.1:55707收到了socket:/127.0.0.1:55708收到消息hello!收到消息hello!

   在单个线程处理中,我人为的使单个线程read后阻塞5秒,就像前面说的,出现阻塞也只是在单个线程中,没有影响到另一个客户端的处理。

  这种阻塞IO的解决方案在大部分情况下是适用的,在出现NIO之前是最通常的解决方案,Tomcat里阻塞IO的实现就是这种方式。但是如果是大量的长连接请求呢?不可能创建几百万个线程保持连接。再退一步,就算线程数不是问题,如果这些线程都需要访问服务端的某些竞争资源,势必需要进行同步操作,这本身就是得不偿失的。

非阻塞IO + IO multiplexing

  Java从1.4开始提供了NIO工具包,这是一种不同于传统流IO的新的IO方式,使得Java开始对非阻塞IO支持;NIO并不等同于非阻塞IO,只要设置Blocking属性就可以控制阻塞非阻塞。至于NIO的工作方式特点原理这里一概不说,以后会写。模式如下图:

  

  下面是简单的实现:

复制代码

public class NioNonBlockingSelectorTest
{
    Selector selector;    private ByteBuffer receivebuffer = ByteBuffer.allocate(1024);
    
    @Test    public void testNioNonBlockingSelector()        throws Exception
    {
        selector = Selector.open();
        SocketAddress address = new InetSocketAddress(10002);
        ServerSocketChannel channel = ServerSocketChannel.open();
        channel.socket().bind(address);
        channel.configureBlocking(false);
        channel.register(selector, SelectionKey.OP_ACCEPT);        
        while(true)
        {
            selector.select();
            Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();            while (iterator.hasNext()) {          
                SelectionKey selectionKey = iterator.next();  
                iterator.remove();  
                handleKey(selectionKey);  
            }  
        }
    }    
    private void handleKey(SelectionKey selectionKey) throws IOException
    {
        ServerSocketChannel server = null;
        SocketChannel client = null;        if(selectionKey.isAcceptable())
        {
            server = (ServerSocketChannel)selectionKey.channel();
            client = server.accept();
            System.out.println("客户端: " + client.socket().getRemoteSocketAddress().toString());
            client.configureBlocking(false);
            client.register(selector, SelectionKey.OP_READ);
        }        if(selectionKey.isReadable())
        {
            client = (SocketChannel)selectionKey.channel();
            receivebuffer.clear();  
            int count = client.read(receivebuffer);   
            if (count > 0) {  
                String receiveText = new String( receivebuffer.array(),0,count);  
                System.out.println("服务器端接受客户端数据--:" + receiveText);  
                client.register(selector, SelectionKey.OP_READ);  
            }
        }
    }
    
}

复制代码

  Java NIO提供的非阻塞IO并不是单纯的非阻塞IO模式,而是建立在Reactor模式上的IO复用模型;在IO multiplexing Model中,对于每一个socket,一般都设置成为non-blocking,但是整个用户进程其实是一直被阻塞的。只不过进程是被select这个函数阻塞,而不是被socket IO给阻塞,所以还是属于非阻塞的IO。

  这篇文章中把这种模式归为了异步阻塞,我其实是认为这是同步非阻塞的,可能看的角度不一样。

异步IO

  Java1.7中提供了异步IO的支持,暂时还没有看过,所以以后再讨论。

网络IO优化

  对于网络IO有一些基本的处理规则如下:

  1。减少交互的次数。比如增加缓存,合并请求。

  2。减少传输数据大小。比如压缩后传输、约定合理的数据协议。

  3。减少编码。比如提前将字符转化为字节再传输。

  4。根据应用场景选择合适的交互方式,同步阻塞,同步非阻塞,异步阻塞,异步非阻塞。

就说到这里吧,感觉有点乱,有些地方还是找不到更贴切的语言来描述。


本文转载自:http://www.cnblogs.com/zhuYears/archive/2012/09/28/2690194.html

Zero零_度
粉丝 69
博文 1267
码字总数 263854
作品 0
程序员
私信 提问
IO的阻塞与非阻塞、同步与异步以及Java网络IO交互方式

最近工作中,接触到了Java网络编程方面的东西:Socket、NIO、MongoDB等,也看了tomcat的源码,也加强了线程方面的知识,也使用了MINA这样的框架。感觉获益良多,原本技术上的薄弱环节也在慢慢...

毛爷爷夸我帅
2015/03/27
46
0
漫话:如何给女朋友解释什么是BIO、NIO和AIO?

周末午后,在家里面进行电话面试,我问了面试者几个关于IO的问题,其中包括什么是BIO、NIO和AIO?三者有什么区别?具体如何使用等问题,但是面试者回答的并不是很满意。于是我在面试评价中写...

漫话编程
07/01
240
0
Java核心(五)深入理解BIO、NIO、AIO

导读:本文你将获取到:同/异步 + 阻/非阻塞的性能区别;BIO、NIO、AIO 的区别;理解和实现 NIO 操作 Socket 时的多路复用;同时掌握 IO 最底层最核心的操作技巧。 BIO、NIO、AIO 的区别是什...

王磊的博客
2018/12/03
936
0
Reactor和Proactor模式

在高性能的I/O设计中,有两个比较著名的模式Reactor和Proactor模式,其中Reactor模式用于同步I/O,而Proactor运用于异步I/O操作。 同步和异步 同步和异步是针对应用程序和内核的交互而言的,...

ksfzhaohui
2012/12/14
325
0
Linux IO模型与Java NIO

概述 看Java NIO一篇文章的时候又看到了“异步非阻塞”这个概念,一直处于似懂非懂的状态,想解释下到底什么是异步 什么是非阻塞,感觉抓不住重点。决定仔细研究一下。 本文试图研究以下问题...

yingtju
2018/06/29
0
0

没有更多内容

加载失败,请刷新页面

加载更多

Spring Boot + Mybatis-Plus 集成与使用(二)

前言: 本章节介绍MyBatis-Puls的CRUD使用。在开始之前,先简单讲解下上章节关于Spring Boot是如何自动配置MyBatis-Plus。 一、自动配置 当Spring Boot应用从主方法main()启动后,首先加载S...

伴学编程
昨天
7
0
用最通俗的方法讲spring [一] ──── AOP

@[TOC](用最通俗的方法讲spring [一] ──── AOP) 写这个系列的目的(可以跳过不看) 自己写这个系列的目的,是因为自己是个比较笨的人,我曾一度怀疑自己的智商不适合干编程这个行业.因为在我...

小贼贼子
昨天
7
0
Flutter系列之在 macOS 上安装和配置 Flutter 开发环境

本文为Flutter开发环境在macOS下安装全过程: 一、系统配置要求 想要安装并运行 Flutter,你的开发环境需要最低满足以下要求: 操作系统:macOS(64位) 磁盘空间:700 MB(不包含 IDE 或其余...

過愙
昨天
6
0
OSChina 周六乱弹 —— 早上儿子问我他是怎么来的

Osc乱弹歌单(2019)请戳(这里) 【今日歌曲】 @凉小生 :#今日歌曲推荐# 少点戾气,愿你和这个世界温柔以待。中岛美嘉的单曲《僕が死のうと思ったのは (曾经我也想过一了百了)》 《僕が死の...

小小编辑
昨天
2.7K
16
Excption与Error包结构,OOM 你遇到过哪些情况,SOF 你遇到过哪些情况

Throwable 是 Java 中所有错误与异常的超类,Throwable 包含两个子类,Error 与 Exception 。用于指示发生了异常情况。 Java 抛出的 Throwable 可以分成三种类型。 被检查异常(checked Exc...

Garphy
昨天
42
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部