传统同步阻塞I/O模型:
{ ExecutorService executor = Excutors.newFixedThreadPollExecutor(100);//线程池 ServerSocket serverSocket = new ServerSocket(); serverSocket.bind(8088); while(!Thread.currentThread.isInturrupted()){//主线程死循环等待新连接到来 Socket socket = serverSocket.accept(); executor.submit(new ConnectIOnHandler(socket));//为新的连接创建新的线程 } class ConnectIOnHandler extends Thread{ private Socket socket; public ConnectIOnHandler(Socket socket){ this.socket = socket; } public void run(){ while(!Thread.currentThread.isInturrupted()&&!socket.isClosed()){//死循环处理读写事件 String someThing = socket.read()....//读取数据 if(someThing!=null){ ......//处理数据 socket.write()....//写数据 } } } }
之所以使用多线程,主要原因在于socket.accept()、socket.read()、socket.write()三个主要函数都是同步阻塞的,当一个连接在处理I/O的时候,系统是阻塞的。
如果是单线程的话必然就挂死在那里,但CPU是被释放出来的,开启多线程,就可以让CPU去处理更多的事情。
这个模型最本质的问题在于,严重依赖于线程。但线程是很"贵"的资源,主要表现在:
线程的创建和销毁成本很高,在Linux这样的操作系统中,线程本质上就是一个进程。创建和销毁都是重量级的系统函数。
线程本身占用较大内存,像Java的线程栈,一般至少分配512K~1M的空间,如果系统中的线程数过千,恐怕整个JVM的内存都会被吃掉一半。
线程的切换成本是很高的。操作系统发生线程切换的时候,需要保留线程的上下文,然后执行系统调用。如果线程数过高,可能执行线程切换的时间甚至会大于线程执行的时间,这时候带来的表现往往是系统load偏高、CPU sy使用率特别高(超过20%以上),导致系统几乎陷入不可用的状态。
容易造成锯齿状的系统负载。因为系统负载是用活动线程数或CPU核心数,一旦线程数量高但外部网络环境不是很稳定,就很容易造成大量请求的结果同时返回,激活大量阻塞线程从而使系统负载压力过大。
所以,当面对十万甚至百万级连接的时候,传统的BIO模型是无能为力的。
NIO简介:
Java NIO 是 java 1.4, 之后新出的一套IO接口NIO中的N可以理解为Non-blocking,不单纯是New。
NIO的特性/NIO与IO区别:
IO是面向流的,NIO是面向缓冲区的;
IO流是阻塞的,NIO流是不阻塞的;
NIO有选择器,而IO没有。
读数据和写数据方式:
从通道进行数据读取 :创建一个缓冲区,然后请求通道读取数据。
从通道进行数据写入 :创建一个缓冲区,填充数据,并要求通道写入数据。
其中Java最早提供的blocking I/O即是阻塞I/O,而NIO即是非阻塞I/O,同时通过NIO实现的Reactor模式即是I/O复用模型的实现,通过AIO实现的Proactor模式即是异步I/O模型的实现。
面向流 vs. 面向缓冲
Java IO是面向流的,每次从流(InputStream/OutputStream)中读一个或多个字节,直到读取完所有字节,它们没有被缓存在任何地方。另外,它不能前后移动流中的数据,如需前后移动处理,需要先将其缓存至一个缓冲区。
Java NIO面向缓冲,数据会被读取到一个缓冲区,需要时可以在缓冲区中前后移动处理,这增加了处理过程的灵活性。但与此同时在处理缓冲区前需要检查该缓冲区中是否包含有所需要处理的数据,并需要确保更多数据读入缓冲区时,不会覆盖缓冲区内尚未处理的数据。
参考:https://www.cnblogs.com/jasongj/p/5797863.html
NIO(Non-blocking I/O):
是一种同步非阻塞的I/O模型。
NIO主要有三大核心部分:Channel(通道),Buffer(缓冲区), Selector。
传统IO基于字节流和字符流进行操作,而NIO基于Channel和Buffer(缓冲区)进行操作,数据总是从通道读取到缓冲区中,或者从缓冲区写入到通道中。
Selector(选择区)用于监听多个通道的事件(比如:连接打开,数据到达)。因此,单个线程可以监听多个数据通道。
NIO和传统BIO之间第一个最大的区别是,BIO是面向流的,NIO是面向缓冲区的。
BIO面向流意味着每次从流中读一个或多个字节,直至读取所有字节,它们没有被缓存在任何地方。
BIO的各种流是阻塞的。这意味着,当一个线程调用read() 或 write()时,该线程被阻塞,直到有一些数据被读取,或数据完全写入。该线程在此期间不能再干任何事情了。
NIO的非阻塞,使一个线程从某通道发送请求读取数据,但是它仅能得到目前可用的数据,如果目前没有数据可用时,就什么都不会获取。而不是保持线程阻塞,所以直至数据变的可以读取之前,该线程可以继续做其他的事情。
线程通常将非阻塞IO的空闲时间用于在其它通道上执行IO操作,所以一个单独的线程现在可以管理多个输入和输出通道(channel)。
Channel:
Channel和BIO中的Stream(流)是差不多一个等级的。只不过Stream是单向的,比如:InputStream, OutputStream.而Channel是双向的。
NIO中的Channel的主要实现有:FileChannel,DatagramChannel,SocketChannel,ServerSocketChannel。
详解:https://zhuanlan.zhihu.com/p/27365009
Buffer:
NIO中的关键Buffer实现有:ByteBuffer, CharBuffer, DoubleBuffer, FloatBuffer, IntBuffer, LongBuffer, ShortBuffer,分别对应基本数据类型: byte, char, double, float, int, long, short。
缓冲区实质上是一个数组。通常它是一个字节数组,但是也可以使用其他种类的数组。
详解:https://zhuanlan.zhihu.com/p/27296046
Selector:
Selector运行单线程处理多个Channel,如果你的应用打开了多个通道,但每个连接的流量都很低,使用Selector就会很方便。
例如在一个聊天服务器中。要使用Selector, 得向Selector注册Channel,然后调用它的select()方法。这个方法会一直阻塞到某个注册的通道有事件就绪。
一旦这个方法返回,线程就可以处理这些事件,事件的例子有如新的连接进来、数据接收等。
详解:https://zhuanlan.zhihu.com/p/27434028
读取文件涉及三个步骤:
(1) 从 FileInputStream 获取 Channel(也可以用RandomAccessFile),(2) 创建 Buffer,(3) 将数据从 Channel 读到 Buffer中。
1 public static void method1(){ 2 RandomAccessFile aFile = null; 3 try{ 4 aFile = new RandomAccessFile("src/nio.txt","rw"); 5 FileChannel fileChannel = aFile.getChannel(); 6 ByteBuffer buf = ByteBuffer.allocate(1024); 7 8 int bytesRead = fileChannel.read(buf); 9 System.out.println(bytesRead); 10 11 while(bytesRead != -1) 12 { 13 buf.flip(); 14 while(buf.hasRemaining()) 15 { 16 System.out.print((char)buf.get()); 17 } 18 19 buf.compact(); 20 bytesRead = fileChannel.read(buf); 21 } 22 }catch (IOException e){ 23 e.printStackTrace(); 24 }finally{ 25 try{ 26 if(aFile != null){ 27 aFile.close(); 28 } 29 }catch (IOException e){ 30 e.printStackTrace(); 31 } 32 } 33 }
NIO事件模型:
NIO的主要事件有几个:读就绪、写就绪、有新连接到来。
我们首先需要注册当这几个事件到来的时候所对应的处理器。然后在合适的时机告诉事件选择器:我对这个事件感兴趣。
新事件到来的时候,会在selector上注册标记位,标示可读、可写或者有连接到来。
select是阻塞的,无论是通过操作系统的通知(epoll)还是不停的轮询(select,poll),这个函数是阻塞的。
1 interface ChannelHandler{ 2 void channelReadable(Channel channel); 3 void channelWritable(Channel channel); 4 } 5 class Channel{ 6 Socket socket; 7 Event event;//读,写或者连接 8 } 9 10 //IO线程主循环: 11 class IoThread extends Thread{ 12 public void run(){ 13 Channel channel; 14 while(channel=Selector.select()){//选择就绪的事件和对应的连接 15 if(channel.event==accept){ 16 registerNewChannelHandler(channel);//如果是新连接,则注册一个新的读写处理器 17 } 18 if(channel.event==write){ 19 getChannelHandler(channel).channelWritable(channel);//如果可以写,则执行写事件 20 } 21 if(channel.event==read){ 22 getChannelHandler(channel).channelReadable(channel);//如果可以读,则执行读事件 23 } 24 } 25 } 26 Map<Channel,ChannelHandler> handlerMap;//所有channel的对应事件处理器 27 }