这些天看NIO时候,遇到了这三个概念,花了些时间,搞懂了里面的区别与联系,记录分享~
BIO
可以理解为block-io,即阻塞型IO,就是当你调用read时候,只有有数据才会返回,而没有数据时,是不会返回只会阻塞等待的。
传统的Java Socket就是这样一种机制,有数据才会返回,否则会一直阻塞。
比如我们使用Socket进行与服务端通信,默认情况下服务端需要对每个请求建立一堆线程等待请求,而客户端发送请求后,先咨询服务端是否有线程相应,如果没有则会一直等待或者遭到拒绝请求,如果有的话,客户端会线程会等待请求结束后才继续执行。
NIO
Java1.4之后,新增加了nio,可以称之为new-io,即使用一种新的编程模式编程,当然底层也有区别。
nio属于非阻塞同步型,它的调用会立即返回给控制权调用者,调用者不需要等待。即它调用,要么成功,要么失败。
如果不可用则需要等待然后再试试(而不是阻塞)。
这个时候再nio中就会需要selector,让它来做轮训,看看那个channel可用,如果可用,则再由线程操作,
这样相对于前面的bio,性能和伸缩性都提高了。
AIO
AIO在java1.7之后新加入进来的,可以理解为nio2.0,异步非阻塞型,重要的有以下几个类
- AsynchronousSocketChannel
- AsynchronousServerSocketChannel
- AsynchronousFileChannel
- AsynchronousByteChannel
它的伸缩性则更强了,在开始的NIO中,虽然是非阻塞的,但是还是需要轮训才知道那个通道数据可用。
而在AIO,并不需要selector去轮训了。
在AIO中,调用方法会立即返回,并且会告诉调用者,这次请求已经开始了,系统会使用另外的资源或者线程来完成
这次调用操作,并且在完成时候会通知调用者(比如回调方法)。
在以上三种IO形式中,非阻塞异步是性能最高、伸缩性最好的。
当然上述只是基本的三种的基本概念,在看nio时候,在思考,它们之间的本质区别是什么。
上面所解释的,是从编程模型上面来讲考虑的。
从操作系统层面来讲,
1.4之前的io和之后出现的nio,有哪些方面不同呢?
传统IO和NIO区别
在使用NIO时,通常需要获得一个channel,然后由channel和buffer之间操作:
int bytesRead = inChannel.read(buf);
利用Channel,将数据读出到buf。
而普通io对文件操作则是如下:
output.write("This text is converted to bytes".getBytes("utf-8"));
先慢慢跟着jdk源码,看看具体调用过程~
传统IO的read方法调用
以FileInputStream为例:
1. 首先是read方法:public int read() throws IOException
2. 接着调用内部native方法read0,private native int read0() throws IOException;
3. 由native方法走向,调用
http://hg.openjdk.java.net/jdk8u/jdk8u/jdk/file/07011844584f/src/share/native/java/io/FileInputStream.c
中的read0方法:
Java_java_io_FileInputStream_read0(JNIEnv *env, jobject this) {
return readSingle(env, this, fis_fd);
}
以此传入JNI环境(例如是windows还是linux等系统),以及当前Java对象this。
readSingle
方法意思是从当前流中读取 4 字节浮点值,并使流的当前位置提升 4 个字节。
NIO中channel的write方法调用
NIO和传统IO有点区别
以FileChannel为例。
1. 通过FileInputStream的getChannel方法,获取一个channel。
public FileChannel getChannel() {
synchronized (this) {
if (channel == null) {
channel = FileChannelImpl.open(fd, path, true, false, this);
}
return channel;
}
}
2.由上述,把channel写成单例模式,调用FileChannelImpl中的open方法,该类在http://hg.openjdk.java.net/jdk8u/jdk8u/jdk/file/07011844584f/src/share/classes/sun/nio/ch/FileChannelImpl.java
中,该类继承自FileChannel,再看open方法:
// Used by FileInputStream.getChannel() and RandomAccessFile.getChannel()
public static FileChannel open(FileDescriptor fd, String path,
boolean readable, boolean writable,
Object parent)
{
return new FileChannelImpl(fd, path, readable, writable, false, parent);
}
- 再看看FileChannelImpl的read方法,同样是以cpu为参照对象,讲数据read到buffer中:
public int read(ByteBuffer dst) throws IOException {
ensureOpen(); // 保证打开
if (!readable) //检验可写
throw new NonReadableChannelException();
synchronized (positionLock) { //加索
int n = 0;
int ti = -1;
try {
begin();
ti = threads.add(); //threads是NativeThreadSet
if (!isOpen())
return 0;
do {
n = IOUtil.read(fd, dst, -1, nd);
} while ((n == IOStatus.INTERRUPTED) && isOpen());
return IOStatus.normalize(n);
} finally {
threads.remove(ti);
end(n > 0);
assert IOStatus.check(n);
}
}
}
- 再看看IOUtil的read方法,位于:
http://hg.openjdk.java.net/jdk8u/jdk8u/jdk/file/07011844584f/src/share/classes/sun/nio/ch/IOUtil.java
static int read(FileDescriptor fd, ByteBuffer dst, long position,
NativeDispatcher nd)
throws IOException
{
if (dst.isReadOnly())
throw new IllegalArgumentException("Read-only buffer");
if (dst instanceof DirectBuffer)
return readIntoNativeBuffer(fd, dst, position, nd);
//获取一个一个dst大小的directBuffer
ByteBuffer bb = Util.getTemporaryDirectBuffer(dst.remaining());
try {
int n = readIntoNativeBuffer(fd, bb, position, nd);
bb.flip();
if (n > 0)
dst.put(bb);
return n;
} finally {
Util.offerFirstTemporaryDirectBuffer(bb);
}
}
在这个read中,思路是:
- 通过获取一个和给定dst相同大小的临时directBuffer,也就是内存映射的buffer,
然后讲数据先读入到这个临时directBuffer中,再把数据加到dst中。
- 再看看
readIntoNativeBuffer
方法:
- 再看看
private static int readIntoNativeBuffer(FileDescriptor fd, ByteBuffer bb,
long position, NativeDispatcher nd)
throws IOException
{
int pos = bb.position(); //获取position
int lim = bb.limit(); //获取limit
assert (pos <= lim);
int rem = (pos <= lim ? lim - pos : 0); //判断是否还有空间
if (rem == 0)
return 0;
int n = 0;
//利用NativeDispatcher的pread进行读取。
if (position != -1) {
n = nd.pread(fd, ((DirectBuffer)bb).address() + pos,rem, position);
} else {
n = nd.read(fd, ((DirectBuffer)bb).address() + pos, rem);
}
if (n > 0)
bb.position(pos + n);
return n;
}
- 再看NativeDispatcher的pread和read方法,追根溯源,去FileChannelmpl里面看NativeDispatcher的初始化:
private static final NativeDispatcher nd = new FileDispatcherImpl();
由于本机是ubuntu,所以去solaris下面看具体java文件:
http://hg.openjdk.java.net/jdk8u/jdk8u/jdk/file/07011844584f/src/solaris/classes/sun/nio/ch/FileDispatcherImpl.java
其中,pread方法,也是间接调用pread0方法,而pread0方法则是一个native方法。
- 一路追寻,找到了FileDispatcherImpl.c文件:
http://hg.openjdk.java.net/jdk8u/jdk8u/jdk/file/07011844584f/src/solaris/native/sun/nio/ch/FileDispatcherImpl.c
看看里面的pread0方法:
JNIEXPORT jint JNICALL
Java_sun_nio_ch_FileDispatcherImpl_pread0(JNIEnv *env, jclass clazz, jobject fdo,
jlong address, jint len, jlong offset)
{
jint fd = fdval(env, fdo); //获取fd
void *buf = (void *)jlong_to_ptr(address);
//首先通过pread64读取,然后调用convertReturnVal进行返回前转化。
return convertReturnVal(env, pread64(fd, buf, len, offset), JNI_TRUE);
}
由代码开头的宏定义可知,实际上调用的是pread方法:
#define pread64 pread
而pread方法则是从c++的方法:
ssize_t pread(intfd, void *buf, size_tcount, off_toffset)
:带偏移量地原子的从文件中读取数据,并放入buf所指向的缓冲区ssize_t readv(int filedes,const struct iovec *iov,int iovcnt)
:readv将读入的数据按上述同样顺序散布读到缓冲区中。readv总是先填满一个缓冲区,然后再填写下一个。readv返回读到的总字节数。如果遇到文件结尾,已无数据可读,则返回0。
总结
从实现来看,传统IO与NIO确实存在不小的区别。
1、传统IO是一次读取4(或其他,有规律)个字节,而NIO,则是一次读取多个,直接存入buffer(在c++层面就是读取多个,而不是一次读取一个再在Java层面拼凑出Buffer)。
2、NIO在读取时,如果传入的不是directBuffer,则最终读取时,还是通过一个临时directBuffer读取,最后放入传入的buffer中。
3、传统IO读取经过以下2个步骤:
- read()方法执行,表示服务器要去磁盘上读文件了,这会导致一个 sys_read()的系统调用。此时由用户态切换到内核态,完成的动作是:DMA把磁盘上的数据读入到内核缓冲区中(这也是第一次拷贝)。
- read()方法的返回(这也说明read()是一个阻塞调用),表示数据已经成功从磁盘上读到内核缓冲区了。此时,由内核态返回到用户态,完成的动作是:将内核缓冲区中的数据拷贝到用户缓冲区(这是第二次拷贝)。
每一次传统io的操作,都伴随着由内核缓冲区到用户缓冲区的拷贝,以及用户状态切换。
而nio则可以一次传递多个字节到buffer中。
4、实现思路,传统IO是阻塞式的,而NIO则是非阻塞式的。
5、NIO中使用的directbuffer(普通buffer也是通过directbuffer来操作的):
内存映射IO:就是复用一个以上的虚拟地址可以指向同一个物理内存地址。将内核空间的缓冲区地址(内核地址空间)映射到物理内存地址区域,将用户空间的缓冲区地址(用户地址空间)也映射到相同的物理内存地址区域。从而数据不需要从内核缓冲区映射的物理内存地址移动到用户缓冲区映射的物理内存地址了。
关于heap buffer和directBuffer可以参看:http://eyesmore.iteye.com/blog/1133335
NIO和AIO区别
二者在操作系统层面对文件的操作是一致的,均是以buffer为单位传送,而在通信模型上有区别:
NIO通常采用Reactor模式,AIO通常采用Proactor模式。AIO简化了程序的编写,stream的读取和写入都有OS来完成,不需 要像NIO那样子遍历Selector。Windows基于IOCP实现AIO,Linux只有eppoll模拟实现了AIO。Java7之前的JDK只支持NIO和BIO,从7开始支持AIO。
接下来文章再分析AIO,这篇只给出了AIO思想。
参考资料:
1. http://blog.csdn.net/u014507083/article/details/73784898
2. http://blog.csdn.net/zhongweijian/article/details/8005444
3. https://baike.baidu.com/item/pread/1839183?fr=aladdin
4. http://www.dewen.net.cn/q/9648
5. http://www.iteye.com/topic/472333
6. https://segmentfault.com/q/1010000000314712
7. http://blog.csdn.net/hapjin/article/details/50538643