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

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

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操作是否结束,直接返回,相应操作在后台继续处理。



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 机制作为资源释放的最后保障,也是必要的。



640.png


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。


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);
}


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


640.png

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

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


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;
}


640.png


这样做的好处:

  • 首先,通过 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) {
    }
});


相关文章
|
4月前
|
监控 Java API
现代 Java IO 高性能实践从原理到落地的高效实现路径与实战指南
本文深入解析现代Java高性能IO实践,涵盖异步非阻塞IO、操作系统优化、大文件处理、响应式网络编程与数据库访问,结合Netty、Reactor等技术落地高并发应用,助力构建高效可扩展的IO系统。
142 0
|
1月前
|
Java Unix Go
【Java】(8)Stream流、文件File相关操作,IO的含义与运用
Java 为 I/O 提供了强大的而灵活的支持,使其更广泛地应用到文件传输和网络编程中。!但本节讲述最基本的和流与 I/O 相关的功能。我们将通过一个个例子来学习这些功能。
158 1
|
3月前
|
Java 测试技术 API
Java IO流(二):文件操作与NIO入门
本文详解Java NIO与传统IO的区别与优势,涵盖Path、Files类、Channel、Buffer、Selector等核心概念,深入讲解文件操作、目录遍历、NIO实战及性能优化技巧,适合处理大文件与高并发场景,助力高效IO编程与面试准备。
|
3月前
|
SQL Java 数据库连接
Java IO流(一):字节流与字符流基础
本文全面解析Java IO流,涵盖字节流、字符流及其使用场景,帮助开发者理解IO流分类与用途,掌握文件读写、编码转换、异常处理等核心技术,通过实战案例提升IO编程能力。
|
4月前
|
存储 Java Linux
操作系统层面视角下 Java IO 的演进路径及核心技术变革解析
本文从操作系统层面深入解析Java IO的演进历程,涵盖BIO、NIO、多路复用器及Netty等核心技术。分析各阶段IO模型的原理、优缺点及系统调用机制,探讨Java如何通过底层优化提升并发性能与数据处理效率,全面呈现IO技术的变革路径与发展趋势。
110 2
|
6月前
|
监控 应用服务中间件 Linux
掌握并发模型:深度揭露网络IO复用并发模型的原理。
总结,网络 I/O 复用并发模型通过实现非阻塞 I/O、引入 I/O 复用技术如 select、poll 和 epoll,以及采用 Reactor 模式等技巧,为多任务并发提供了有效的解决方案。这样的模型有效提高了系统资源利用率,以及保证了并发任务的高效执行。在现实中,这种模型在许多网络应用程序和分布式系统中都取得了很好的应用成果。
204 35
|
8月前
|
存储 网络协议 安全
Java网络编程,多线程,IO流综合小项目一一ChatBoxes
**项目介绍**:本项目实现了一个基于TCP协议的C/S架构控制台聊天室,支持局域网内多客户端同时聊天。用户需注册并登录,用户名唯一,密码格式为字母开头加纯数字。登录后可实时聊天,服务端负责验证用户信息并转发消息。 **项目亮点**: - **C/S架构**:客户端与服务端通过TCP连接通信。 - **多线程**:采用多线程处理多个客户端的并发请求,确保实时交互。 - **IO流**:使用BufferedReader和BufferedWriter进行数据传输,确保高效稳定的通信。 - **线程安全**:通过同步代码块和锁机制保证共享数据的安全性。
338 23
|
9月前
|
缓存 网络协议 Java
JAVA网络IO之NIO/BIO
本文介绍了Java网络编程的基础与历史演进,重点阐述了IO和Socket的概念。Java的IO分为设备和接口两部分,通过流、字节、字符等方式实现与外部的交互。
291 0
|
12月前
|
Java
java 中 IO 流
Java中的IO流是用于处理输入输出操作的机制,主要包括字节流和字符流两大类。字节流以8位字节为单位处理数据,如FileInputStream和FileOutputStream;字符流以16位Unicode字符为单位,如FileReader和FileWriter。这些流提供了读写文件、网络传输等基本功能。
218 10
|
存储 Java 数据安全/隐私保护
从零开始学习 Java:简单易懂的入门指南之IO字符流(三十一)
从零开始学习 Java:简单易懂的入门指南之IO字符流(三十一)