一.NIO和IO的主要区别
下表总结了Java IO和NIO之间的主要区别:
| IO | NIO |
|面向流| 面向缓冲|
| 阻塞IO | 非阻塞IO|
| 无 | 选择器|
Io | Nio |
---|---|
面向流 | 面向缓冲 |
– | – |
阻塞 | 非阻塞 |
– | – |
无 | 选择器 |
- 面向流意味着必须从头到尾读写流,中间不能中断。是直线的。
- 面向缓冲是读写数据是以块的形式读写,可以中断,中断后可以重接着读写。因为读写的数据都存放在Buffer缓冲里。
阻塞就是:当某个事件或者任务在执行过程中,它发出一个请求操作,但是由于该请求操作需要的条件不满足,那么就会一直在那等待,直至条件满足;
非阻塞就是:当某个事件或者任务在执行过程中,它发出一个请求操作,如果该请求操作需要的条件不满足,会立即返回一个标志信息告知条件不满足,不会一直在那等待。
- 阻塞是当读写的时候线程属于阻塞状态,必需在有返回值才能从阻塞状态变成别的状态。所以说 IO是同步的。
- 非阻塞 是通过 Selector(选择器)轮询 Channel(通道)的状态。如果有IO操作轮询到就之间执行。NIO也是同步的但是AIO是异步的。选择器可以管理多个通道。
- 选择器 允许一个单独的线程来监视多个输入通道,你可以注册多个通道使用一个选择器,然后使用一个单独的线程来“选择”通道:这些通道里已经有可以处理的输入,或者选择已准备写入的通道。这种选择机制,使得一个单独的线程很容易来管理多个通道。
- NIO主要有三大核心部分:Channel(通道),Buffer(缓冲区), Selector。
1Channel
-
FileChannel
DatagramChannel
SocketChannel
ServerSocketChannel
分别可以对应文件IO、UDP和TCP(Server和Client)。
2Buffer
ByteBuffer, CharBuffer, DoubleBuffer, FloatBuffer, IntBuffer, LongBuffer, ShortBuffer,分别对应基本数据类型: byte, char, double, float, int, long, short。当然NIO中还有MappedByteBuffer, HeapByteBuffer, DirectByteBuffer等。
3Selector
Selector运行单线程处理多个Channel,如果你的应用打开了多个通道,但每个连接的流量都很低,使用Selector就会很方便。例如在一个聊天服务器中。要使用Selector, 得向Selector注册Channel,然后调用它的select()方法。这个方法会一直阻塞到某个注册的通道有事件就绪。一旦这个方法返回,线程就可以处理这些事件,事件的例子有如新的连接进来、数据接收等。
- FileChannel
new RandomAccessFile("src/nio.txt","rw");
new FileInputStream("src/nomal_io.txt");
-
Buffer的使用
分配空间(ByteBuffer buf = ByteBuffer.allocate(1024);
(还有一种allocateDirector)写入数据到Buffer(int bytesRead = fileChannel.read(buf)?
调用filp()方法( buf.flip();)
从Buffer中读取数据(System.out.print((char)buf.get());)
调用clear()方法或者compact()方法
实际上是一个容器,一个连续数组。读写都要经过Buffer。
-
向Buffer中写数据:
从Channel写到Buffer (fileChannel.read(buf))
通过Buffer的put()方法 (buf.put(…))
从Buffer中读取数据:
从Buffer读取到Channel (channel.write(buf))
使用get()方法从Buffer中读取数据 (buf.get())
-
标记
capacity, position, limit, mark
索引 | 说明 |
---|---|
capacity | 缓冲区数组的总长度 |
position | 下一个要操作的数据元素的位置 |
limit | 缓冲区数组中不可操作的下一个元素的位置:limit<=capacity |
mark | 用于记录当前position的前一个位置或者默认是-1 |
创建Buffer capacity为11
ByteBuffer.allocate(11);
当我们写入5个字节时,变化如下图:
这时我们需要将缓冲区中的5个字节数据写入Channel的通信信道,所以我们调用ByteBuffer.flip()方法,变化如下图所示(position设回0,并将limit设成之前的position的值):
这时底层操作系统就可以从缓冲区中正确读取这个5个字节数据并发送出去了。在下一次写数据之前我们再调用clear()方法,缓冲区的索引位置又回到了初始位置。
调用clear()方法:position将被设回0,limit设置成capacity,换句话说,Buffer被清空了,其实Buffer中的数据并未被清除,只是这些标记告诉我们可以从哪里开始往Buffer里写数据。如果Buffer中有一些未读的数据,调用clear()方法,数据将“被遗忘”,意味着不再有任何标记会告诉你哪些数据被读过,哪些还没有。如果Buffer中仍有未读的数据,且后续还需要这些数据,但是此时想要先写些数据,那么使用compact()方法。compact()方法将所有未读的数据拷贝到Buffer起始处。然后将position设到最后一个未读元素正后面。limit属性依然像clear()方法一样,设置成capacity。现在Buffer准备好写数据了,但是不会覆盖未读的数据。
通过调用Buffer.mark()方法,可以标记Buffer中的一个特定的position,之后可以通过调用Buffer.reset()方法恢复到这个position。Buffer.rewind()方法将position设回0,所以你可以重读Buffer中的所有数据。limit保持不变,仍然表示能从Buffer中读取多少个元素。
- Selector
Selector selector = Selector.open();
为了将Channel和Selector配合使用,必须将Channel注册到Selector上,通过SelectableChannel.register()方法来实现:
ssc= ServerSocketChannel.open();
ssc.socket().bind(new InetSocketAddress(PORT));//设置端口
ssc.configureBlocking(false);//费阻塞状态
ssc.register(selector, SelectionKey.OP_ACCEPT);//selector监听某一事件
register()方法的第二个参数。这是一个“interest集合”,意思是在通过Selector监听Channel时对什么事件感兴趣。可以监听四种不同类型的事件:
- Connect
- Accept
- Read
- Write
- SelectionKey.OP_CONNECT//连接就绪
- SelectionKey.OP_ACCEPT//接受就绪
- SelectionKey.OP_READ//读就绪
- SelectionKey.OP_WRITE//写就绪
- SelectionKey
当向Selector注册Channel时,register()方法会返回一个SelectionKey对象。这个对象包含了一些你感兴趣的属性:
interest集合
ready集合
Channel
Selector
附加的对象(可选)
可以用像检测interest集合那样的方法,来检测channel中什么事件或操作已经就绪。但是,也可以使用以下四个方法,它们都会返回一个布尔类型:
selectionKey.isAcceptable();
selectionKey.isConnectable();
selectionKey.isReadable();
selectionKey.isWritable();
- 通过Selector选择通道
int select()
int select(long timeout)
int selectNow()
服务端NIOServer
public class ServerConnect
{
private static final int BUF_SIZE=1024;
private static final int PORT = 8080;
private static final int TIMEOUT = 3000;
public static void main(String[] args)
{
selector();
}
public static void handleAccept(SelectionKey key) throws IOException{
ServerSocketChannel ssChannel = (ServerSocketChannel)key.channel();
SocketChannel sc = ssChannel.accept();
sc.configureBlocking(false);
sc.register(key.selector(), SelectionKey.OP_READ,ByteBuffer.allocateDirect(BUF_SIZE));
}
public static void handleRead(SelectionKey key) throws IOException{
SocketChannel sc = (SocketChannel)key.channel();
ByteBuffer buf = (ByteBuffer)key.attachment();
long bytesRead = sc.read(buf);
while(bytesRead>0){
buf.flip();
while(buf.hasRemaining()){
System.out.print((char)buf.get());
}
System.out.println();
buf.clear();
bytesRead = sc.read(buf);
}
if(bytesRead == -1){
sc.close();
}
}
public static void handleWrite(SelectionKey key) throws IOException{
ByteBuffer buf = (ByteBuffer)key.attachment();
buf.flip();
SocketChannel sc = (SocketChannel) key.channel();
while(buf.hasRemaining()){
sc.write(buf);
}
buf.compact();
}
public static void selector() {
Selector selector = null;
ServerSocketChannel ssc = null;
try{
selector = Selector.open();
ssc= ServerSocketChannel.open();
ssc.socket().bind(new InetSocketAddress(PORT));
ssc.configureBlocking(false);
ssc.register(selector, SelectionKey.OP_ACCEPT);
while(true){
if(selector.select(TIMEOUT) == 0){
System.out.println("==");
continue;
}
Iterator<SelectionKey> iter = selector.selectedKeys().iterator();
while(iter.hasNext()){
SelectionKey key = iter.next();
if(key.isAcceptable()){
handleAccept(key);
}
if(key.isReadable()){
handleRead(key);
}
if(key.isWritable() && key.isValid()){
handleWrite(key);
}
if(key.isConnectable()){
System.out.println("isConnectable = true");
}
iter.remove();
}
}
}catch(IOException e){
e.printStackTrace();
}finally{
try{
if(selector!=null){
selector.close();
}
if(ssc!=null){
ssc.close();
}
}catch(IOException e){
e.printStackTrace();
}
}
}
}
2.1 ByteBuffer字节缓冲区
操作系统的IO是以字节为单位的,因此,字节缓冲区跟其他缓冲区不同,对操作系统的IO只能是基于字节缓冲区的,所以通道(channel)只接收ByteBuffer作为参数。
2.2 直接缓冲区和非直接缓冲区
ByteBuffer又分为直接缓冲区和非直接缓冲区。
非直接缓冲区可以通过ByteBuffer.wrap(byte[] array);ByteBuffer.allocate(int capacity)这两个方法来创建
直接缓冲区可通过ByteBuffer.allocateDirect(int capacity)来创建
(直接缓冲区是将原来的“copy”部分换成“物理内存映射文件” , 当用户程序写数据的时候,就将该数据写到这个 映射文件中,之后物理磁盘直接从这个物理磁盘获取数据,反之用户程序也可以直接从映射文件中读取数据。也就少了“copy”的开销)
在Buffer类中定义了一个变量adress,作为直接缓冲区使用,
通过调用JNI的方法来获得一个内存地址。
也就是说,直接缓冲区说指向内存中的某个地址,
而不是JVM中的某个区域。由于是内存中的某个区域,
并且通过JNI去调用操作系统底层的指令,
因此在某些情况下相对的高效。非直接缓冲区指向的是JVM内某个数组空间。
字节缓冲区要么是直接的,要么是非直接的。如果为直接字节缓冲区,则 Java 虚拟机会尽最大努力直接在此缓冲区上执行本机 I/O 操作。也就是说,在每次调用基础操作系统的一个本机 I/O 操作之前(或之后),虚拟机都会尽量避免将缓冲区的内容复制到中间缓冲区中(或从中间缓冲区中复制内容)。
对直接缓冲区进行分配和取消分配所需成本通常高于非直接缓冲区。直接缓冲区的内容可以驻留在常规的垃圾回收堆之外,因此,它们对应用程序的内存需求量造成的影响可能并不明显。所以,建议将直接缓冲区主要分配给那些易受基础系统的本机 I/O 操作影响的大型、持久的缓冲区。一般情况下,最好仅在直接缓冲区能在程序性能方面带来明显好处时分配它们。
直接字节缓冲区还可以通过映射将文件区域直接映射到内存中来创建。Java 平台的实现有助于通过 JNI 从本机代码创建直接字节缓冲区。
-XX:MaxDirectMemorySize=<size> 限制堆外缓冲区大小