010.NIO、BIO、AIO,NIO如何实现多路复用!
一、同步阻塞I/O(BIO):
同步阻塞I/O,服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销,可以通过线程池机制来改善,在读取输入流或者写入输出流是,在读写动作完成之前,线程会一直阻塞在哪,他们之间的调用时可靠的先行顺序。
BIO方式适用于连接数目比较小且固定的架构,这种方式对服务端资源要求比较高,并发局限于应用中,在jdk1.4以前是唯一的io,但程序直观简单易理解。
很多时候,人们也把 http://java.net下面提供的部分网络API,比如 Socket、 Serversocket、 HttpURLConnection也归类到同步阻塞IO类库,因为网络通信IO行为
二、同步非阻塞I/O(NIO):
同步非阻塞I/O,提供了Channel、Selector、Buffer等新的抽象,可以构建多路复用的、同步非阻塞 IO 程序,同时提供了更接近操作系统底层的高性能数据操作方式。服务器实现模式为一个请求一个线程,即客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有IO请求时才启动一个线程进行处理。
NIO方式适用于连接数目多且连接比较短(轻操作)的架构,比如聊天服务器,并发局限于应用中,编程比较复杂,jdk1,4开始支持。
三、异步非阻塞I/O(AIO):
异步非阻塞I/O,异步IO操作基于事件和回调机制,可以简单理解为,应用操作直接返回,而不会阻塞在那里,当后台处理完成,操作系统会通知相应线程进行后续工作。服务器实现模式为一个有效请求一个线程,客户端的IO请求都是由操作系统先完成了再通知服务器用其启动线程进行处理。
AIO方式适用于连接数目多且连接比较长(重操作)的架构,比如相册服务器,充分调用OS参与并发操作,编程比较复杂,jdk1.7开始支持。
四、IO与NIO区别:
- IO面向流,NIO面向缓冲区
- IO的各种流是阻塞的,NIO是非阻塞模式
- Java NIO的选择允许一个单独的线程来监视多个输入通道,可以注册多个通道使用一个选择器,然后使用一个单独的线程来“选择”通道:这些通道里已经有可以处理的输入或选择已准备写入的通道。这种选择机制,使得一个单独的线程很容易来管理多个通道
五、同步与异步的区别:
- 同步:发送一个请求,等待返回,再发送下一个请求,同步可以避免出现死锁,脏读的发生
- 异步:发送一个请求,不等待返回,随时可以再发送下一个请求,可以提高效率,保证并发
简单来说,同步是一种可靠的有序运行机制,当我们进行同步操作时,后续的任务是等待当前调用返回,才会进行下而异步则相反,其他任务不需要等待当前调用返回,通常依靠件、回调等机制来实现任务间次序关系
同步异步关注点在于消息通信机制,
阻塞与非阻塞关注的是程序在等待调用结果时(消息、返回值)的状态:
- 阻塞调用是指调用结果返回之前,当前线程会被挂起。调用线程只有在得到结果之后才会返回。
- 非阻塞调用指在不能立刻得到结果之前,该调用不会阻塞当前线程
I/O Streams (The Java™ Tutorials > Essential Classes > Basic I/O) (oracle.com)
不同层次:
CPU层次:操作系统进行IO或任务调度层次,现代操作系统通常使用异步非阻塞方式进行IO(有少部分IO可能会使用同步非阻塞),即发出IO请求后,并不等待IO操作完成,而是继续执行接下来的指令(非阻塞),IO操作和CPU指令互不干扰(异步),最后通过中断的方式通知IO操作的完成结果。
线程层次:操作系统调度单元的层次,操作系统为了减轻程序员的思考负担,将底层的异步非阻塞的IO方式进行封装,把相关系统调用(如read和write)以同步的方式展现出来,然而同步阻塞IO会使线程挂起,同步非阻塞IO会消耗CPU资源在轮询上,3个解决方法;
- 多线程(同步阻塞)
- IO多路复用(select、poll、epoll)
- 直接暴露出异步的IO接口,kernel-aio和IOCP(异步非阻塞)
Linux IO模型:
阻塞/非阻塞:等待I/O完成的方式,阻塞要求用户程序停止执行,直到IO完成,而非阻塞在IO完成之前还可以继续执行
同步/异步:获知IO完成的方式,同步需要时刻关心IO是否完成,异步无需主动关心,在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 机制作为资源释放的最后保障,也是必要的。
final 、finally finalize 有什么不同? (qq.com)
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。
jdk/jdk: d8327f838b88 src/java.base/linux/classes/sun/nio/ch/EPollSelectorImpl.java
jdk/jdk: d8327f838b88 src/java.base/windows/classes/sun/nio/ch/Iocp.java
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);
}
通过一个固定大小的线程池,来负责管理工作线程,避免频繁创建,销毁线程的开销。
试想,如果连接数并不是特别多,只有几百个连接,这种模式可以很好的工作。但是如果连接数急剧上升,这种实现就无法很好的工作,因为线程上下文切换开销会在高并发时变得很明显。
如果连接数并不是非常多,只有最多几百个连接的普通应用,这种模式往往可以工作的很好。但是,如果连接数量急剧上升,这种实现方式就无法很好地工作了,因为线程上下文切换开销会在高并发时变得很明显,这是同步阻塞方式的低扩展性劣势。
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;
}
这样做的好处:
- 首先,通过 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) {
}
});