java高并发程序设计学习笔记八BIO、NIO和AIO

BIO:

Blocking I/O,传统的同步阻塞式网络编程;

网络编程的基本模型是C/S模型,即两个进程间的通信;服务端提供IP和监听端口,客户端通过连接向服务端发起连接请求,通过三次握手连接,若成功则双方通过套接字进行通信;

采用BIO通信模型的服务端,通常由一个独立的Acceptor线程负责监听客户端的连接,它接收到客户端连接请求之后为每个客户端创建一个新的线程,

进行链路处理,处理完成后,通过输出流返回应答给客户端,线程销毁。即典型的一请求一应答通宵模型。

问题是:

缺乏弹性伸缩能力,当客户端并发访问量增加后,服务端的线程个数和客户端并发访问数呈1:1的正比关系;

线程数量快速膨胀后,系统的性能将急剧下降,随着访问量的继续增大,系统最终就死-掉-了


 伪异步I/O编程
为了改进这种一连接一线程的模型,使用线程池来管理这些线程,实现1个或多个线程处理N个客户端的模型(但是底层还是使用的同步阻塞I/O),

通常被称为“伪异步I/O模型“。
不能使用使用CachedThreadPool线程池,建议FixedThreadPool,

如果发生大量并发请求,超过最大数量的线程就只能等待,直到线程池中的有空闲的线程可以被复用。

在读取数据较慢时(比如数据量大、网络传输慢等),大量并发的情况下,其他接入的消息,只能一直等待,这是问题。


什么是NIO?

New I/O简称,或Non-block I/O,与旧式的基于流的I/O方法相对,表示一套新的I/O标准;jdk1.4引入的;

NIO提供了与传统BIO模型中的Socket和ServerSocket相对应的SocketChannel和ServerSocketChannel,两种不同的套接字通道实现。

新增的着两种通道都支持阻塞和非阻塞两种模式,阻塞模式使用就像传统中的支持一样,比较简单,但是性能和可靠性都不好;非阻塞模式正好与之相反

  对于低负载、低并发的应用程序,可以使用同步阻塞I/O来提升开发速率和更好的维护性;

对于高负载、高并发的(网络)应用,应使用NIO的非阻塞模式来开发。

NIO是基于块(Block)的,以块为基本单位处理数据,传统I/O基于字节(Byte)的;性能比流好些;

为所有原始类型提供(Buffer)缓存支持;

增加通道(Channel)对象,作为新的I/O抽象,类似旧式的Stream;

支持文件锁,和内存映射文件的文件访问接口;使用文件来实现锁;

基于网络操作,提供了Selector的异步网络I/O;


Buffer(缓存区)&Channel(通道)

Buffer:NIO的核心部分,所有IO操作部分都要Buffer做,与Channel(通道)交互;

缓冲区实际上是一个数组,并提供了对数据结构化访问以及维护读写位置等信息。

Channel,通道是IO的抽象,另一端就是要操作的对象或socket;

通过对buffer的读取,来实现对IO的操作;

ByteBuffer、CharBuffer,DoubleBuffer,IntBuffer,LongBuffer等,实现了Buffer

Channel我们对数据的读取和写入要通过Channel,它就像水管一样,是一个通道。

通道不同于流的地方就是通道是双向的,可以用于读、写和同时读写操作。

底层的操作系统的通道一般都是全双工的,所以全双工的Channel比流能更好的映射底层操作系统的API。

Channel主要分两大类:

SelectableChannel:用户网络读写(子类有:ServerSocketChannel和SocketChannel);

FileChannel:用于文件操作;


基本使用一:

FileInputStream fin = new FileInputStream(new File("d:\\temp_buffer.tmp"));
FileChannel fc=fin.getChannel();

ByteBuffer byteBuffer=ByteBuffer.allocate(1024);
fc.read(byteBuffer);

fc.close();
byteBuffer.flip();
 


基本使用二:NIO复制文件

public static void nioCopyFile(String resource, String destination)
throws IOException {
FileInputStream fis = new FileInputStream(resource);
FileOutputStream fos = new FileOutputStream(destination);
FileChannel readChannel = fis.getChannel(); //读文件通道
FileChannel writeChannel = fos.getChannel(); //写文件通道
ByteBuffer buffer = ByteBuffer.allocate(1024); //读入数据缓存
while (true) {
buffer.clear();
int len = readChannel.read(buffer); //读入数据
if (len == -1) {
break;
//读取完毕
}
buffer.flip();
writeChannel.write(buffer);//写入文件
}
readChannel.close();
writeChannel.close();
}
 


