31

高级 Java 面试必问的三大 IO 模型,你 get 了吗?

 5 years ago
source link: https://mp.weixin.qq.com/s/1eaRYeQnjahudacD9uulYw
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

VZrqQrf.jpg!web

Photo By Instagram sooyaaa

问题 14

不管你平时是否接触大量的 IO 网络编程,IO 模型都是高级 Java 工程师面试非常高频的一道题。你了解 Java 的 IO 模型吗?多路复用技术是什么?

我的答案

在了解 Java IO 模型之前,我们先来明确几个概念,初学者通常会被如下几个概念给误导:

同步和异步

同步指的是当程序在做一个任务的时候,必须做完当前任务才能继续做下一个任务,这是一种可靠有序的运行机制,假设当前任务执行失败了,可能就不会进行下一个任务了,往往在一些有依赖性的任务之间,我们使用同步机制。而异步恰恰相反,它不能保证有序性。程序在提交当前任务后,并不会等待任务结果,而是直接进行下一个任务,通常在一些任务之间没有依赖性关系的时候可以使用异步机制。

这么说可能还是有点抽象,我们举个例子来说吧。假设有 4 个数字 a, b, c, d,我们需要计算它们连续相除的结果。那么可以写这样一个函数:

public static int divide ( int  paraA,  int  paraB) {

return paraA / paraB;

}

如上即为我们的方法,假设我们使用同步机制去做,程序会写成类似如下这样:

int tmp1 = divide(a, b);

int tmp2 = divide(tmp1, c);

int

result = divide(tmp2, d);

此处假如我们定义了 4 个数字的值为如下:

int a =  1 ;

int b =  0 ;

int c =  1 ;

int

d = 

1

;

这时候我们编写的同步机制的程序,tmp2 的计算需要依赖于 tmp1,result 又依赖于 tmp2,事实上计算 tmp1 的值时候即会发生除数为的 0 的异常 ArithmeticException。

我们也可以通过多线程来 将这个程序转换为异步机制的方式去做,如下(我们不考虑整数进位造成的结果不同问题):

Callable<Integer> cA = () -> divide(a, b);

FutureTask<Integer> taskA =  new FutureTask<>(cA);

new Thread(taskA).start();

Callable<Integer> cB = () -> divide(c, d);

FutureTask<Integer> taskB =  new FutureTask<>(cB);

new Thread(taskB).start();

int

tResult = taskA.get() / taskB.get();

如上我们使用多线程将同步的运作的程序修改为了异步,先去同时计算 a / b 和 b / c 的结果,它俩之间没有相互依赖,taskB 不会等待 taskA 的结果,taskA 出现 ArithmeticException 也不会影响 taskB 的运行。

这就是同步与异步,你 get 到了吗?

阻塞和非阻塞

阻塞指的是当前线程在执行运算的时候会阻塞直到预期的结果出现后,线程才可以继续进行后续的操作。而非阻塞则是在执行某项操作后直接返回,无论结果是什么。是不是还有点抽象,我们来举个例子。改造一下上面的 divide 方法,将 divide 方法改造为会阻塞的方法:

public synchronized int blockingDivide ( int  paraA,  int  paraB) throws InterruptedException  {

synchronized (SyncOrAsyncDemo.class) {

wait( 5000 );

return paraA / paraB;

}

}

如上,我们将 divide 方法修改为了一个会阻塞的方法,当我们的主线程去调用 blockingDivide 方法的时候,该方法会将当前线程阻塞直到方法运行结束。我们也可以使用多线程和回调将该方法修改为一个非阻塞方法:

