NIO基本概述[^3]
NIO主要围绕三个核心组件:Channel、Buffer、Selector;以及若干核心工具类:Pipe、FileLock、Charset、Files、Channels(用于IO和NIO转换)
- NIO有两个核心概念,即==通道(channel)和缓冲区(buffer)==,其核心就是从通道中读取数据到缓冲区,或者从缓冲区中读取数据写入通道。
- IO的核心概念主要是字符流和字节流,因此NIO面向的是缓冲区,而IO直接就是面对流。
- NIO是非阻塞的(Non-Blocking IO),==只要把数据交给缓冲区,剩下的就可以不用管了==,直接用线程去处理其他事情就可以。而阻塞模型的话,线程必须阻塞等待流的IO完成。
- NIO可以使用Selector实现多路复用功能。
- 通道是全双工的,可以实现异步读写,而流是单向的,只能单向读或单向写,也就是单工的。
- JDK1.4提供了对非阻塞IO(NIO)的支持,JDK1.5_update10版本使用epoll替代了传统的select/poll,极大的提升了NIO通信的性能。Java 1.7 增加了异步IO
Channel通道
主要分以下几个具体实现类,分别针对不同的实现,比如文件、UDP、TCP。
- FileChannel - 文件,路径参看[^6]
- DatagramChannel – UDP
- SocketChannel – TCP
- ServerSocketChannel – TCP,监听网络请求,为每个请求建立SocketChannel
FileChannel
该类是一个抽象类,但其对象是线程安全的,多个线程运行并发调用该对象。
影响通道位置或者影响文件大小的操作都是单线程的(single-threaded)。如果有一个线程已经在执行会影响通道位置或文件大小的操作,那么其他尝试进行此类操作之一的线程必须等待。并发行为也会受到底层的操作系统或文件系统影响。[^7]
因为当前操作系统大部分的文件I/O还不支持异步请求,因而大部分情况下的FileChannel总是阻塞式的。该通道不能直接创建,一个FileChannel只能读写一个File对象,具有和File对象相同的文件访问权限,且只能通过调用File对象(RandomAccessChannel、FileInputStream、FileOutputStream)的getChannel方法获取[^7]。
简单示例
读文件的具体案例可以参看“Buffer缓存区的简单示例”。写文件的案例如下[^9][^10]:
/**
* 将内容写入文件中,如果文件已经存在,则附加在后面,否则新建文件
*
* @param stringContent
* @param filePath
* @param isAppend 判断是否追加还是覆盖
*/
public static boolean writeStringToFile(final String stringContent, final String filePath, final boolean isAppend) {
return AccessController.doPrivileged(new PrivilegedAction<Boolean>() {
@Override
public Boolean run() {
if (StringUtils.isBlank(stringContent)) {
return false;
}
File file = checkFile(filePath);
if (file == null) {
//文件读取失败
return false;
}
RandomAccessFile randomAccessFile = null;
FileChannel fileChannel = null;
ByteBuffer byteBuffer = null;
try {
randomAccessFile = new RandomAccessFile(file, "rw");
fileChannel = randomAccessFile.getChannel();
byte[] bytes = stringContent.getBytes(Charset.forName("utf-8"));
byteBuffer = ByteBuffer.wrap(bytes);
int byteLength = bytes.length, length = 0;
if (isAppend) {
//判断是追加
fileChannel.position(file.length());
} else {
fileChannel.position(0);
//强制指定长度
fileChannel.truncate(byteLength);
}
while (byteBuffer.hasRemaining()) {
fileChannel.write(byteBuffer);
}
//强制刷新到文件系统
fileChannel.force(true);
return true;
} catch (FileNotFoundException e) {
//文件读取失败
return false;
} catch (IOException e) {
//写文件失败
return false;
} finally {
if (randomAccessFile != null) {
try {
randomAccessFile.close();
} catch (IOException e) {
;
}
}
if (fileChannel != null) {
try {
fileChannel.close();
} catch (IOException e) {
;
}
}
if (byteBuffer != null) {
byteBuffer.clear();
}
}
}
});
}
/**
* 判断当前文件是否存在,如果不存在则创建,否则直接返回File描述文件对象
* <pre>
* 注意:如果该文件是一个目录,则直接返回null
* </pre>
*
* @param filePath
* @return
*/
public static File checkFile(final String filePath) {
if (StringUtils.isNotBlank(filePath)) {
final File file = new File(filePath);
if (file.isDirectory()) {
//如果是目录,则直接返回null
return null;
}
if (file.exists()) {
//如果文件已经存在,则直接返回文件描述对象
return file;
}else{
//如果文件不存在,则直接创建文件
return AccessController.doPrivileged(new PrivilegedAction<File>() {
@Override
public File run() {
try {
boolean success = file.createNewFile();
if(success){
return file;
}
return null;
} catch (IOException e) {
}
return null;
}
});
}
}
return null;
}
注意[^8][^10]:
- FileDescriptor类是读写文件的核心,代表一个独立于系统的隐式句柄流。看下面的JDK说明:
Instances of the file descriptor class serve as an opaque handle to the underlying machine-specific structure representing an open file, an open socket, or another source or sink of bytes. The main practical use for a file descriptor is to create a FileInputStream or FileOutputStream to contain it.
FileDescriptor类作为一个隐式句柄,可用于代表与底层机器适配的一个打开文件、一个打开的socket,另一个数据源或字节槽。最主要的用法是用于创建FileInputStream或FileOutputStream以包含它。(注:我们常用的System.in/System.out/System.err就是包含了这个)
- File类是一个独立于系统的、不可变的(immutable)目录或文件分层路径名抽象(An abstract representation of file and directory pathnames),可用于“抽象路径名(an abstract pathname)与路径字符串(a pathname string)的转换”(路径包含绝对路径和相对路径,前者说明定位文件或目录不需要额外信息,而后者还需要系统属性user.dir配合,user.dir属性是指当前应用JVM启动的地址),也可以描述文件和目录信息,比如文件大小、名称、是否存在、获取父亲目录地址(getParent)等信息,不能用于改变或读写文件内容,读写内容是通过FileDescriptor句柄流实现的。java.nio.file包提供了更加丰富的抽象及工具类。
- Ra