Buffer中3个重要参数:位置(position)、容量(capacity)、上限(limit);
位置:记录当前缓冲区的位置,将从此位置开始读写数据;

容量:缓存区的总容量上限;

上限:缓存区实际数据的大小,可读取的容量,总是小于等于容量;通常情况和容量相等;


Buffer的3个函数:

flip():读写转换时使用;先将limit设置为当前position的位置(实际大小),再将position重置为0,清除标志位Mark

rewind():将position置零,并清除标志位;

clear():将position置零,同时将limit设置为capacity大小,清除标志位Mark;


实例展示:

ByteBuffer b=ByteBuffer.allocate(15); //15个字节大小的缓冲区  
System.out.println("limit="+b.limit()+"capacity="+b.capacity()+"position="+b.position();
for(int i=0;i<10;i++){ //存入10个字节数据
b.put((byte)i);
}
System.out.println("limit="+b.limit()+" capacity="+b.capacity()+" position="+b.position());
b.flip(); //重置position 
System.out.println("limit="+b.limit()+" capacity="+b.capacity()+" position="+b.position());
for(int i=0;i<5;i++){
System.out.print(b.get());
}

System.out.println("limit="+b.limit()+" capacity="+b.capacity()+" position="+b.position());


文件映射到内存实例:

RandomAccessFile raf = new RandomAccessFile("C:\\mapfile.txt", "rw");
FileChannel fc = raf.getChannel(); //将文件映射到内存中

MappedByteBuffer mbb = fc.map(FileChannel.MapMode.READ_WRITE, 0, raf.length());

while(mbb.hasRemaining()){
System.out.print((char)mbb.get());
}
mbb.put(0,(byte)98); //修改文件
raf.close();


多路复用器 Selector

NIO的基础;

Selector提供选择已经就绪的任务的能力:Selector会不断轮询注册在其上的Channel;

如果某个Channel上面发生读或者写事件,这个Channel就处于就绪状态,会被Selector轮询出来,

然后通过SelectionKey可以获取就绪Channel的集合,进行后续的I/O操作。

一个Selector可以同时轮询多个Channel,因为JDK使用了epoll()代替传统的select实现,所以没有最大连接句柄1024/2048的限制。

所以,只需要一个线程负责Selector的轮询,就可以接入成千上万的客户端。

网络编程

多线程网络服务器的一般结构:

(图,N<--->N)

问题:

为每一个客户端开启一个线程,如果客户端延时异常,线程可能被长时间占用,因为数据的准备和读取都在这个线程中,占用时间;

若客户端数量众多,可能消耗大量的系统资源;

解决:

非阻塞的NIO;

数据准备好了再工作;


NIO做法:把数据准备好了再通知我;

Channel类似于流,一个Channel可以和文件或者网络Socket对应;

一个Selector对应一个线程,可以轮询多个Channel,一个Channel背后对应一个Socket,即一个客户端;

所以一个线程可以轮询了多个channel,选择准备好数据的Channel;

Selector的两个方法,

select():若没有准备好数据的Channel,则阻塞;准备好数据后返回SelectionKey,表示一对关系selector和channel的对应;

selectNow():也是选择已经准备好的channel,若没有准备好的,则直接返回,不阻塞;


代码实例:

一定要写个例子看看哦;


总结:

NIO会将数据准备好后再交给应用处理,数据读取过程依然在线程中完成;

节省数据准备时间(因为selector可以复用);


AIO

NIO是剥离了数据准备时,避免大量线程的等待操作,数据读写处理还是在线程应用中完成;

AIO更进一步,在数据准备及读写时都不处理,等数据操作完成后,加入回调函数即可;异步操作IO;

IO本身的速度是没有改变的,这几种方式只是改变里处理IO的方式,改善线程调度,看起来系统性能变高;

server = AsynchronousServerSocketChannel.open().bind(new InetSocketAddress(PORT));

使用server上的accept()方法:

public abstract <A> void accept(A attachment, CompletionHandler<AsynchronousSocketChannel,?super A> handler);

还有read()、 write() 方法;AsynchronousSocketChannel.read(ByteBuffer b, ...)


为什么需要了解NIO和AIO?

AIO是真正的异步非阻塞的,所以,在面对超级大量的客户端,更能得心应手。

03

具体选择什么样的模型或者NIO框架,完全基于业务的实际应用场景和性能需求,

如果客户端很少,服务器负荷不重,就没有必要选择开发起来相对不那么简单的NIO做服务端;

相反,就应考虑使用NIO或者相关的框架了





评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值