java 的 IO、 线程池、NIO与多路复用

前言

什么是IO ?

IO就是输入输出的意思,是操作数据的接口总称呼

什么是传统的BIO ?


传统的BIO 在java.io包下,是基于流模型实现的,比如:File类,输入输出流操作之类的;他们是以同步、阻塞的方式进行读出或写入操作的,当一个线程进行IO操作时,会在读写完成之前,一直阻塞在哪里,多个请求时他们可以可靠的按照线性顺序执行。
java.io包的好处是代码比较简单,直观,缺点是IO效率和扩展性存在局限,成为性能瓶颈。
java.net下面提供的部分网络API,比如Socket、ServerSocket、HttpURLConnection也归类到同步阻塞IO类库。


什么是NIO ?


java1.4引入了NIO框架(java.io包),提供了Channel,Selector,Buffer等新的抽象,可以构建多路复用的、同步非阻塞IO程序,,提供了更接近操作系统底层的高性能数据操作方式
3.java7中,NIO有了进一步的改进,也就是NIO2,引入了异步非阻塞IO方式,也有很多人叫它AIO(Asynchronous IO)。异步IO操作基于事件和回调机制,可以理解为,应用操作直接返回,而不会阻塞在那里,当后台处理完成,操作系统会通知相应线程进行后续工作

在面试中,考察的点涉及方方面面

基础API功能和设计,InputStream/OutputStream和Reader/writer的关系和区别(BIO)
NIO AIO的基本组成
给定场景,分别用不同模型实现,分析BIO、NIO等模式的设计和实现原理
NIO提供的高性能数据操作方式是基于什么原理,如何使用?
或者,从开发者的角度来看,你觉得NIO自身实现存在哪些问题?有什么改进的想法吗?
区分同步和异步?
同步是一种可靠的有序运行机制,当我们进行同步操作时,后续的任务是等待当前调用返回,才会进行下一步,而异步则相反,其他任务不需要等待当前调用返回,通常依靠事件、回调等机制来实现任务间次序关系

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

我们不能说同步和阻塞就是低效,分情况而定。

对于java.io的总结:

IO不仅仅是对文件的操作,网络编程中,比如Socket通信,都是典型的IO 操作目标
输入流,输出流是用于读取或写入字节的,操作图片文件
Reader/Writer则是用于操作字符,增加了字符编解码等功能,适用于类似从文件中读取或者写入文本信息。本质上计算机操作的都是字节,不管是网络通信还是文件读取,相当于构建了应用逻辑和原始数据之间的桥梁
BufferdOutputStream等缓冲区的实现,可以避免频繁的磁盘读写,进而提高IO处理效率,这种设计利用了缓冲区,将批量数据进行一次操作,使用中不能忘了flush
很多IO工具类都实现了Closeable接口,因为需要进行资源的释放。比如:打开FilelnputStream,它就会获取相应的文件描述符(FileDescriptor),需要try-with-resources、try-finally等机制保证FilelnputStream被明确关闭,进而相应文件描述也会失效,否则将导致资源无法被释放
JAVA NIO
NIO的组成部分:
1.Buffer,高效的数据容器,除了布尔类型,所有原始数据类型都有相应的Buffer实现
2.Channel,文件描述符,是NIO中被用来支持批量式IO操作的一种抽象。file 、Socket ,被认为是比较高层次的抽象,而 channel则是更加操作系统底层的一种抽象。我们可以通过Socket获取Channel。
4.Selector,是NIO实现多复用的基础,他提供了一种高效的机制,可以检测到注册在Selector的多个Channel中,是否有Channel处于就绪状态,进而实现单线程对多CHANNNEL的高效管理

NIO解决的问题
为什么需要NIO,为什么需要多路复用?
场景:我们需要实现一个服务器应用,简单要求能够同时服务多个客户端请求


package DemoClient;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;

public class DemoServer  extends Thread{
    private ServerSocket serverSocket;
    public int getPort(){
        return serverSocket.getLocalPort();//相当于一个平常的成员变量
    }

