当前位置 : 主页 > 编程语言 > java >

Java 提供了哪些 IO 方式, NIO 如何实现多路复用

来源:互联网 收集:自由互联 发布时间:2022-06-30
Java 提供了哪些 IO 方式, NIO 如何实现多路复用 Java IO 方式 Java IO 方式有很多种,基于不同的 IO 抽象模型和交互方式,可以进行简单区分。 同步阻塞 IO 首先,传统的 Java.io 包基于流模

Java  提供了哪些 IO 方式, NIO 如何实现多路复用

Java IO 方式

Java IO 方式有很多种,基于不同的 IO 抽象模型和交互方式,可以进行简单区分。

同步阻塞 IO

首先,传统的 Java.io 包基于流模型实现,提供了我们最熟知的一些 IO 功能,比如 File 抽象,输入输出流等,交互方式是同步 、阻塞的方式,也就是说,在读取输入流或者写入输出流是,在读写动作完成之前,线程会一直阻塞在哪,他们之间的调用时可靠的先行顺序。

java.io 包的好处就是代码比较简单直观,缺点就是 IO 效率和扩展性存在的局限性,容易成为应用性能的瓶颈。

很多时候,人们也把 java.net下面提供的部分网络API,比如 Socket、 Serversocket、 HttpURLConnection也归类到同步阻塞IO类库,因为网络通信IO行为。

同步非阻塞IO

在Java1.4中引入了NIO框架(java.nio包),提供了 Channel、 Selector、 Buffer等新的抽象,可以构建多路复用的、同步非阻塞IO程序,同时提供了更接近操作系统底层的高性能数据操作方

异步非阻塞IO

第三,在Java7中,NIO有了进一步的改进,也就是NIO2,引入了异步非阻塞IO方式,也有很多人叫它AIO( Asynchronous IO)。异步IO操作基于事件和回调机制,可以简单理解为,应用操作直接返回,而不会阻塞在那里,当后台处理完成,操作系统会通知相应线程进行后续工作。

什么是同步异步?

区分同步或异步( synchronous/ asynchronous)。简单来说,同步是一种可靠的有序运行机制,当我们进行同步操作时,后续的任务是等待当前调用返回,才会进行下而异步则相反,其他任务不需要等待当前调用返回,通常依靠事件、回调等机制来实现任务间次序关系

什么是阻塞非阻塞?

区分阻塞与非阻塞( blocking/on- blocking)。在进行阻塞操作时,当前线程会处于阻塞状态,无法从事其他任务,只有当条件就绪才能继续,比如 Serversocket新连接建立完毕,或数据读取、写入操作完成;而非阻塞则是不管IO操作是否结束,直接返回,相应操作在后台继续处理。

  • ​​https://docs.oracle.com/javase/tutorial/essential/io/streams.html​​

java.io 具体实现

  • IO不仅仅是对文件的操作,网络编程中,比如 Socket 通信,都是典型的IO操作目标。
  • 输入流、输出流( Inputstream/outputstream)是用于读取或写入字节的,例如操作图片文件。
  • Reader/ Writer则是用于操作字符,增加了字符编解码等功能,适用于类似从文件中读取或者写入文本信息。本质上计算机操作的都是字节,不管是网络通信还是文件读取, Reader/ Writer相当于构建了应用逻辑和原始数据之间的桥梁
  • BufferedOutputStream 等带缓冲区的实现,可以避免频繁的磁盘读写,进而提高IO处理效率。这种设计利用了缓冲区,将批量数据进行一次操作,很多IO工具类都实现了Closeable接口,因为需要进行资源的释放。比如,打开 FileInputstream,它就会获取相应的文件描述符( FileDescriptor)
  • 利用 try-with-resources、try-finally 等机制保证 FileInputstream被明确关闭,进而相应文件描述符也会失效,否则将导致资源无法被释放。之前提到的 Cleaner或finalizer 机制作为资源释放的最后保障,也是必要的。
  • ​​https://mp.weixin.qq.com/s?__biz=MzU4NDEwMzU3Mg==&mid=2247484409&idx=1&sn=3493be8a596b09ce0303cde0c0bc9fed&chksm=fd9fa002cae82914d77792d2232a74a76cb41525a164abd3bb1ac012d78edf089e242d3f9d5d&token=2120176138&lang=zh_CN#rd​​

Java  提供了哪些 IO 方式, NIO 如何实现多路复用_客户端image

Java NIO

