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 ;
intd =
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 == null ) continue ;
你需要注意的是此时调用 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 会来得更简单。欢迎点击在看,关注公众号,回复 " 礼物 " 获取。
点下“在看”,鼓励一下?
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网
猜你喜欢:- 前端面试之盒模型
- 面试官:说说双亲委派模型?
- 从一道 iOS 面试题到 Swift 对象模型和运行时细节——「iOS 面试之道」勘误
- 面试官,你别再问了——JAVA之内存模型(简化版)
- 再见 Go 面试官:GMP 模型,为什么要有 P?
- 高级 Java 面试必问的三大 IO 模型,你 get 了吗?
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
有限与无限的游戏
[美]詹姆斯·卡斯 / 马小悟、余倩 / 电子工业出版社 / 2013-10 / 35.00元
在这本书中,詹姆斯·卡斯向我们展示了世界上两种类型的「游戏」:「有限的游戏」和「无限的游戏」。 有限的游戏,其目的在于赢得胜利;无限的游戏,却旨在让游戏永远进行下去。有限的游戏在边界内玩,无限的游戏玩的就是边界。有限的游戏具有一个确定的开始和结束,拥有特定的赢家,规则的存在就是为了保证游戏会结束。无限的游戏既没有确定的开始和结束,也没有赢家,它的目的在于将更多的人带入到游戏本身中来,从而延续......一起来看看 《有限与无限的游戏》 这本书的介绍吧!