Java IO,NIO多路复用

简述

IO

1.首先,传统java.io包,基于流模型实现,提供常见功能,File抽象,输入输出流等。交互方式是同步、阻塞的方式,即读取输入流或写入输出流时,在读、写动作完成之前,线程会一直阻塞,他们之间的调用时可靠的线性顺序。
2.java.io包的好处是代码比较简单、直观,缺点是IO效率和扩展性存在局限性,容易成为性能瓶颈
3.很多时候,java.net下面提供的部分网络API,比如Socket,ServerSocket,HttpURLConnection也归类到同步阻塞IO类库,因为网络通信同样是IO行为

NIO

java 1.4引入NIO框架,提供了Channel、Selector、buffer等新的抽象,可以构建多路复用,同步非阻塞IO程序,同时提供了更接近系统底层的高性能数据操作方式。
java 1.7中NIO有了进一步改进,即NIO2,引入异步非阻塞IO方式,或AIO(Asynchronous IO),异步IO操作基于事件和回调机制。简单理解,应用操作直接返回,而不会
阻塞在哪里,当后台处理完成,操作系统会通知相应线程进行后续工作。

#知识扩展

同步、异步

简单来说,同步是一种可靠的有序运行机制,当我们进行同步操作时,后续任务是等待当前调用返回,才会进行下异步;
异步相反,其他任务不需要当前任务执行完毕返回结果,通常依赖事件、回调机制实现任务间次序关系

阻塞、非阻塞

在进行阻塞操作时,当前线程会处于阻塞状态,无法从事其他任务,一致等到阻塞的任务完成,条件就绪才能继续,如ServerSocket新连接建立完毕,或数据读取、写入操作完成
而非阻塞则是不管IO操作是否结束,直接返回,相应操作在后台继续处理。

Java.IO

不能一概而论同步或阻塞就是低效,具体要看应用和系统特征,有的时候同步操作是必要
1.IO操作对象时File,网络(socket通信)等。
2.输入、输出流(InputStream/OutputStream)是用于读取或写入字节的,如操作图片、文件
3.Reader/Writer则是用于字符,增加字符编、解码适用于类似从文件中读取、写入文本信息。不管网络IO还是文件IO,本质操作都是字节,reader/writer相当于构建了应用逻辑和原始数据之间的桥梁。
4.BufferedOutputStream等缓冲区的实现,可以避免频繁从磁盘读取数据,将读写中间数据缓存在缓冲区,一次进行读取或写入磁盘,使用中千万别忘记flush()(刷新操作)
5.很多IO工具类都实现Closeable接口,进行资源释放。如打开FileInputStream,会获取对应文件描述符(FileDescriptor),需要使用try-with-resource-finally等机制保证字节操作流被明确关闭,相应FileDescriptor也会失效,否则导致资源无法被释放。利用java Cleaner,finalize机制作为资源释放的最后把关也是必要的。

IO类图

1.所有字节流、字符流都继承Object基类,实现Closeable接口
2.FileInputStream,ByteArrayInputStream,ObjectInputStream(序列化操作类),PipedInputStream等继承InputStream
3.FileOutputStream,ByteArrayOutputStream,ObjectOutputStream(序列化操作类),PipedOutputStream等继承OutputStream
4.InputStreamReader(管道套接流),BufferedReader,PipedReader等继承自Reader
5.OutputStreamWriter(管道套接流),BufferedWriter,PipedWriter等继承自Writer
6.BufferedInputStream继承自FileInputStream,BufferOutputStream继承自FileOutputStream 
7.FileReader继承自InputStreamReader,FileWriter继承自OutputStreamWriter

Java NIO

  • Buffer
高效数据容器,除了布尔类型,所有原始数据类型都有相应的Buffer实现
  • Channel
类似在Linux系统的FileDescriptor,NIO中被用来支持批量IO操作的抽象
File或Socket是较高层次抽象,channel是更加偏向操作系统底层的一种抽象,是的NIO得意充分利用现代OS底层机制,获得特定场景性能优化,如DMA(Direct Memory Access)等。
不同层次的抽象是相互关联的,我们可以通过Socket获取Channel,反之也可通过channel获取socket
  • Selector
NIO多路复用基础,提供一个高效机制,可以检测到注册到Selector上的多个Channel,是否有Channel处于就绪状态,进而实现单线程对多Channel的高效管理
Selector同样是基于底层操作系统机制,不同模式、不同版本都存在区别,如最新实现Linnux上依赖于[epoll](http://hg.openjdk.java.net/jdk/jdk/file/d8327f838b88/src/java.base/linux/classes/sun/nio/ch/EPollSelectorImpl.java)

window上NIO2(AIO)模式依赖于[iocp](http://hg.openjdk.java.net/jdk/jdk/file/d8327f838b88/src/java.base/windows/classes/sun/nio/ch/Iocp.java)
  • CharSet
提供Unicode字符串定义,NIO也提供相应的编解码器等,例如,通过下面的方式进行字符串到ByteBuffer转换
Charset.defaultCharset().encode("Hello world!"));

Java NIO应用场景

##服务器应用同时服务多个客户端请求

(1).java.io 和java.net 中同步、阻塞式API,简单实现,要点如下
1.服务器启动ServerSocket,端口0表示自动绑定一个空闲端口(也可以指定特定端口)
2.调用accept方法,阻塞等待客户端连接
3.利用Socket模拟一个简单客户端,只进行连接、读取、打印
4.当连接建立后,启动一个单独线程负责恢复客户端请求

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();
                RequestHandler requestHandler = new RequestHandler(socket);
                requestHandler.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 bufferedReader = new BufferedReader(new InputStreamReader(client.getInputStream()));
            bufferedReader.lines().forEach(s -> System.out.println(s));
        }catch(Exception e){
		
		}finally{
				if(null != bufferedReader)
					bufferedReader.close();
		}
    }
 }