组成部分

  • Buffer , 高效的数据容器,处理布尔类型,所有的原始数据类型,都有相应的Buffer 实现。
  • Channel ,类似 在linux 之类操作系统上看到的文件描述符,是 NIO 中被用来支撑批量式 IO 操作的一种抽象。File 或者 Socket ,通常被认为是 比较高层次的抽象,而 Channel 则是更加操作系统底层的一种抽象,这也使得 NIO 可以充分利用现代操作系统底层机制,获得特定场景的性能优化。
  • Selector 是 NIO 实现多路复用的基础,它提供了一种高效的机制,可以检测到注册在Selector 上的多个 Channel 中,是否有 Channel 处于就绪状态,进而实现单线程对多 Channel 的高效处理。
  • Charset 提供了 Unicode 字符串定义,NIO 提供了相应的解码器等,
Charset.defaultCharset().encode("Hello world!")

Selector 同样是基于底层操作系统机制,不同模式,不同版本都存在区别,例如。在 linux 上依赖 epoll, windows 上 NIO2 依赖的是 iocp。

  • hg.openjdk.java.net/jdk/jdk/file/d8327f838b88/src/java.base/linux/classes/sun/nio/ch/EPollSelectorImpl.java
  • ​​http://hg.openjdk.java.net/jdk/jdk/file/d8327f838b88/src/java.base/windows/classes/sun/nio/ch/Iocp.java​​

NIO 能解决什么问题

通过一个典型场景,为什么需要多路复用,如果需要实现一个服务器应用,只简单要求能同时服务多个客户端请求即可。

同步阻塞 API 实现

  • 服务器端启动 ServerSocket ,端口0表示自动绑定一个空隙端口。
  • 调用 accept 方法,阻塞等待客户端连接
  • 利用 Socket 模拟一个简单的客户端只进行连接,读取打印。
  • 当连接建立后,启动一个单独线程回复端请求。

同步阻塞IO 实现

public class DemoServer extends Thread {
private ServerSocket serverSocket;

public int getPort() {
return serverSocket.getLocalPort();
}

public void run() {
try {
serverSocket = new ServerSocket(0);
while (true) {
// 非常占用内存资源,每个客户端启用一个线程是十分不合理
Socket socket = serverSocket.accept();
RequesHandler requesHandler = new RequesHandler(socket);
requesHandler.start();
}
} catch (IOException e) {
e.printStackTrace();
} finally {
if (serverSocket != null) {
try {
serverSocket.close();
} catch (IOException e) {
e.printStackTrace();
}
;
}
}
}

public static void main(String[] args) throws IOException {
DemoServer server = new DemoServer();
server.start();
try (Socket client = new Socket(InetAddress.getLocalHost(), server.getPort())) {
BufferedReader buferedReader = new BufferedReader(new InputStreamReader(client.getInputStream()));
buferedReader.lines().forEach(s -> System.out.println(s));
}
}
}

// 简化实现,不做读取,直接发送字符串
class RequesHandler extends Thread {
private Socket socket;

RequesHandler(Socket socket) {
this.socket = socket;
}

@Override
public void run() {
try (PrintWriter out = new PrintWriter(socket.getOutputStream());) {
out.println("Hello world!");
out.flush();
} catch (Exception e) {
e.printStackTrace();
}
}
}

每次 new 一个线程或者销毁一个线程是有明显的开销的,每个线程都有单独的线程结构,非常占用内存资源,每个客户端启用一个线程是十分不合理的, 因此可以采用线程池的方式进行优化.

伪异步 IO

也是阻塞IO,采用线程池的方式处理请求,当来一个新的客户端连接时,将请求 Socket 封装成一个 task ,放到线程池中取执行。

serverSocket = new ServerSocket(0);
executor = Executors.newFixedThreadPool(8);
while (true) {
Socket socket = serverSocket.accept();
RequesHandler requesHandler = new RequesHandler(socket);
executor.execute(requesHandler);
}

通过一个固定大小的线程池,来负责管理工作线程,避免频繁创建,销毁线程的开销。

Java  提供了哪些 IO 方式, NIO 如何实现多路复用_客户端_02image

试想,如果连接数并不是特别多,只有几百个连接,这种模式可以很好的工作。但是如果连接数急剧上升,这种实现就无法很好的工作,因为线程上下文切换开销会在高并发时变得很明显。

如果连接数并不是非常多,只有最多几百个连接的普通应用,这种模式往往可以工作的很好。但是,如果连接数量急剧上升,这种实现方式就无法很好地工作了,因为线程上下文切换开销会在高并发时变得很明显,这是同步阻塞方式的低扩展性劣势。

NIO 实现

NIO(非阻塞IO) 多路复用机制

