Java 提供的一些IO方式

       传统的java.io包基于流模型实现,提供了一些 如File抽象,输入输出流等基本功能,交互方式是同步、阻塞的方式(Blocking IO)。也就是说,在读取输入流或者写入输出流时,在读、写操作完成之前,线程会一直阻塞在那里,他们之间的调用是可靠的线性顺序。传统的Java.io的优势在于简单直观,但IO效率不高,其扩展性受限,很容易成为项目的瓶颈。
       另外,Java.net中的一些网络API也可以归结到同步阻塞的IO类库,比如Socket、ServerSocket、HttpURLConnection。因为网络通信同样也是IO行为。
       在Java1.4版本中引入了NIO框架(java.nio),提供了Channel、Buffer、Selector等新的抽象,可以用以构建多路复用、同步非阻塞的IO程序
       在Java7中的NIO2(AIO)框架引入了异步、非阻塞的交互方式,异步IO操作基于事件和回调机制,应用操作直接返回而不会阻塞在那里,当后台处理完后,操作系统会通知相应的线程进行后续工作。

  • java.io不仅仅是对文件的操作,网络通信中,比如Socket通信,都是典型的IO操作的目标。

  • 输入流/输出流(InputStream/OutputStream):是用来读取或者写入字节的,例如操作图片文件。

  • Reader/Writer:是用来操作字符的,增加了字符的编解码功能,适用于从文件中读取或者写入文本信息。本质上计算机操作的都是字节,不管是文件读取还是网络通信,Reader和Writer构建了应用逻辑和原始数据之间的桥梁。

  • 带缓冲区的读写操作,例如BufferedInputStream、BufferedOutputStream,可以避免频繁的磁盘读写,这种设计利用了缓冲区,一次操作批量数据。在使用中千万不能忘了flash

  • 参考下面简化类图,很多IO工具类都实现了Closeable接口,对资源进行释放。
    在这里插入图片描述

实例:实现一个服务器应用,简单要求:能够为多个客户端提供服务。

  • 使用java.io和java.net中的同步、阻塞式API来简单实现
    1.服务端启动一个ServerSocket,端口0表示自动绑定一个空闲的端口。
    2.调用accept方法,阻塞等待客户端连接。一旦连接成功,服务端的accept会返回一个负责传输数据的已连接socket。(一般,连接建立成功后,双方就可以开始通过read和write函数来读写数据)
    3.连接建立成功后,启动一个单独线程回复客户端的请求。
    4.利用Socket模拟一个简单的客户端,只进行连接、读取、打印。
    相关模拟代码:
package Test;

import java.io.*;
import java.net.*;

public class DemoServer extends Thread {
    private ServerSocket serverSocket;
    public int getPort() {
        return  serverSocket.getLocalPort();
    }
    public void run() {
        try {
            //服务端启动一个ServerSocket,端口0表示自动绑定一个空闲的端口
            //这里的ServerSocket是一个监听的socket
            serverSocket = new ServerSocket(0);
            while (true) {
                //调用accept方法,阻塞等待客户端连接
                //这里的Socket是一个用于连接的socket,负责传输数据
                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();
        //利用socket模拟一个简单的客户端,只进行连接、读取、打印
        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));
        }
    }
}


//响应客户端请求的线程
package Test;

import java.io.PrintWriter;
import java.net.Socket;

// 简化实现,不做读取,直接发送字符串
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();
        }
    }
}

使用简单的java.io与java.net实现的同步、阻塞式API存在的潜在问题:Java目前的线程实现是比较重量级的,创建或者销毁一个线程有明显的开销,每个线程都有单独的线程栈结构,需要占用比较明显的内存,所以,每一个client启动一个线程,挺浪费的。

解决方案:引入线程池来避免浪费——通过一个固定大小的线程池,来负责管理响应客户端请求的工作线程,避免频繁创建、销毁线程的开销,这是构建并发服务的典型。参考下图进行理解:
在这里插入图片描述
相关代码:

