java IO NIO(2) AIO漫谈 weir 2016-06-14 08:55:46.0 java,IO,NIO,AIO 2824 今天来聊聊IO操作,关于这个话题我一上来就打算从操作系统对磁盘的读写原理开始,IO是什么?把它打回原形说就是CPU和磁盘之间的数据交互,我不能再通俗了CPU和磁盘你应该知道吧。对于高性能来说磁盘可谓是无法超越的难题CPU的发展我们有目共睹,然而磁盘的发展虽然SSD(固态硬盘)的发展速度也挺快的,但是目前价格仍然很贵而且SSD的寿命也是问题,大家都知道机械硬盘的优势就在于存储数据的寿命长。 IO在CPU和磁盘之间起的作用就可想而知了,交互速度越快那不就是性能越高了。计算怎么工作的底层怎么实现的我就不说了因为我也说不清楚(大哭),但是我可以通俗的解释一下,我们都知道计算机的之间传递的都是电流(正负),换成数子0 1 还有中间怎么转换呀二极管呀这些啊啊啊想死,你自己去了解吧求你们了!说到这里你就会有疑问了IO不只是CPU和磁盘吧,那是当然。好像给我们现在说的java io 也扯不上关系吧,我为什么要说这些其实是看了网上别人对nio aio的底层分析才想出来的,有必要从最底层聊聊一步一步聊到上层。 如果我一开始就给你说阻塞非阻塞、同步异步那多没劲,如果大家从计算机的整体出发了解计算机底层和计算机各个组件之间是怎么通讯打交道的岂不是站的角度和层次会更加清晰。虽然这些工作都被我们的科学家实现了而我们只需要在此基础上做更高层的操作就行了,但是基础的力量就显示出来了。 什么是阻塞?你从课本上学,所有的套接字都是阻塞的,什么意思,套接字是什么?recvfrom是什么?这些也是系统底层的封装,还是回到计算机的工作原理怎么把电信号转换成0 1数字的,对不对,这个过程我们的科学家做了什么封装才把这个过程实现的而不需要我们去关心我们只知道通了电电脑就可以工作了。操作系统是什么东西?他为什么可以管理好各个硬件还提供给我们软件使用,我给你说实话这些东西我相信专业知识都会学,可我当时就没有追究这些问题根源的兴趣,这大概就是天才和普通人的差别。 什么是阻塞?我还是问这个问题。我不知道你会给我什么回答,我想说的是我们现在讨论的一切问题都是在操作系统之上讨论的,换句话说,操作系统封装了硬件之间的交互并提供给我们和底层硬件交互的接口,我们才能在操作系统基础上面讨论我们的问题。聊到现在发现我们说的阻塞原来是操作系统提供给我们的接口,那就又有的说了,原来我们说的阻塞呀非阻塞呀同步呀异步呀都是操作系统在搞鬼,如果操作系统没有定义这些名词什么的没有提供这些接口的接口只有一个的话我们还讨论什么,操作系统就像是中间人一样阻碍着我们和硬件的直接接触。这就是我在上一篇文章中说的分而治之的世界现在的游戏规则,社会越发达精细化程度越高,这其实也从另一个侧面反映了人不是万能的,人在整个宇宙中的渺小。 从阻塞到非阻塞,从同步到异步这是我们对性能追求的结果。这个世界哪里会有上帝,谁会有那么超前的智慧,人类进化了多少年还不是昙花一现的烟火。从科学进步的历史来看,儒家思想和道家思想统统都扼杀了中华民族追求科学的好奇心,在事实面前我们真的要谦虚卧薪尝胆的好好学习现代科学。 我现在还记得我当时上学的时候是怎么学习java基础的,我们的老师是加拿大回国的教授,当时他让我们在windows系统里面安装的模拟unix系统cygnus 这个东西,然后就开始了java的基础学习。说实话但是学习的基础还真是马马虎虎,工作之后第一次面试连java的集合里面的一些特性都说错了就就模模糊糊的概念,这不导致现在还是一遍一遍的学习基础,这是惭愧之极。 这是我在书中截的图,大家会发现到I/O复用前三个没有‘通知’这两个字。 再看看上面的图只有最后一个异步IO没有出现‘阻塞’这两个字。 大家谈到java的IO都知道有这么一个发展过程IO,NIO, NIO2。 最初的IO就这些当然也没有穷尽,当然还有file和socket,这些都属于IO的范畴,还可以根据数据的来源不同划分,通过网络传输的和本地IO传输的,网上大多文章分析javaIO都是从字节流和字符流开始的,说字节流也是从操作系统的角度分析的,知道后来的NIO出现其实也是流,只不过流被封装了不直接面对流了,出现通道的概念。 这是io读取的源码,这些代码不难理解,其中int c = read();中read()返回一个int,为什么? 从输入流中读取数据的下一个字节。返回0到255范围内的int字节值。如果因为已经到达流末尾而没有可用的字节,则返回值-1。在输入数据可用、检测到流末尾或者抛出异常前,此方法一直阻塞。 这段话是jdk1.6的中文翻译我想大家都看到过,我想说的是任何语言逃不出硬件底层的实现,我们的计算机只能理解0 1 (正 负),所有我们看到的东西都是被包装过的,显然“字节”也是被包装过的,这就是操作系统所能给我们提供的最大的支持了,换句话说操作系统只能一个字节一个字节的向外提供被各种编程语言使用,我不知道这话严不严谨但是我们暂且这么理解,再看字符流: 发现他直接就是char cbuf[],也就是说我们是可以把字节再打包一下的,这里就出现了缓冲区的概念,不知道大家看到下面的图是什么感觉,这个图反映的是字节流还是字符流呢? 这个图我是从网上看到的,我不知道别人是怎么理解,看解释我也看懂了,但是有一点不明白图中read()让人感到困惑read()是java中的么?所以我加上了java中的read(),这样才直观的概述大家两个read()的作用和出处。内核空间是操作系统的,数据从磁盘到内核空间我们很容易理解,它通过磁盘控制器由DMA完成数据的转移,那数据是怎么又跑到用户空间的呢?网上的解释是进程发出的read()命令将内核空间的缓冲数据转移到用户空间的缓冲区的,然后进程就通知更上层的java去读数据。这样解释虽然没问题但是这两个图有问题,我之前说前一个图不完整其实不然,图本身没问题只是我们理解的有问题,要想理解这个问题首先要理解什么是内核空间什么是用户空间,当我去查资料了解这些问题时我发现了一个很重要的问题,我发现所有关于java高性能高并发的问题底层都或多或少跟linux(unix)有关系,那你说跟windows底层有没有关那是当然的,只不过大多数服务器的系统都是linux。 那也就是说用户空间或进程其实就是java虚拟机,因为java虚拟机就是单进程多线程的,所以说前一个图没问题,如果直接将用户空间换成java虚拟机那就直接多了。接着再看一个图: 网上人们的解释是文件内存映射,想弄明白这个问题还要首先明白什么是虚拟内存映射这个概念,所以说对操作系统的认知直接影响对高级语言的认知,有时候真的是恨自己当初为什么没有好好学习这些知识。大家还要去理解一个进程是怎么分配内存的,网上是这么说的每个进程可以分配4G内存,凭什么为什么是4G内存空间?所以说不敢想你只要多想一点点那问题就是一个接着一个的出来,到最后你会问我为什么在这儿?天哪杀了我吧!所以呀即便是每个进程可以分配4G的内存空间实际上也不会这么做的因为没有人那么傻。嘿嘿虚拟内存空间的概念就有了,一旦有了虚拟的东西那就少不了的是虚拟与实际的映射关系,就是怎么把虚拟的转换成真实的呀。 那对于我们的javaNIO来说似乎简单多了: 但是底层的封装就麻烦多了,你想呀首先拿到的是虚拟的地址,这个时候是没有IO交互的,然后通过这个虚拟的地址转换成真实的地址,然后开始IO交互或者放到一个缓冲区或者放在内存中,网上有这样一个例子: package com.weir.io; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.nio.MappedByteBuffer; import java.nio.channels.FileChannel; public class FileCopyTest { public static void main(String[] args) throws Exception { String sourcePath = "e:/lauvan.log"; String destPath1 = "e:/lauvan1.log"; String destPath2 = "e:/lauvan2.log"; String destPath3 = "e:/lauvan3.log"; long t1 = System.currentTimeMillis(); traditionalCopy(sourcePath, destPath1); long t2 = System.currentTimeMillis(); System.out.println("传统IO方法实现文件拷贝耗时:" + (t2 - t1) + "ms"); nioCopy(sourcePath, destPath2); long t3 = System.currentTimeMillis(); System.out.println("利用NIO文件通道方法实现文件拷贝耗时:" + (t3 - t2) + "ms"); nioCopy2(sourcePath, destPath3); long t4 = System.currentTimeMillis(); System.out.println("利用NIO文件内存映射及文件通道实现文件拷贝耗时:" + (t4 - t3) + "ms"); } private static void nioCopy2(String sourcePath, String destPath) throws Exception { File source = new File(sourcePath); File dest = new File(destPath); if (!dest.exists()) { dest.createNewFile(); } FileInputStream fis = new FileInputStream(source); FileOutputStream fos = new FileOutputStream(dest); FileChannel sourceCh = fis.getChannel(); FileChannel destCh = fos.getChannel(); MappedByteBuffer mbb = sourceCh.map(FileChannel.MapMode.READ_ONLY, 0, sourceCh.size()); destCh.write(mbb); sourceCh.close(); destCh.close(); } private static void traditionalCopy(String sourcePath, String destPath) throws Exception { File source = new File(sourcePath); File dest = new File(destPath); if (!dest.exists()) { dest.createNewFile(); } FileInputStream fis = new FileInputStream(source); FileOutputStream fos = new FileOutputStream(dest); byte[] buf = new byte[512]; int len = 0; while ((len = fis.read(buf)) != -1) { fos.write(buf, 0, len); } fis.close(); fos.close(); } private static void nioCopy(String sourcePath, String destPath) throws Exception { File source = new File(sourcePath); File dest = new File(destPath); if (!dest.exists()) { dest.createNewFile(); } FileInputStream fis = new FileInputStream(source); FileOutputStream fos = new FileOutputStream(dest); FileChannel sourceCh = fis.getChannel(); FileChannel destCh = fos.getChannel(); destCh.transferFrom(sourceCh, 0, sourceCh.size()); sourceCh.close(); destCh.close(); } } 输出: 传统IO方法实现文件拷贝耗时:126ms 利用NIO文件通道方法实现文件拷贝耗时:21ms 利用NIO文件内存映射及文件通道实现文件拷贝耗时:7ms 单纯的看这个文件拷贝的例子来说也能说明一些问题。 我不知道大家有没有玩过linux,或者在linux上面拷贝文件,那速度比java程序高多了。 javaNIO的通道、缓冲区、选择器这些概念理解起来确实比较难: 有人也用这样的图来简化表达他们之间的关系,有人也用linuxIO的多路复用什么的概念来帮助理解javaNIO,说实话我到现在还是懵懵懂懂的。可能也正是因为javaNIO的难以理解和诸多问题才使得netty等第三方的框架出来,但是如果很好的使用第三方的框架还是要学习javaNIO,所以说javaNIO还需要提供更加友好简单的调用方式。 还要说明一点,现代的linux系统已实现了缓存IO,直接IO和内存映射,对应到javaIO,缓存IO和内存映射就是javaNIO,至于至于直接IO是不是java中的字节流式的IO我还不敢下结论。但有一点是可以确定的就是现代操作系统所能提供的java里面基本都实现了。所以java也成为了高性能语言的最佳代言。