// 简化实现,不做读取,直接发送字符串
class RequestHandler extends Thread {
    private Socket socket;
    RequestHandler(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();
        }
    }
 }


(2).潜在问题
java语言线程实现比较重量级,启动、销毁一个线程有明显开销,每个线程都有单独线程结构,内存占用明显,故每个client启动一个线程有些浪费。
改进如下:使用线程池机制来避免浪费,将启动客户端实例业务处理提交至线程池,管理工作线程,避免频繁创建、销毁线程的开销,是构建并发服务的典型工作方式

serverSocket = new ServerSocket(0);
executor = Executors.newFixedThreadPool(8);
 while (true) {
    Socket socket = serverSocket.accept();
    RequestHandler requestHandler = new RequestHandler(socket);
    executor.execute(requestHandler);
}
![Alt!](https://img-blog.csdnimg.cn/20200225132107196.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L2R5bWtrag==,size_16,color_FFFFFF,t_70)
4.如果连接数不多,只有最多几百个连接普通应用,这种模式可以工作很好,但是如果连接数急剧上升,这种实现方式就无法很好地工作,因为上下文的切换回变得很明显,这是同步阻塞方式的低扩展性劣势
NIO引入多路复用机制,实现上述功能.要点如下:
1.首先,通过Selector.open()创建一个selector,作为调度员
2.然后创建ServerSocketChannel,并且向Selector注册,通过指定SELECTKey.OP_ACCEPT,告诉调度员,它关注的是新的连接请求,因为阻塞模式下注册操作不允许(抛出IllegalBlockingModeException).故将其配置为非阻塞模式。
3.Selector阻塞在selector操作,当有Channel发生接入请求,就会被唤醒
4.sayHelloWorld方法中,通过SocketChannel和Buffer进行数据操作,在本例中发送一段字符串
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();) {          client.write(Charset.defaultCharset().encode("Hello world!"));
        }
    }
    public static void main(String[] args) throws IOException {        
	NIOServer server = new NIOServer();        
	server.start();        
	try (Socket client = new Socket(InetAddress.getLocalHost(), server.getPort())) {
            BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(client.getInputStream()));
            bufferedReader.lines().forEach(s -> System.out.println(s));
        }catch(Exception e){
		
		}finally{
				if(null != bufferedReader)
					bufferedReader.close();
		}
	}
}


5.总结
可以看到,前面两个例子,IO都是同步阻塞模式,所以需要多线程以实现多任务处理。而NIO利用单线程轮询事件机制,通过高效定位就绪Channel
来决定做什么,仅仅在select阶段进行阻塞,其他情况是不阻塞的,有效避免大量客户连接时,频繁线程切换带来的问题,应用能力大幅提高
NIO图示如下:
 ![Alt]![单线程通过selectkeys对应就绪事件,只注册select阶段,避免大量连接情况下,线程频繁切换代理的开销](https://img-blog.csdnimg.cn/20200225132214942.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L2R5bWtrag==,size_16,color_FFFFFF,t_70)

Java7 NIO

java7 引入的NIO2 中,有添加了一种额外的异步IO操作,利用事件和回调,处理Accept,Read等操作。实例如下
1.基本抽象先死,AysnchornousServerScoketChannel对应上面面例子ServerSocketChannel,AsynchronousSocketChannel对应SocketChannel
2.业务逻辑关键在于,通过指定CompletionHandler回调接口,在accept/read/write等关键节点,通过事件机制调用。

AsynchronousServerSocketChannel serverSock = AsynchronousServerSocketChannel.open().bind(sockAddr);

//为异步操作指定CompletionHandler回调函数
serverSock.accept(serverSock, new CompletionHandler<>() { 
    @Override
    public void completed(AsynchronousSocketChannel sockChannel, AsynchronousServerSocketChannel serverSock) {
        serverSock.accept(serverSock, this);
        // 另外一个 write(sock,CompletionHandler{})
        sayHelloWorld(sockChannel, Charset.defaultCharset().encode
                ("Hello World!"));
    }
  // 省略其他路径处理方法...
});
发布了150 篇原创文章 · 获赞 15 · 访问量 10万+

猜你喜欢

转载自blog.csdn.net/dymkkj/article/details/104495758