package Test;

import java.io.*;
import java.net.*;
import java.util.concurrent.*;

public class DemoServer extends Thread {
    private ServerSocket serverSocket;
    public int getPort() {
        return  serverSocket.getLocalPort();
    }
    public void run() {
        try {
            //服务端启动一个ServerSocket,端口0表示自动绑定一个空闲的端口
            //这里的ServerSocket是一个监听的socket
            serverSocket = new ServerSocket(0);
            //创建一个指定工作数量的线程池
            ExecutorService executor = Executors.newFixedThreadPool(8);
            while (true) {
                //调用accept方法,阻塞等待客户端连接
                // 这里的Socket是一个用于连接的socket,负责传输数据
                Socket socket = serverSocket.accept();
                //创建一个响应客户端的请求的工作线程提交到线程池中 
                RequestHandler requestHandler = new RequestHandler(socket);
                executor.execute(requestHandler);
            }
        } 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 {
        //服务端启动ServerSocket
        DemoServer server = new DemoServer();
        server.start();
        //利用socket模拟一个简单的客户端,只进行连接、读取、打印
        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));
        }
    }
}

线程池解决 重量级线程创建与销毁的开销大 的方法只在普通应用——仅几百个连接的情况下,可以工作得很好。一旦连接数量变得超级多,线程上下文切换的开销会在高并发是变得非常明显,这是同步阻塞方式的劣势——低扩展性。

前两个示例,IO都是同步阻塞模式,需要利用多线程来实现多任务处理。而NIO多路复用利用了单线程轮询事件的机制,通过定位就绪的Channel来决定做什么,仅仅在select阶段是阻塞的,可以有效避免当大量的客户端连接时,频繁的线程切换带来的开销。如下图所示。
在这里插入图片描述

  • NIO引入多路复用机制
    1.通过Selector.open()创建一个selector,作为类似调度员的角色。
    2.通过ServerSocketChannel.open()创建一个Channel,明确配置其为非阻塞模式。
    3.Selector阻塞在select操作,当有Channel发生连接请求时,Selector被唤醒。
    4.通过SocketChannel和Buffer进行数据操作,发送字符串。

相关代码:

package Test;

import java.io.*;
import java.net.*;
import java.nio.channels.*;
import java.nio.charset.Charset;
import java.util.*;

public class NIOServer extends Thread {
    public void run() {
        // 通过Selector.open()创建一个selector,作为类似调度员的角色
        //通过ServerSocketChannel.open()创建一个channel
        try (Selector selector = Selector.open();
             ServerSocketChannel serverSocket = ServerSocketChannel.open();) {
            //绑定ip和端口
            serverSocket.bind(new InetSocketAddress(InetAddress.getLocalHost(), 8888));
            //需要明确配置是 非阻塞模式,因为在阻塞模式下,注册操作是不允许的,会抛出IllegalBlockingModeException异常
            serverSocket.configureBlocking(false);
            // 注册到 Selector,并指定SelectionKey.OP_ACCEPT,告诉Selector,它关注的是最新连接请求
            serverSocket.register(selector, SelectionKey.OP_ACCEPT);
            while (true) {
                //Selector阻塞在select()请求,当有Channel发生连接请求,就会被唤醒
                // 阻塞等待就绪的 Channel,这是关键点之一
                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();
        }
    }

    //通过SocketChannel和Buffer进行数据操作,发送字符串
    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 nioserver = new NIOServer();
        nioserver.start();
        //利用socket模拟一个简单的客户端,只进行连接、读取、打印
        try (Socket client = new Socket(InetAddress.getLocalHost(), 8888)) {
            BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(client.getInputStream()));
            bufferedReader.lines().forEach(s -> System.out.println(s));
        }
    }
}


参考:
Java核心技术
Java BIO|NIO|AIO的学习
Java常用的线程池的几种比较
并发和并行的区别

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值