public synchronized void nonBlockingDivide ( int  paraA,  int  paraB, Callback callback) throws InterruptedException  {

new Thread( new Runnable() {

@Override

public void run () {

synchronized (SyncOrAsyncDemo.class) {

try {

wait( 5000 );

catch (InterruptedException e) {

e.printStackTrace();

}

callback.handleResult(paraA / paraB);

}

}

}).start();

}

如上,我们将业务逻辑包装到了一个单独的线程中,执行结束后调用主线程设置好的回调函数即可。而主线程在调用该方法时不会阻塞,会直接返回结果,然后进行接下来的操作,这就是非阻塞机制。

弄清楚这几个概念以后,让我们一起来看看 Java IO 的几种模型吧。

Blocking IO(同步阻塞 IO)

在 Java 1.0 时代 JDK 提供了面向 Stream 流的同步阻塞式 IO 模型的实现,让我们用一段伪代码实际感受一下:

try (ServerSocket serverSocket =  new ServerSocket( 8888 )) {

while ( true ) {

Socket socket = serverSocket.accept();

// 提交到线程池处理后续的任务

executorService.submit( new ProcessRequest(socket));

}

catch (Exception e) {

e.printStackTrace();

}

我们在一个死循环里面调用了 ServerSocket 的阻塞方法 accept 方法,该方法调用后会阻塞直到有客户端连接过来。如果此时有客户端连接了,任务继续进行,我们此处将连接后的处理放在了线程池中去处理。接着我们模拟一个读取客户端内容的逻辑,也就是 ProcessRequest 的内在逻辑

try (BufferedReader reader =  new BufferedReader( new InputStreamReader(socket.getInputStream()))) {

int ch;

while ((ch = reader.read()) != - 1 ) {

System.out.print(( char )ch);

}

catch (Exception e) {

e.printStackTrace();

}

我们采用 BufferedReader 读取来自客户端的内容,调用 read 方法后,服务器端线程会一直阻塞直至收到客户端发送的内容过来,这就是 Java 1.0 提供的同步阻塞式 IO 模型。

Non-Blocking IO(同步非阻塞 IO)

在 Java 1.4 时代 JDK 为我们提供了面 Channel 的同步非阻塞的 IO 模型实现,同样以一段伪代码来展示:

try {

ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();

serverSocketChannel.bind( new InetSocketAddress( "127.0.0.1"8888 ));

while ( true ) {

SocketChannel socketChannel = serverSocketChannel.accept();

executorService.execute( new ProcessChannel(socketChannel));

}

catch (Exception e) {

e.printStackTrace();

}

默认情况下 ServerSocketChannel 采用的阻塞方式,调用 accept 方法会阻塞直到有客户端连接过来,通过 Channel 的 read 方法获取管道里面的内容时候同样会阻塞直到客户端有内容输入到服务器端:

ByteBuffer buffer = ByteBuffer.allocate( 1024 );

while ( true ) {

try {

if (socketChannel.read(buffer) != - 1 ) {

// do something

}

catch (IOException e) {

e.printStackTrace();

}

}

这时候我们可以调用 configureBlocking 方法,将管道设置为非阻塞模式:

serverSocketChannel.configureBlocking( false );

这个时候调用 accept 方法就是非阻塞方式了,它会立即返回结果,但是返回结果有可能是 null,所以我们做额外的判断处理,如

if (socketChannel ==  nullcontinue ;

你需要注意的是此时调用 Channel 的 read 方法仍然会阻塞当前线程知道有客户端有结果返回,不是说非阻塞吗,怎么还是阻塞呢?是时候亮出大杀器 Selector 了。

Selector 多路复用器可以让阻塞 IO 变得更加灵活,注意注册 Selector 必须将 Channel 设置为非阻塞模式:

/**省略部分相同的代码**/

Selector selector = Selector.open();

serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);

while ( true ) {

selector.select();

Set<SelectionKey> keys = selector.selectedKeys();

Iterator<SelectionKey> iter = keys.iterator();

while (iter.hasNext()) {

SelectionKey key = iter.next();

iter.remove();

if (key.isAcceptable()) {

SocketChannel socketChannel = ((ServerSocketChannel)key.channel()).accept();

socketChannel.configureBlocking( false );

socketChannel.register(selector, SelectionKey.OP_READ);

}

if (key.isReadable()) {

SocketChannel channel = (SocketChannel) key.channel();

ByteBuffer buffer = ByteBuffer.allocate( 1024 );

if (channel.read(buffer) != - 1 ) {

buffer.flip();

System.out.println(Charset.forName( "utf-8" ).decode(buffer));

key.cancel();

}

}

}

使用了 Selector 以后,我们会使用它的 select 方法来阻塞当前线程直到监听到操作系统的 IO 就绪事件,这里首先设置了 SelectionKey.OP_ACCEPT,当 select 方法返回时候代表 accept 已经就绪,服务器端与客户端可以正式连接,这时候的连接操作会立即返回属于非阻塞操作。

当与客户端建立连接后,我们关注的是 SelectionKey.OP_READ 事件,伪代码中使用

socketChannel.register(selector, SelectionKey.OP_READ)

注册了这个事件,当 select 方法再次返回时候代表 IO 目前已经到达可读状态,可以直接调用 channel.read(buffer) 来读取客户端发送过来的内容,这时候的 read 方法同样是一个非阻塞的操作。

如上就是 Java 1.4 为我们提供的非阻塞 IO 模式加上 Selector 多路复用技术,从而摆脱一个客户端连接占用一个线程资源的窘境,此处只有 select 方法阻塞,其余方法都是非阻塞运作。

虽然多路复用技术在性能上带来了提升,但是你也看到了。非阻塞编程相对于阻塞模式的代码段变得更加复杂了,而且还需要处理 NPE 问题。

Async Non-Blocking IO(异步非阻塞 IO)

Java 1.7 时代推出了异步非阻塞的 IO 模型,同样以一段伪代码来展示一下

AsynchronousServerSocketChannel serverChannel = AsynchronousServerSocketChannel

.open()

.bind( new InetSocketAddress( 8888 ));

serverChannel.accept(serverChannel,  new CompletionHandler<AsynchronousSocketChannel, AsynchronousServerSocketChannel>() {

@Override

public void completed (AsynchronousSocketChannel result, AsynchronousServerSocketChannel attachment) {

serverChannel.accept(serverChannel,  this );

ByteBuffer buffer = ByteBuffer.allocate( 1024 );

/**连接客户端成功后注册 read 事件**/

result.read(buffer, buffer,  new CompletionHandler<Integer, ByteBuffer>() {

@Override

public void completed (Integer result, ByteBuffer attachment) {

/**IO 可读事件出现的时候,读取客户端发送过来的内容**/

attachment.flip();

System.out.println(Charset.forName( "utf-8" ).decode(attachment));

}

/**省略无关紧要的方法**/

});

}

/**省略无关紧要的方法**/

});

你会发现异步非阻塞的代码量很少,而且AsynchronousServerSocketChannel 的 accept 方法使用后完全不会阻塞我们的主线程。主线程继续做后续的事情,在回调方法里面处理 IO 就绪事件后面的流程,这与前面介绍的 2 种同步 IO 模型编程思想上有比较大的区别。

想必通过开头介绍的几个概念你已经可以想到这款异步非阻塞的 IO 模型背后的实现原理了,无非就是 JDK 帮助我们启动了单独的线程,将同步的 IO 操作转换为了异步的 IO 操作,同时利用操作的 IO 事件模型,将阻塞的方法转换为了非阻塞的方法。

当然啦,NIO 为我们提供的也不仅仅是 Selector 多路复用技术,还有一些其他黑科技我们没有提到,感兴趣的话欢迎关注我等待后续的内容。

如上就是 Java 给我们提供的三种 IO 模型,通过我们一起探讨,你现在是不是已经掌握了它们之间的区别呢?欢迎留言与我讨论。

以上即为昨天的问题的答案,小伙伴们对这个答案是否满意呢?欢迎留言和我讨论。

又要到年末了,你是不是又悄咪咪的开始看机会啦。 为了广大小伙伴能充足电量,能顺利通过 BAT 的面试官无情三连炮,我特意推出大型刷题节目。 每天一道题目,第二天给答案,前一天给小伙伴们独立思考的机会。

我在公众号后台为正在准备面试的你准备了一份礼物 ,有它助你面试,offer 会来得更简单。欢迎点击在看,关注公众号,回复 " 礼物 " 获取。

yMFRJrF.jpg!web

点下“在看”,鼓励一下?


Recommend

  • 99
    • www.jianshu.com 7 years ago
    • Cache

    Java面试必问,ThreadLocal终极篇

    占小狼IP属地: 海南52018.01.21 10:33:09字数 1,433阅读 40,830 在面试环节中,考察"ThreadLocal"也是面试官的家常便...

  • 56
    • 微信 mp.weixin.qq.com 6 years ago
    • Cache

    面试必问之JVM原理

  • 63
    • 掘金 juejin.im 6 years ago
    • Cache

    互联网公司面试必问的Redis题目

    Redis是一个非常火的非关系型数据库,火到什么程度呢?只要是一个互联网公司都会使用到。Redis相关的问题可以说是面试必问的,下面我从个人当面试官的经验,总结几个必须要掌握的知识点。 介绍:Redis 是一个开源的使用 ANSI C 语言编写、遵守 BSD

  • 84

    这是mysql系列的下篇,上篇文章地址我附在文末。 什么是数据库索引?索引有哪几种类型?什么是最左前缀原则?索引算法有哪些?有什么区别? 索引是对数据库表中一列或多列的值进行排序的一种结构。一个非常恰当的比喻就是书的目录页与书的正文内容之间的关系,为了...

  • 75
    • database.51cto.com 6 years ago
    • Cache

    互联网公司面试必问的MySQL题目

    互联网公司面试必问的MySQL题目(上) 01什么是数据库事务?如果没有事物会有什么后果?事务的特性是什么? 事务是指作为单个逻辑工作单元执行的一系列操作,可以被看作一个单元的一系列SQL语句的集合。要么完全地执行,...

  • 37

    在进行社招面试时,有一个问题几乎是必问的: 你为什么要离开上一家公司? 其实这个问题主要是想试探一下你的核心诉求,并借此预估一下你在本公司工作的稳定性。常见的答案也无非就是这么几种:对薪酬不满意、干得不爽...

  • 49

    前言接下来我们来一起研究下Redis工程架构相关的问题,这部分内容出现的概率相对大一些,因为并不是所有人都会去研究源码,如果面试一味问源码那么可能注定是一场尬聊。面试时在不要求候选人对Redis非常熟练的前提下,工程问题将是不二之选。通过本文你将了解到以...

  • 40

    今天是 Python专题 第20篇文章,我们来聊聊Python当中的多线程。 其实关于元类还有很多种用法,比如说如何在元类当中设置参数啦,以及一些规约的用法等等。只不过这些用法比较小众,使用频率非常低,所以我们 不过多阐...

  • 9
    • segmentfault.com 3 years ago
    • Cache

    Android 面试之必问高级知识点

    Android 面试之必问Java基础Android 面试之必问Android基础知识1,编译模式1.1 概念在Android早期的版...

  • 7

    高级Java程序员必问,Redis事务终极篇

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK