什么是同步?什么是异步?阻塞和非阻塞又有什么区别?本文先从 Unix 的 I/O 模型讲起,介绍了5种常见的 I/O 模型。而后再引出 Java 的 I/O 模型的演进过程,并用实例说明如何选择合适的 Java I/O 模型来提高系统的并发量和可用性。

由于,Java 的 I/O 依赖于操作系统的实现,所以先了解 Unix 的 I/O 模型有助于理解 Java 的 I/O。

相关概念

同步和异步

描述的是用户线程与内核的交互方式:

  • 同步是指用户线程发起 I/O 请求后需要等待或者轮询内核 I/O 操作完成后才能继续执行;
  • 异步是指用户线程发起 I/O 请求后仍继续执行,当内核 I/O 操作完成后会通知用户线程,或者调用用户线程注册的回调函数。

阻塞和非阻塞

描述的是用户线程调用内核 I/O 操作的方式:

  • 阻塞是指 I/O 操作需要彻底完成后才返回到用户空间;
  • 非阻塞是指 I/O 操作被调用后立即返回给用户一个状态值,无需等到 I/O 操作彻底完成。

一个 I/O 操作其实分成了两个步骤:发起 I/O 请求和实际的 I/O 操作。 阻塞 I/O 和非阻塞 I/O 的区别在于第一步,发起 I/O 请求是否会被阻塞,如果阻塞直到完成那么就是传统的阻塞 I/O ,如果不阻塞,那么就是非阻塞 I/O 。 同步 I/O 和异步 I/O 的区别就在于第二个步骤是否阻塞,如果实际的 I/O 读写阻塞请求进程,那么就是同步 I/O 。

Unix I/O 模型

Unix 下共有五种 I/O 模型:

  1. 阻塞 I/O
  2. 非阻塞 I/O
  3. I/O 多路复用(select 和 poll)
  4. 信号驱动 I/O(SIGIO)
  5. 异步 I/O(Posix.1 的 aio_ 系列函数)

阻塞 I/O

请求无法立即完成则保持阻塞。

  • 阶段1:等待数据就绪。网络 I/O 的情况就是等待远端数据陆续抵达;磁盘I/O的情况就是等待磁盘数据从磁盘上读取到内核态内存中。
  • 阶段2:数据拷贝。出于系统安全,用户态的程序没有权限直接读取内核态内存,因此内核负责把内核态内存中的数据拷贝一份到用户态内存中。

非阻塞 I/O

  • socket 设置为 NONBLOCK(非阻塞)就是告诉内核,当所请求的 I/O 操作无法完成时,不要将进程睡眠,而是返回一个错误码(EWOULDBLOCK) ,这样请求就不会阻塞
  • I/O 操作函数将不断的测试数据是否已经准备好,如果没有准备好,继续测试,直到数据准备好为止。整个 I/O 请求的过程中,虽然用户线程每次发起 I/O 请求后可以立即返回,但是为了等到数据,仍需要不断地轮询、重复请求,消耗了大量的 CPU 的资源
  • 数据准备好了,从内核拷贝到用户空间。

一般很少直接使用这种模型,而是在其他 I/O 模型中使用非阻塞 I/O 这一特性。这种方式对单个 I/O 请求意义不大,但给 I/O 多路复用铺平了道路.

I/O 多路复用(异步阻塞 I/O)

I/O 多路复用会用到 select 或者 poll 函数,这两个函数也会使进程阻塞,但是和阻塞 I/O 所不同的的,这两个函数可以同时阻塞多个 I/O 操作。而且可以同时对多个读操作,多个写操作的 I/O 函数进行检测,直到有数据可读或可写时,才真正调用 I/O 操作函数。

从流程上来看,使用 select 函数进行 I/O 请求和同步阻塞模型没有太大的区别,甚至还多了添加监视 socket,以及调用 select 函数的额外操作,效率更差。但是,使用 select 以后最大的优势是用户可以在一个线程内同时处理多个 socket 的 I/O 请求。用户可以注册多个 socket,然后不断地调用 select 读取被激活的 socket,即可达到在同一个线程内同时处理多个 I/O 请求的目的。而在同步阻塞模型中,必须通过多线程的方式才能达到这个目的。

I/O 多路复用模型使用了 Reactor 设计模式实现了这一机制。

调用 select / poll 该方法由一个用户态线程负责轮询多个 socket,直到某个阶段1的数据就绪,再通知实际的用户线程执行阶段2的拷贝。 通过一个专职的用户态线程执行非阻塞I/O轮询,模拟实现了阶段一的异步化

信号驱动 I/O(SIGIO)

首先我们允许 socket 进行信号驱动 I/O,并安装一个信号处理函数,进程继续运行并不阻塞。当数据准备好时,进程会收到一个 SIGIO 信号,可以在信号处理函数中调用 I/O 操作函数处理数据。

异步 I/O

调用 aio_read 函数,告诉内核描述字,缓冲区指针,缓冲区大小,文件偏移以及通知的方式,然后立即返回。当内核将数据拷贝到缓冲区后,再通知应用程序。

异步 I/O 模型使用了 Proactor 设计模式实现了这一机制。

告知内核,当整个过程(包括阶段1和阶段2)全部完成时,通知应用程序来读数据.

几种 I/O 模型的比较

前四种模型的区别是阶段1不相同,阶段2基本相同,都是将数据从内核拷贝到调用者的缓冲区。而异步 I/O 的两个阶段都不同于前四个模型。

同步 I/O 操作引起请求进程阻塞,直到 I/O 操作完成。异步 I/O 操作不引起请求进程阻塞。

常见 Java I/O 模型

在了解了 UNIX 的 I/O 模型之后,其实 Java 的 I/O 模型也是类似。

“阻塞I/O”模式

在上一节 Socket 章节中的 EchoServer 就是一个简单的阻塞 I/O 例子,服务器启动后,等待客户端连接。在客户端连接服务器后,服务器就阻塞读写取数据流。

EchoServer 代码:

  1. public class EchoServer {
  2. public static int DEFAULT_PORT = 7;
  3.  
  4. public static void main(String[] args) throws IOException {
  5.  
  6. int port;
  7.  
  8. try {
  9. port = Integer.parseInt(args[0]);
  10. } catch (RuntimeException ex) {
  11. port = DEFAULT_PORT;
  12. }
  13.  
  14. try (
  15. ServerSocket serverSocket =
  16. new ServerSocket(port);
  17. Socket clientSocket = serverSocket.accept();
  18. PrintWriter out =
  19. new PrintWriter(clientSocket.getOutputStream(), true);
  20. BufferedReader in = new BufferedReader(
  21. new InputStreamReader(clientSocket.getInputStream()));
  22. ) {
  23. String inputLine;
  24. while ((inputLine = in.readLine()) != null) {
  25. out.println(inputLine);
  26. }
  27. } catch (IOException e) {
  28. System.out.println("Exception caught when trying to listen on port "
  29. + port + " or listening for a connection");
  30. System.out.println(e.getMessage());
  31. }
  32. }
  33. }

改进为“阻塞I/O+多线程”模式

使用多线程来支持多个客户端来访问服务器。

主线程 MultiThreadEchoServer.java

  1. public class MultiThreadEchoServer {
  2. public static int DEFAULT_PORT = 7;
  3.  
  4. public static void main(String[] args) throws IOException {
  5.  
  6. int port;
  7.  
  8. try {
  9. port = Integer.parseInt(args[0]);
  10. } catch (RuntimeException ex) {
  11. port = DEFAULT_PORT;
  12. }
  13. Socket clientSocket = null;
  14. try (ServerSocket serverSocket = new ServerSocket(port);) {
  15. while (true) {
  16. clientSocket = serverSocket.accept();
  17.  
  18. // MultiThread
  19. new Thread(new EchoServerHandler(clientSocket)).start();
  20. }
  21. } catch (IOException e) {
  22. System.out.println(
  23. "Exception caught when trying to listen on port " + port + " or listening for a connection");
  24. System.out.println(e.getMessage());
  25. }
  26. }
  27. }

处理器类 EchoServerHandler.java

  1. public class EchoServerHandler implements Runnable {
  2. private Socket clientSocket;
  3.  
  4. public EchoServerHandler(Socket clientSocket) {
  5. this.clientSocket = clientSocket;
  6. }
  7.  
  8. @Override
  9. public void run() {
  10. try (PrintWriter out = new PrintWriter(clientSocket.getOutputStream(), true);
  11. BufferedReader in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));) {
  12.  
  13. String inputLine;
  14. while ((inputLine = in.readLine()) != null) {
  15. out.println(inputLine);
  16. }
  17. } catch (IOException e) {
  18. System.out.println(e.getMessage());
  19. }
  20. }
  21. }

存在问题:每次接收到新的连接都要新建一个线程,处理完成后销毁线程,代价大。当有大量地短连接出现时,性能比较低。

改进为“阻塞I/O+线程池”模式

针对上面多线程的模型中,出现的线程重复创建、销毁带来的开销,可以采用线程池来优化。每次接收到新连接后从池中取一个空闲线程进行处理,处理完成后再放回池中,重用线程避免了频率地创建和销毁线程带来的开销。

