Java核心技术面试精讲
杨晓峰
前Oracle首席工程师
立即订阅
43250 人已学习
课程目录
已完结 43 讲
0/4登录后,你可以任选4讲全文学习。
开篇词 (1讲)
开篇词 | 以面试题为切入点,有效提升你的Java内功
免费
模块一 Java基础 (14讲)
第1讲 | 谈谈你对Java平台的理解?
第2讲 | Exception和Error有什么区别?
第3讲 | 谈谈final、finally、 finalize有什么不同?
第4讲 | 强引用、软引用、弱引用、幻象引用有什么区别?
第5讲 | String、StringBuffer、StringBuilder有什么区别?
第6讲 | 动态代理是基于什么原理?
第7讲 | int和Integer有什么区别?
第8讲 | 对比Vector、ArrayList、LinkedList有何区别?
第9讲 | 对比Hashtable、HashMap、TreeMap有什么不同?
第10讲 | 如何保证集合是线程安全的? ConcurrentHashMap如何实现高效地线程安全?
第11讲 | Java提供了哪些IO方式? NIO如何实现多路复用?
第12讲 | Java有几种文件拷贝方式?哪一种最高效?
第13讲 | 谈谈接口和抽象类有什么区别?
第14讲 | 谈谈你知道的设计模式?
模块二 Java进阶 (16讲)
第15讲 | synchronized和ReentrantLock有什么区别呢?
第16讲 | synchronized底层如何实现?什么是锁的升级、降级?
第17讲 | 一个线程两次调用start()方法会出现什么情况?
第18讲 | 什么情况下Java程序会产生死锁?如何定位、修复?
第19讲 | Java并发包提供了哪些并发工具类?
第20讲 | 并发包中的ConcurrentLinkedQueue和LinkedBlockingQueue有什么区别?
第21讲 | Java并发类库提供的线程池有哪几种? 分别有什么特点?
第22讲 | AtomicInteger底层实现原理是什么?如何在自己的产品代码中应用CAS操作?
第23讲 | 请介绍类加载过程,什么是双亲委派模型?
第24讲 | 有哪些方法可以在运行时动态生成一个Java类?
第25讲 | 谈谈JVM内存区域的划分,哪些区域可能发生OutOfMemoryError?
第26讲 | 如何监控和诊断JVM堆内和堆外内存使用?
第27讲 | Java常见的垃圾收集器有哪些?
第28讲 | 谈谈你的GC调优思路?
第29讲 | Java内存模型中的happen-before是什么?
第30讲 | Java程序运行在Docker等容器环境有哪些新问题?
模块三 Java安全基础 (2讲)
第31讲 | 你了解Java应用开发中的注入攻击吗?
第32讲 | 如何写出安全的Java代码?
模块四 Java性能基础 (3讲)
第33讲 | 后台服务出现明显“变慢”,谈谈你的诊断思路?
第34讲 | 有人说“Lambda能让Java程序慢30倍”,你怎么看?
第35讲 | JVM优化Java代码时都做了什么?
模块5 Java应用开发扩展 (4讲)
第36讲 | 谈谈MySQL支持的事务隔离级别,以及悲观锁和乐观锁的原理和应用场景?
第37讲 | 谈谈Spring Bean的生命周期和作用域?
第38讲 | 对比Java标准NIO类库,你知道Netty是如何实现更高性能的吗?
第39讲 | 谈谈常用的分布式ID的设计方案?Snowflake是否受冬令时切换影响?
周末福利 (2讲)
周末福利 | 谈谈我对Java学习和面试的看法
周末福利 | 一份Java工程师必读书单
结束语 (1讲)
结束语 | 技术没有终点
Java核心技术面试精讲
登录|注册

第11讲 | Java提供了哪些IO方式? NIO如何实现多路复用?

杨晓峰 2018-05-29
IO 一直是软件开发中的核心部分之一,伴随着海量数据增长和分布式系统的发展,IO 扩展能力愈发重要。幸运的是,Java 平台 IO 机制经过不断完善,虽然在某些方面仍有不足,但已经在实践中证明了其构建高扩展性应用的能力。
今天我要问你的问题是,Java 提供了哪些 IO 方式? NIO 如何实现多路复用?