    public void run(){
        try{
            serverSocket =new ServerSocket(0);//服务器端启动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();//服务器开启
        /**
         * 利用Scoket模拟一个简单的客户端,只进行连接、读取、打印
         */
        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));
        }
    }
    
    
    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();
            }
        }
    }
}


线程实现比较重量级,启动和销毁一个线程有明显开销,每个线程都有单独的线程栈结构 ,占用非常明显的内存,所以,每一个Client启动一个线程都有点浪费

所以,我们可以修正问题,引入线程池机制来避免浪费

通过一个固定大小的线程池,来负责管理工作线程,避免频繁创建、销毁线程的开销,这是构建并发服务的典型方式
如果只有几百个连接的普通应用,这种模式往往可以工作很好。但是如果数量急剧上升,这种实现方式就无法很好工作,因为线程上下文切换开销会在高并发时变得很明显,这是同步阻塞方式的低扩展性劣势

NIO的多路复用机制

NIO引用的多路复用机制,提供了另外一种思路

package DemoServerClient;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.nio.charset.Charset;
import java.util.Iterator;
import java.util.Set;

public class NIOServer  extends Thread{
    /**NIO的组成1.Buffer   2.channel 多线程并发抽象概念  3.Selector筛选器
     * 同步非阻塞IO框架
     */
    public void run(){
        try(Selector selector =Selector.open();//通过Selector.open()创建一个Selector,作为一个类似调度员的角色
            /**通过创建一个ServerSocketChannnel,并且向Selector注册,通过指定SelectionKey.OP_ACCEPT,
             * 告诉调度员,它关注的是新的连接请求
             * 为什么我们要明确配置非阻塞模式呢?因为在阻塞模式下,注册操作是不允许的
             */
            ServerSocketChannel serverSocket =ServerSocketChannel.open();){//创建channel selector
            serverSocket.bind(new InetSocketAddress(InetAddress.getLocalHost(),8888));//创建连接端口,监听
            serverSocket.configureBlocking(false);
            //注册到Selector,并说明关注点
            serverSocket.register(selector, SelectionKey.OP_ACCEPT);
            while(true){
                /**Selector阻塞在select操作,当有Channel发生接入请求后,就会被唤醒
                 */
                selector.select();//阻塞等待就绪的Channel,这是关键点之一
                Set <SelectionKey> selectionKeys =selector.selectedKeys();//建一个selector的HashSet
                Iterator <SelectionKey> iter =selectionKeys.iterator();//迭代器迭代 selectionKey的Set
                while(iter.hasNext()){
                    SelectionKey key =iter.next();
                    //生产系统中一般会额外进行就绪状态检查
                    sayHelloWorld((ServerSocketChannel)key.channel());//强制类型转换,找出那个channel
                }

            }
        }catch (Exception e){
            e.printStackTrace();
        }
    }

    /**
     *在此方法中,通过ScoketChannel和Buffer进行数据操作,发送一段字符串
     * @param server
     * @throws IOException
     */
    private void sayHelloWorld(ServerSocketChannel server) throws IOException{
        try(SocketChannel client =server.accept();){//客户端只管发送请求就ok
            client.write(Charset.defaultCharset().encode("helloworld!"));
        }
    }

}


IO是同步阻塞模式,需要多线程实现多任务处理
NIO利用单线程轮询时间的机制,高效定位就绪的Channel,来决定做什么,仅仅select阶段是阻塞的,可以避免大量客户连接时,频繁切换线程带来的问题

异步非阻塞AIO
java7引入NIO2(AIO)时,又增添了一种额外的异步IO模式,利用事件和回调,处理Accept,Read等操作
AsychronousServerSocketChannel对应于上面例子的SeverSocketChannel;
AsychronousSocketChannel对应于SocketChannel
业务逻辑的关键点在于,通过指定的CompletionHandler回调接口,在accept/read/write等关键节点,通过事件机制调用,实现异步

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

会飞的小蜗

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值