主线程 ThreadPoolEchoServer.java

  1. public class ThreadPoolEchoServer {
  2. public static int DEFAULT_PORT = 7;
  3.  
  4. public static void main(String[] args) throws IOException {
  5.  
  6. int port;
  7.  
  8. try {
  9. port = Integer.parseInt(args[0]);
  10. } catch (RuntimeException ex) {
  11. port = DEFAULT_PORT;
  12. }
  13. ExecutorService threadPool = Executors.newFixedThreadPool(5);
  14. Socket clientSocket = null;
  15. try (ServerSocket serverSocket = new ServerSocket(port);) {
  16. while (true) {
  17. clientSocket = serverSocket.accept();
  18.  
  19. // Thread Pool
  20. threadPool.submit(new Thread(new EchoServerHandler(clientSocket)));
  21. }
  22. } catch (IOException e) {
  23. System.out.println(
  24. "Exception caught when trying to listen on port " + port + " or listening for a connection");
  25. System.out.println(e.getMessage());
  26. }
  27. }
  28. }

存在问题:在大量短连接的场景中性能会有提升,因为不用每次都创建和销毁线程,而是重用连接池中的线程。但在大量长连接的场景中,因为线程被连接长期占用,不需要频繁地创建和销毁线程,因而没有什么优势。

改进为“非阻塞I/O”模式

“阻塞I/O+线程池”网络模型虽然比”阻塞I/O+多线程”网络模型在性能方面有提升,但这两种模型都存在一个共同的问题:读和写操作都是同步阻塞的,面对大并发(持续大量连接同时请求)的场景,需要消耗大量的线程来维持连接。CPU 在大量的线程之间频繁切换,性能损耗很大。一旦单机的连接超过1万,甚至达到几万的时候,服务器的性能会急剧下降。

而 NIO 的 Selector 却很好地解决了这个问题,用主线程(一个线程或者是 CPU 个数的线程)保持住所有的连接,管理和读取客户端连接的数据,将读取的数据交给后面的线程池处理,线程池处理完业务逻辑后,将结果交给主线程发送响应给客户端,少量的线程就可以处理大量连接的请求。

Java NIO 由以下几个核心部分组成:

  • Channel
  • Buffer
  • Selector

要使用 Selector,得向 Selector 注册 Channel,然后调用它的 select()方法。这个方法会一直阻塞到某个注册的通道有事件就绪。一旦这个方法返回,线程就可以处理这些事件,事件的例子有如新连接进来,数据接收等。

主线程 NonBlokingEchoServer.java

  1. public class NonBlokingEchoServer {
  2. public static int DEFAULT_PORT = 7;
  3.  
  4. public static void main(String[] args) throws IOException {
  5.  
  6. int port;
  7.  
  8. try {
  9. port = Integer.parseInt(args[0]);
  10. } catch (RuntimeException ex) {
  11. port = DEFAULT_PORT;
  12. }
  13. System.out.println("Listening for connections on port " + port);
  14.  
  15. ServerSocketChannel serverChannel;
  16. Selector selector;
  17. try {
  18. serverChannel = ServerSocketChannel.open();
  19. InetSocketAddress address = new InetSocketAddress(port);
  20. serverChannel.bind(address);
  21. serverChannel.configureBlocking(false);
  22. selector = Selector.open();
  23. serverChannel.register(selector, SelectionKey.OP_ACCEPT);
  24. } catch (IOException ex) {
  25. ex.printStackTrace();
  26. return;
  27. }
  28.  
  29. while (true) {
  30. try {
  31. selector.select();
  32. } catch (IOException ex) {
  33. ex.printStackTrace();
  34. break;
  35. }
  36. Set<SelectionKey> readyKeys = selector.selectedKeys();
  37. Iterator<SelectionKey> iterator = readyKeys.iterator();
  38. while (iterator.hasNext()) {
  39. SelectionKey key = iterator.next();
  40. iterator.remove();
  41. try {
  42. if (key.isAcceptable()) {
  43. ServerSocketChannel server = (ServerSocketChannel) key.channel();
  44. SocketChannel client = server.accept();
  45. System.out.println("Accepted connection from " + client);
  46. client.configureBlocking(false);
  47. SelectionKey clientKey = client.register(selector,
  48. SelectionKey.OP_WRITE | SelectionKey.OP_READ);
  49. ByteBuffer buffer = ByteBuffer.allocate(100);
  50. clientKey.attach(buffer);
  51. }
  52. if (key.isReadable()) {
  53. SocketChannel client = (SocketChannel) key.channel();
  54. ByteBuffer output = (ByteBuffer) key.attachment();
  55. client.read(output);
  56. }
  57. if (key.isWritable()) {
  58. SocketChannel client = (SocketChannel) key.channel();
  59. ByteBuffer output = (ByteBuffer) key.attachment();
  60. output.flip();
  61. client.write(output);
  62.  
  63. output.compact();
  64. }
  65. } catch (IOException ex) {
  66. key.cancel();
  67. try {
  68. key.channel().close();
  69. } catch (IOException cex) {
  70. }
  71. }
  72. }
  73. }
  74.  
  75. }
  76. }