public class NIOServer extends Thread {
public void run() {
try (Selector selector = Selector.open(); ServerSocketChannel serverSocket = ServerSocketChannel.open();) {// 创建Selector和Channel
serverSocket.bind(new InetSocketAddress(InetAddress.getLocalHost(), 8888));
serverSocket.configureBlocking(false);
// 注册到Selector,并说明关注点
serverSocket.register(selector, SelectionKey.OP_ACCEPT);
while (true) {
selector.select();// 阻塞等待就绪的Channel,这是关键点之一
Set<SelectionKey> selectedKeys = selector.selectedKeys();
Iterator<SelectionKey> iter = selectedKeys.iterator();
while (iter.hasNext()) {
SelectionKey key = iter.next();
// 生产系统中一般会额外进行就绪状态检查
sayHelloWorld((ServerSocketChannel) key.channel());
iter.remove();
}
}
} catch (IOException e) {
e.printStackTrace();
}
}

private void sayHelloWorld(ServerSocketChannel server) throws IOException {
try (SocketChannel client = server.accept();) {
ByteBuffer readBuffer = ByteBuffer.allocate(32);
client.read(readBuffer);
System.out.println("Server received : " + new String(readBuffer.array()));
ByteBuffer writeBuffer = ByteBuffer.allocate(128);
writeBuffer.put("hello xiaoming".getBytes());
writeBuffer.flip();
client.write(writeBuffer);
//client.write(Charset.defaultCharset().encode("Hello world!"));
}
}

public static void main(String[] args) throws IOException {
NIOServer server = new NIOServer();
server.start();

try {
SocketChannel socketChannel = SocketChannel.open();
socketChannel.connect(new InetSocketAddress(InetAddress.getLocalHost(), 8888));

ByteBuffer writeBuffer = ByteBuffer.allocate(32);
ByteBuffer readBuffer = ByteBuffer.allocate(32);

writeBuffer.put("hello".getBytes());
writeBuffer.flip();

while (true) {
writeBuffer.rewind();
socketChannel.write(writeBuffer);
// readBuffer.clear();
socketChannel.read(readBuffer);
System.out.println("Client received : " + new String(readBuffer.array()));
}
} catch (IOException e) {
}

}

/**
* @return
*/
private int getPort() {
return 8888;
}

Java  提供了哪些 IO 方式, NIO 如何实现多路复用_非阻塞_03这样做的好处:

  • 首先,通过 Selector.open()创建一个 Selector 类似调度员的角色。
  • 然后,创建一个 ServerSocketChannel ,并且向 Selector 注册,并且通过指定 SelectionKey.OP_ACCEPT ,告诉调度员,他关注的是最新连接请求。
  • Selector 阻塞在 select 操作,当有Channel 发送接入请求,就会被唤醒。
  • 在 sayHelloWorld 方法中,通过 socketChannel 和 Buffer 进行数据操作。

在前面两个样例,阻塞IO和伪异步IO,一个是使用 new 线程的方式,一个是采用线程池管理的方式, IO都是同步阻塞模式,所以需要多线程以实现多任务处理。而 NIO 则是利用了单线程轮询事件的机制,通过高效地定位就绪的Channel,来决定做什么,仅仅select阶段是阻塞的,可以有效避免大量客户端连接时,频繁线程切换带来的问题,应用的扩展能力有了非常大的提高。

AIO 实现

JDK 1.7 升级了NIO 类库,升级后的 NIO 也被称为 NIO 2.0 ,NIO 2.0 引入了异步通道的概念,并提供了异步文件通道和异步套接字通道的实现。

  • 通过 java.util.concurrent.Future 类来标识异步操作的结果
  • 在执行异步操作的时候出入一个 java.nio.channels

跟 NIO 比对

  • 基本抽象很相似, AsynchronousServerSocketChannel对应于NIO例子中的ServerSocketChannel;AsynchronousSocketChannel则对应SocketChannel。
  • 业务逻辑的关键在于,通过指定CompletionHandler回调接口,在accept/read/write等关键节点,通过事件机制调用,这是非常不同的一种编程思路。
AsynchronousServerSocketChannel serverSock = AsynchronousServerSocketChannel.open().bind(new InetSocketAddress("127.0.0.1", 8888));
serverSock.accept(null, new CompletionHandler<AsynchronousSocketChannel, Object>() {
final ByteBuffer buffer = ByteBuffer.allocate(1024);
@Override
public void completed(final AsynchronousSocketChannel result, Object attachment) {
buffer.clear();
try {
// 把socket中的数据读取到buffer中
result.read(buffer).get();
buffer.flip();
System.out.println("Echo " + new String(buffer.array()).trim() + " to " + result);
// 把收到的直接返回给客户端
result.write(buffer);
buffer.flip();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
} finally {

}
}

@Override
public void failed(Throwable throwable, Object attachment) {
}
});

程序员开发者社区

Java  提供了哪些 IO 方式, NIO 如何实现多路复用_非阻塞_04

关注我们,了解更多

关注公众号:【程序员开发者社区】 回复:资源


上一篇:Java String 源码分析
下一篇:没有了
网友评论