典型回答

Java IO 方式有很多种,基于不同的 IO 抽象模型和交互方式,可以进行简单区分。
首先,传统的 java.io 包,它基于流模型实现,提供了我们最熟知的一些 IO 功能,比如 File 抽象、输入输出流等。交互方式是同步、阻塞的方式,也就是说,在读取输入流或者写入输出流时,在读、写动作完成之前,线程会一直阻塞在那里,它们之间的调用是可靠的线性顺序。
java.io 包的好处是代码比较简单、直观,缺点则是 IO 效率和扩展性存在局限性,容易成为应用性能的瓶颈。
很多时候,人们也把 java.net 下面提供的部分网络 API,比如 Socket、ServerSocket、HttpURLConnection 也归类到同步阻塞 IO 类库,因为网络通信同样是 IO 行为。
第二,在 Java 1.4 中引入了 NIO 框架(java.nio 包),提供了 Channel、Selector、Buffer 等新的抽象,可以构建多路复用的、同步非阻塞 IO 程序,同时提供了更接近操作系统底层的高性能数据操作方式。
第三,在 Java 7 中,NIO 有了进一步的改进,也就是 NIO 2,引入了异步非阻塞 IO 方式,也有很多人叫它 AIO(Asynchronous IO)。异步 IO 操作基于事件和回调机制,可以简单理解为,应用操作直接返回,而不会阻塞在那里,当后台处理完成,操作系统会通知相应线程进行后续工作。

考点分析

我上面列出的回答是基于一种常见分类方式,即所谓的 BIO、NIO、NIO 2(AIO)。
在实际面试中,从传统 IO 到 NIO、NIO 2,其中有很多地方可以扩展开来,考察点涉及方方面面,比如:
基础 API 功能与设计, InputStream/OutputStream 和 Reader/Writer 的关系和区别。
NIO、NIO 2 的基本组成。
给定场景,分别用不同模型实现,分析 BIO、NIO 等模式的设计和实现原理。
NIO 提供的高性能数据操作方式是基于什么原理,如何使用?
或者,从开发者的角度来看,你觉得 NIO 自身实现存在哪些问题?有什么改进的想法吗?
IO 的内容比较多,专栏一讲很难能够说清楚。IO 不仅仅是多路复用,NIO 2 也不仅仅是异步 IO,尤其是数据操作部分,会在专栏下一讲详细分析。

知识扩展