改进为“异步I/O”模式

Java SE 7 版本之后,引入了异步 I/O (NIO.2) 的支持,为构建高性能的网络应用提供了一个利器。

主线程 AsyncEchoServer.java

  1. public class AsyncEchoServer {
  2.  
  3. public static int DEFAULT_PORT = 7;
  4.  
  5. public static void main(String[] args) throws IOException {
  6. int port;
  7.  
  8. try {
  9. port = Integer.parseInt(args[0]);
  10. } catch (RuntimeException ex) {
  11. port = DEFAULT_PORT;
  12. }
  13.  
  14. ExecutorService taskExecutor = Executors.newCachedThreadPool(Executors.defaultThreadFactory());
  15. // create asynchronous server socket channel bound to the default group
  16. try (AsynchronousServerSocketChannel asynchronousServerSocketChannel = AsynchronousServerSocketChannel.open()) {
  17. if (asynchronousServerSocketChannel.isOpen()) {
  18. // set some options
  19. asynchronousServerSocketChannel.setOption(StandardSocketOptions.SO_RCVBUF, 4 * 1024);
  20. asynchronousServerSocketChannel.setOption(StandardSocketOptions.SO_REUSEADDR, true);
  21. // bind the server socket channel to local address
  22. asynchronousServerSocketChannel.bind(new InetSocketAddress(port));
  23. // display a waiting message while ... waiting clients
  24. System.out.println("Waiting for connections ...");
  25. while (true) {
  26. Future<AsynchronousSocketChannel> asynchronousSocketChannelFuture = asynchronousServerSocketChannel
  27. .accept();
  28. try {
  29. final AsynchronousSocketChannel asynchronousSocketChannel = asynchronousSocketChannelFuture
  30. .get();
  31. Callable<String> worker = new Callable<String>() {
  32. @Override
  33. public String call() throws Exception {
  34. String host = asynchronousSocketChannel.getRemoteAddress().toString();
  35. System.out.println("Incoming connection from: " + host);
  36. final ByteBuffer buffer = ByteBuffer.allocateDirect(1024);
  37. // transmitting data
  38. while (asynchronousSocketChannel.read(buffer).get() != -1) {
  39. buffer.flip();
  40. asynchronousSocketChannel.write(buffer).get();
  41. if (buffer.hasRemaining()) {
  42. buffer.compact();
  43. } else {
  44. buffer.clear();
  45. }
  46. }
  47. asynchronousSocketChannel.close();
  48. System.out.println(host + " was successfully served!");
  49. return host;
  50. }
  51. };
  52. taskExecutor.submit(worker);
  53. } catch (InterruptedException | ExecutionException ex) {
  54. System.err.println(ex);
  55. System.err.println("\n Server is shutting down ...");
  56. // this will make the executor accept no new threads
  57. // and finish all existing threads in the queue
  58. taskExecutor.shutdown();
  59. // wait until all threads are finished
  60. while (!taskExecutor.isTerminated()) {
  61. }
  62. break;
  63. }
  64. }
  65. } else {
  66. System.out.println("The asynchronous server-socket channel cannot be opened!");
  67. }
  68. } catch (IOException ex) {
  69. System.err.println(ex);
  70. }
  71. }
  72. }

源码

本章例子的源码,可以在 https://github.com/waylau/essential-java 中 com.waylau.essentialjava.net.echo 包下找到。

参考引用

