java提供的IO方式

根据IO的抽象模型,通常分为BIO,NIO,AIO三种

区分同步和异步(Synchronous/asynchronous):同步是一种比较可靠的运行方式,当进行同步操作时,后续的任务等待当前的调用操作返回之后才往下进行。而异步机制不需要等待当前调用操作返回,通常依赖事件、回调机制来实现任务之间的先后顺序关系。

区分阻塞和非阻塞blocking/non-blocking。当进行阻塞操作时,当前线程会运行在阻塞状态(线程共有五种状态:新建New,运行Running,就绪Runnable,阻塞blocked, 死亡Dead),无法处理其他任务,只有当条件就绪之后,才能继续下去。而非阻塞不管IO操作是否完成,直接返回,后续的操作在后台继续处理。

BIO

BIO是阻塞IO,交互方式是同步、阻塞的方式,在读取输入流或者写入输出流的时候,在读写动作完成之前,线程会一致阻塞在那里,他们之间的关系是线性顺序的。

传统的java.io包就是这种方式,它采用流模型是先,提供了常用的输入输出流。java.io包的好处就是代码简单、看起来直观,缺点是IO的效率不高,扩展性也不好。

通常情况下,把java.net下面提供的有些网络API,比如Socket,ServerSocket、HttpUELConnection也划分到同步阻塞IO类库里面,因为网络通信也同样是IO行为。

NIO

在java1.4的时候引入了NIO框架(java.nio包),提供了Channel、Selector,Buffer这类新的抽象,写出来的程序可以使多路复用,同步非阻塞的。

NIO的主要组成部分:

  • Buffer:一个高效率的数据容器,除了布尔类型,其他的原始数据类型都有相应的Buffer实现。(原始数据类型包括 逻辑类型Boolean,文本类型char,整数类型byte,short,int,long。浮点型double,float。 )
  • Channel:是NIO中用来支持批量IO操作的一种抽象结构,类似于Linux系统中的文件描述符。Socket是更高层次的抽象,而channel是更底层的抽象,我们可以通过Socket来获取Channel。
  • Selector:是NIO实现多路复用的基础,可以检测注册到Selector上的多个Channel中,有没有Channel处于就绪状态,这样就实现了单线程对多Channel的高效管理。
  • Charset,提供Unicode字符串定义,IO也提供了对象的编码解码器,比如可以通过下面的方式进行字符串到ByteBuffer的转换: Charset.defaultCharset().encode("Hello world!");

NIO能解决什么问题?
下面举一个具体场景,假如要求实现一个服务器应用,要求能够同时服务多个客户端请求。先使用java.io和java.net中的同步、阻塞API简单实现。

public class DemoServer extends Thread {
    private ServerSocket serverSocket;
    public int getPort() {
        return serverSocket.getLocalPort();
    }
    try {
        serverSocket = new ServerSocket(0);
        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())) {
			BufferReader bufferReader = new BufferReader(new InputStreamReader(client.getInputStream));
			buferReader.lines().forEach(s -> System.out.print(s));
		} 
	}
}
//简化实现,不做读取操作,直接发送字符串
class RequestHandler extends Thread {
    private Socket socket;
    ReqestHandler(Socket socket) {
		this.socket = socket;
	}
	@Override
	public void run() {
		try(PrintWriter out = new PrintWriter(socket.getOutputStream());) {
			out.println("Hello world!");
			out.flush();
		} catch(IOExceptiojn e) {
			e.printStackTrace();
		}
	}
}

上面的程序几个要点:

  • 服务器端启动ServerScoket,,端口0自动绑定一个空闲端口
  • 调用accept方法,阻塞等待客户端连接
  • 利用Socket模拟一个简单的客户端 ,只进行连接、读取,打印
  • 当连接建立之后,启动一个单独的线程负责客户端的请求。
    这个解决方案,在扩展性方面嘛有问题,由于java的线程实现是比较重量级的操作,启动销毁一个线程有明显的开销。未来解决这个问题,可以引入线程池的机制
serverSocket = new ServerSocket(0);
executor = Executor.newFixedThreadPool(8);
while(true) {
	Socket socket = serverSocket.accept();
	RequestHandler requestHandler = new RequestHandler(socket);
	executor.exetute(requestHandler);
}

这样通用化一个固定大小的线程池,来管理线程,避免线程的频繁创建和销毁,这也是构建并发服务的一个典型方式。如果连接不是很多,比如只有几百个连接的普通应用,这种模式可以很好地工作。但是如果连接数极速上升,在高并发情况下,线程的上下文切换开销就会比较大,这时候同步阻塞方式就遇到了瓶颈了。
NIO提供的多路复用机制提供了另一种解决方案。

public class NIOServer extends Thread {
	public void run() {
		try(Selector selector = Selector.open();
			ServerSocketChannel serverSocket = ServerSocketChannel.open();) {
			serverSocket.bind(new InetSocketAddress(InetAddress.getLocalHost(), 8888));
			serverSocket.configuerBlocking(false);
			//注册到Selector,并说明关注点
			serverSocket.register(selector, SelectionKey.OP_ACCEPT);
			while(true) {
				selector.select();//阻塞就绪的Chanel,这是关键点之一
				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();
		}
	}
	public void sayHelloWorld(ServerSocketChannel server) throws IOException{
		try(SocketChannel client = server.accept();) {
			client.write(Charset.defaultCharset().encode("Hello world"););
		}
	}
	//main函数与前面类似,暂时省略
}

在上面的代码中,有几个主要步骤如下:

  • 首先,通过Selector.open()创建一个Selector,角色类似于调度员
  • 然后,创建一个ServerSocketChannel,并向Selector注册,通过指定SelectionKey.OP_ACCEPT,同时调度员,它关注的是新的连接请求。 为什么在这里明确要求配置非阻塞模式?因为在阻塞模式下,注册操作时不允许的,会抛出IllegalBlockingModeException异常。
  • Selector阻塞在select操作,当有Channel发生接入请求,就会被唤醒。
  • 在sayHelloWorld方法中,通过SocketChannel和Buffer进行数据操作,在上面代码中是发送了一段字符串

当IO处于同步阻塞模式的时候,需要多线程实现多任务管理,而处于NIO模式的时候,可以通过单线程轮询,高效地找到就绪的Channel,来决定做什么,仅仅在select阶段是阻塞的,这样可以避免大量客户端连接的时候,由于频繁的线程切换带来的性能问题,可以提升应用的扩展能力

AIO

在java7当中,NIO引入了异步非阻塞IO方式,也称为AIO(Asynchronous IO)。异步IO操作基于事件和回调机制,可以处理Read,Accept之类的操作,可以理解为应用操作直接返回,而不用阻塞在那里等待,当后台处理完成,操作系统会通知相应线程完成后续的工作。

AsynchronousServerSocketChannel serverSock = AsynchronousServerSocketChannel.open().bind(sockAddr);
serverSock.accept(serverSock, new CompletionHandler<>() {
	//为异步操作指定CompletionHandler回调函数
	@override
	public void completed(AsynchronousServerSocketChannel  sockChannel,
										AsynchronousServerSocketChannel  serverSock) {
			serverSock.accept(serverSock, this);
			sayHelloWorld(sockChannel, Charset.defaultCharset().encode("Hello World!"););
	}
});

猜你喜欢

转载自blog.csdn.net/shida_hu/article/details/83720338