首先,需要澄清一些基本概念:
区分同步或异步(synchronous/asynchronous)。简单来说,同步是一种可靠的有序运行机制,当我们进行同步操作时,后续的任务是等待当前调用返回,才会进行下一步;而异步则相反,其他任务不需要等待当前调用返回,通常依靠事件、回调等机制来实现任务间次序关系。
区分阻塞与非阻塞(blocking/non-blocking)。在进行阻塞操作时,当前线程会处于阻塞状态,无法从事其他任务,只有当条件就绪才能继续,比如 ServerSocket 新连接建立完毕,或数据读取、写入操作完成;而非阻塞则是不管 IO 操作是否结束,直接返回,相应操作在后台继续处理。
不能一概而论认为同步或阻塞就是低效,具体还要看应用和系统特征。
对于 java.io,我们都非常熟悉,我这里就从总体上进行一下总结,如果需要学习更加具体的操作,你可以通过教程等途径完成。总体上,我认为你至少需要理解:
IO 不仅仅是对文件的操作,网络编程中,比如 Socket 通信,都是典型的 IO 操作目标。
输入流、输出流(InputStream/OutputStream)是用于读取或写入字节的,例如操作图片文件。
而 Reader/Writer 则是用于操作字符,增加了字符编解码等功能,适用于类似从文件中读取或者写入文本信息。本质上计算机操作的都是字节,不管是网络通信还是文件读取,Reader/Writer 相当于构建了应用逻辑和原始数据之间的桥梁。
BufferedOutputStream 等带缓冲区的实现,可以避免频繁的磁盘读写,进而提高 IO 处理效率。这种设计利用了缓冲区,将批量数据进行一次操作,但在使用中千万别忘了 flush。
参考下面这张类图,很多 IO 工具类都实现了 Closeable 接口,因为需要进行资源的释放。比如,打开 FileInputStream,它就会获取相应的文件描述符(FileDescriptor),需要利用 try-with-resources、 try-finally 等机制保证 FileInputStream 被明确关闭,进而相应文件描述符也会失效,否则将导致资源无法被释放。利用专栏前面的内容提到的 Cleaner 或 finalize 机制作为资源释放的最后把关,也是必要的。
下面是我整理的一个简化版的类图,阐述了日常开发应用较多的类型和结构关系。
1.Java NIO 概览
首先,熟悉一下 NIO 的主要组成部分:
Buffer,高效的数据容器,除了布尔类型,所有原始数据类型都有相应的 Buffer 实现。
Channel,类似在 Linux 之类操作系统上看到的文件描述符,是 NIO 中被用来支持批量式 IO 操作的一种抽象。
File 或者 Socket,通常被认为是比较高层次的抽象,而 Channel 则是更加操作系统底层的一种抽象,这也使得 NIO 得以充分利用现代操作系统底层机制,获得特定场景的性能优化,例如,DMA(Direct Memory Access)等。不同层次的抽象是相互关联的,我们可以通过 Socket 获取 Channel,反之亦然。
Selector,是 NIO 实现多路复用的基础,它提供了一种高效的机制,可以检测到注册在 Selector 上的多个 Channel 中,是否有 Channel 处于就绪状态,进而实现了单线程对多 Channel 的高效管理。
Selector 同样是基于底层操作系统机制,不同模式、不同版本都存在区别,例如,在最新的代码库里,相关实现如下:
Windows上NIO2(AIO)模式则是依赖于iocp([](http://hg.openjdk.java.net/jdk/jdk/file/d8327f838b88/src/java.base/windows/classes/sun/nio/ch/Iocp.java))。
Chartset,提供 Unicode 字符串定义,NIO 也提供了相应的编解码器等,例如,通过下面的方式进行字符串到 ByteBuffer 的转换:
Charset.defaultCharset().encode("Hello world!"));
2.NIO 能解决什么问题?
下面我通过一个典型场景,来分析为什么需要 NIO,为什么需要多路复用。设想,我们需要实现一个服务器应用,只简单要求能够同时服务多个客户端请求即可。
使用 java.io 和 java.net 中的同步、阻塞式 API,可以简单实现。
取消
完成
0/1000字
划线
笔记
复制
© 版权归极客邦科技所有,未经许可不得传播售卖。 页面已增加防盗追踪,如有侵权极客邦将依法追究其法律责任。
该试读文章来自付费专栏《Java核心技术面试精讲》,如需阅读全部文章,
请订阅文章所属专栏。
立即订阅
登录 后留言

精选留言(61)

  • 王睿
    举个收快递的例子不知道理解是否正确。

    BIO,快递员通知你有一份快递会在今天送到某某地方,你需要在某某地方一致等待快递员的到来。

    NIO,快递员通知你有一份快递会送到你公司的前台,你需要每隔一段时间去前台询问是否有你的快递。

    AIO,快递员通知你有一份快递会送到你公司的前台,并且前台收到后会给你打电话通知你过来取。
    2018-09-13
    89
  • I am a psycho
    由于nio实际上是同步非阻塞io,是一个线程在同步的进行事件处理,当一组事channel处理完毕以后,去检查有没有又可以处理的channel。这也就是同步+非阻塞。同步,指每个准备好的channel处理是依次进行的,非阻塞,是指线程不会傻傻的等待读。只有当channel准备好后,才会进行。那么就会有这样一个问题,当每个channel所进行的都是耗时操作时,由于是同步操作,就会积压很多channel任务,从而完成影响。那么就需要对nio进行类似负载均衡的操作,如用线程池去进行管理读写,将channel分给其他的线程去执行,这样既充分利用了每一个线程,又不至于都堆积在一个线程中,等待执行。杨老师,不知道上述理解是否正确?
    2018-05-29
    82
  • 明翼
    看完之后还是不了解nio,感觉看起来越来越吃力了,大半天都啃不了一篇,好多东西都不熟悉,还要自己查资料去了解然后再回过来看,,,老师最好给些学习的资料让我们能找到,就这一篇感觉根本不够
    2018-07-05
    46
  • Chan
    B和N通常是针对数据是否就绪的处理方式来
    sync和async是对阻塞进行更深一层次的阐释,区别在于数据拷贝由用户线程完成还是内核完成,讨论范围一定是两个线程及以上了。


    同步阻塞,从数据是否准备就绪到数据拷贝都是由用户线程完成

    同步非阻塞,数据是否准备就绪由内核判断,数据拷贝还是用户线程完成

    异步非阻塞,数据是否准备就绪到数据拷贝都是内核来完成

    所以真正的异步IO一定是非阻塞的。

    多路复用IO即使有Reactor通知用户线程也是同步IO范畴,因为数据拷贝期间仍然是用户线程完成。

    所以假如我们没有内核支持数据拷贝的情况下,讨论的非阻塞并不是彻底的非阻塞,也就没有引入sync和async讨论的必要了

    不知道这样理解是否正确

    2018-06-16
    37
  • 雷霹雳的爸爸
    批评NIO确实要小心,我觉得主要是三方面,首先是如果是从写BIO过来的同学,需要有一个巨大的观念上的转变,要清楚网络就是并非时刻可读可写,我们用NIO就是在认真的面对这个问题,别把channel当流往死里用,没读出来写不进去的时候,就是该考虑让度线程资源了,第二点是NIO在不同的平台上的实现方式是不一样的,如果你工作用电脑是win,生产是linux,那么建议直接在linux上调试和测试,第三点,概念上的,理解了会在各方面都有益处,NIO在IO操作本身上还是阻塞的,也就是他还是同步IO,AIO读写行为的回调才是异步IO,而这个真正实现,还是看系统底层的,写完之后,我觉得我这一二三有点凑数的嫌疑

    作者回复: 不错,不过,在非常有必要之前,不见得都要底层,毕竟各种抽象,都是为特定领域工程师准备的,JMM等抽象都是为了大家有个清晰的、不同层面的高效交流

    2018-05-29
    31
  • Allen
    希望能听到更多原理性的东西,而不是在网上能搜到的样例代码
    2018-06-30
    25
  • Chan
    忘记回答问题了。所以对于多路复用IO,当出现有的IO请求在数据拷贝阶段,会出现由于资源类型过份庞大而导致线程长期阻塞,最后造成性能瓶颈的情况

    作者回复: 对

    2018-06-16
    13
  • aiwen
    到底啥是多路复用?一个线程管理多个链接就是多路复用?
    2018-06-02
    11
  • zjh
    看nio代码部分,请求接受和处理都是一个线程在做。这样的话,如果有多个请求过来都是按顺序处理吧,其中一个处理时间比较耗时的话那所有请求不都卡住了吗?如果把nio的处理部分也改成多线程会有什么问题吗

    作者回复: 这种情况需要考虑把耗时操作并发处理,再说处理是费cpu,还是重io,需要不同处理;如果耗时操作非常多,就不符合这种模型的适用场景

    2018-05-31
    7
  • lorancechen
    我也自己写过一个基于nio2的网络程序,觉得配合futrue写起来很舒服。
    仓库地址:https://github.com/LoranceChen/RxSocket 欢迎相互交流开发经验~

    记得在netty中,有一个搁置的netty5.x项目被废弃掉了,原因有一点官方说是性能提升不明显,这是可以理解的,因为linux下是基于epoll,本质还是select操作。

    听了课程之后,有一点印象比较深刻,select模式是使用一个线程做监听,而bio每次来一个链接都要做线程切换,所以节省的时间在线程切换上,当然如果是c/c++实现,原理也是一样的。


    想问一个一直困惑的问题,select内部如何实现的呢?
    个人猜测:不考虑内核,应用层的区分,单纯从代码角度考虑,我猜测,当select开始工作时,有一个定时器,比如每10ms去检查一下网络缓冲区中是否有tcp的链接请求包,然后把这些包筛选出来,作为一个集合(即代码中的迭代器)填入java select类的一个集合成员中,然后唤醒select线程,做一个while遍历处理链接请求,这样一次线程调度就可以处理10ms内的所有链接。与bio比,节省的时间在线程上下文切换上。不知道这么理解对不对。
    另外,也希望能出一个课程,按照上面这种理解底层的方式,讲讲select(因为我平常工作在linux机器,所以对select epoll比较感兴趣)如何处理read,write操作的。谢谢~

    作者回复: 坦白说,内核epoll之类实现细节目前我的理解也有限

    2018-05-31
    7
  • 扁担
    据我理解,NIO2的局限性在于,如果回调时客户端做了重操作,就会影响调度,导致后续的client回调缓慢

    作者回复: 对,这是这种多路复用的主要局限之一,nodejs等其他类似框架都有这问题

    2018-10-21
    6
  • 扁扁圆圆
    这里Nio的Selector只注册了一个sever chanel,这没有实现多路复用吧,多路复用不是注册了多个channel ,处理就绪的吗?而且处理客户端请求也是在同线程内,这还不如上面给的Bio解决方案吧

    作者回复: 这是简化的例子,少占篇幅

    2018-06-02
    5
  • 逐梦之音
    IO的调用可以分为三大块,请求调用,逻辑处理,响应返回处理。常规的BIO在这三个阶段会串行的阻塞的。NIO其实可以理解为将这三个阶段尽可能的去阻塞或者减少阻塞。看了上面的例子,NIO的服务器端在接受客户端请求的时候,是单线程执行的,而BIO是多线程处理的。但是不管咋的,他们服务器端处理具体的客户业务逻辑是都要用多线程的吧?
    2018-05-29
    5
  • godtrue
    对比而言,感觉这节讲解的不够细致,学完以后,能回答开篇的两个问题了,Java提供了IO/BIO/NIO/AIO这几种IO的方式,但是他们的优缺点,使用场景等讲解的不多,推荐看下慕课的 https://www.imooc.com/learn/941 这个课程,前半部分把Java IO相关的东西讲解的还是比较详细的。多路复用,这个东西能节省服务端的线程创建成本,一个线程监听多个通道,那个通道就绪了,就处理那个通道,不过具体他是怎么实现的呢?主动的不断轮询?还是被动的接受信息?
    如果一个通道的处理时间比较长,其他的多个通道又都就绪了,此时该怎么办?就让其他的就绪的通道等待着?
    感觉是开眼界还行,如果是全弄明白,还是乏力的,要看专业的书啦!
    2018-12-15
    4
  • 萧萧
    作者对同步/异步, 阻塞/非阻塞的概念说明存在问题。

    《操作系统(第9版)》中关于进程通信中有对这部分概念做过解释, 在进程间通信的维度, 同步和阻塞,异步和非阻塞是相同的概念。

     沿着作者的概念解释简单推论一下就可以发现:

    如果同步操作是需要等待调用返回才能进行下一步, 显然这个调用是阻塞的。

    反之, 不需要等待调用返回的接口,必然需要提供事件, 回调等机制,这种调用显然是非阻塞的。

    作者回复: 这东西并没有完全共识,概念定义要看上下文,很多情况下可以算是同等,但在网络IO编程中是区分的,本文的关注点就是这个

    2018-08-08
    4
  • 张凯江
    cpu运算密集型应用。node得诟病
    2018-07-18
    4
  • lorancechen
    还有一个问题请教,select在单线程下处理监听任务是否会成为瓶颈?能否通过创建多个select实例,并发监听socket事件呢?

    作者回复: Doug Lea曾经推荐过多个Selector,也就是多个reactor,如果你是这意思

    2018-05-31
    4
  • ykkk88
    这个nio看起来还是单线程在处理,如果放到多线程池中处理和bio加线程池有啥区别呢
    2018-05-29
    4
  • 残月@诗雨
    杨老师,有个问题一直不太明白:BufferedInputStream和普通的InputStream直接read到一个缓冲数组这两种方式有什么区别?

    作者回复: 我理解是bufferedIS是内部预读,所以两个buffer的意义不一样,前面是减少磁盘之类操作

    2018-05-29
    4
  • 灰飞灰猪不会灰飞.烟灭
    老师 注册管道到select上,应该用队列实现的吧?
    开启一个线程大概需要多少内存开销呢,我记得数据库连接大概2M

    作者回复: 线程看定义stack大小等,32、64位都不一样

    2018-05-29
    4
收起评论
61
返回
顶部