Java I/O 模型的演进的更多相关文章

  1. Java进阶(五)Java I/O模型从BIO到NIO和Reactor模式

    原创文章,同步发自作者个人博客,http://www.jasongj.com/java/nio_reactor/ Java I/O模型 同步 vs. 异步 同步I/O 每个请求必须逐个地被处理,一个请 ...

  2. java线程内存模型,线程、工作内存、主内存

    转自:http://rainyear.iteye.com/blog/1734311 java线程内存模型 线程.工作内存.主内存三者之间的交互关系图: key edeas 所有线程共享主内存 每个线程 ...

  3. Java虚拟机内存模型及垃圾回收监控调优

    Java虚拟机内存模型及垃圾回收监控调优 如果你想理解Java垃圾回收如果工作,那么理解JVM的内存模型就显的非常重要.今天我们就来看看JVM内存的各不同部分及如果监控和实现垃圾回收调优. JVM内存 ...

  4. java String 内存模型

    关于java的内存模型,参照以下的一篇文章: https://isudox.com/2016/06/22/memory-model-of-string-in-java-language/

  5. Java虚拟机--内存模型与线程

    Java虚拟机--内存模型与线程 高速缓存:处理器要与内存交互,如读取.存储运算结果,而计算机的存储设备和处理器的运算速度差异巨大,所以加入一层读写速度和处理器接近的高速缓存来作为内存和处理器之间的缓 ...

  6. Java网络编程和NIO详解3:IO模型与Java网络编程模型

    Java网络编程和NIO详解3:IO模型与Java网络编程模型 基本概念说明 用户空间与内核空间 现在操作系统都是采用虚拟存储器,那么对32位操作系统而言,它的寻址空间(虚拟存储空间)为4G(2的32 ...

  7. OSGI 面向Java的动态模型系统

    OSGI (面向Java的动态模型系统) OSGi(Open Service Gateway Initiative)技术是Java动态化模块化系统的一系列规范.OSGi一方面指维护OSGi规范的OSG ...

  8. Java I/O模型从BIO到NIO和Reactor模式(转)

    原创文章,转载请务必将下面这段话置于文章开头处(保留超链接).本文转发自技术世界,原文链接 http://www.jasongj.com/java/nio_reactor/ Java I/O模型 同步 ...

  9. 掌握Java的内存模型,你就是解决并发问题最靓的仔

    摘要:如果编写的并发程序出现问题时,很难通过调试来解决相应的问题,此时,需要一行行的检查代码,这个时候,如果充分理解并掌握了Java的内存模型,你就能够很快分析并定位出问题所在. 本文分享自华为云社区 ...

随机推荐

  1. 【转】YUV格式&像素

    一幅彩色图像的基本要素是什么? 说白了,一幅图像包括的基本东西就是二进制数据,其容量大小实质即为二进制数据的多少.一幅1920x1080像素的YUV422的图像,大小是1920X1080X2=4147 ...

  2. css与 js动画 优缺点比较

    我们经常面临一个抉择:到底使用JavaScript还是CSS动画,下面做一下对比 JS动画 缺点:(1)JavaScript在浏览器的主线程中运行,而主线程中还有其它需要运行的JavaScript脚本 ...

  3. HTML--控制小人自由移动

    <!DOCTYPE html> <html> <head> <meta http-equiv="Content-Type" content ...

  4. .NET基础拾遗(4)委托和事件1

    一.委托初窥:一个拥有方法的对象 (1)本质:持有一个或多个方法的对象:委托和典型的对象不同,执行委托实际上是执行它所“持有”的方法. (2)如何使用委托? ①声明委托类型(delegate关键字) ...

  5. CodeSmith使用总结--读取一个表试试

    我感觉CodeSmith对于我的最大用途是不用我手动创建每个表的Model.BLL和DAL了,那些繁琐的工作真的让我很无语. CodeSmith要读取数据库中的表就要先连接数据库. 新建一个数据库连接 ...

  6. SpinLock(自旋锁)

    SpinLock(自旋锁) SpinLock 结构是一个低级别的互斥同步基元,它在等待获取锁时进行旋转. 在多核计算机上,当等待时间预计较短且极少出现争用情况时,SpinLock 的性能将高于其他类型 ...

  7. sql 根据时间获取数据

    获取当月数据 MONTH(时间字段)=MONTH(GETDATE()) and year(时间字段)=year(GETDATE()) 计算两个时间差了多少分钟 DATEDIFF(mi,'7:00',c ...

  8. asp.net 正则表达式

    在平时的开发中常常用到替换法:  普通的字符串替换可以使用string.replace(string,string),但是比较难解决HTML文本的替换. 经我几番查找,终也找出解决办法:正则匹配替换. ...

  9. SQLServer 2008 R2 发布订阅配置指南

    原以为配置SQLServer 2008 R2的发布订阅很简单,实际配置后才发现过程中有问题地方一直都没搞明白,最后经过几天的查找问题和实践,终于搞定了.现将过程记录如下. SQLServer 2008 ...

  10. linux服务器伪分布模式安装hadoop-1.1.2

    1:环境准备    1台linux服务器, hadoop安装包(apache官方网下载) jdk1.6+   2:安装jdk ,配置好环境变量(etc/profile),java -version 测 ...