一、NIO的与IO的区别:
1、IO是面向流的,NIO是面向缓冲的;
2、IO是阻塞的,NIO是非阻塞的;
3、IO是单线程的,NIO 是通过选择器来模拟多线程的;
通道 Channel 是对原 I/O 包中的流的模拟,可以通过它读取和写入数据。
通道与流的不同之处在于,流只能在一个方向上移动(一个流必须是 InputStream 或者 OutputStream 的子类),而通道是双向的,可以用于读、写或者同时用于读写。
通道包括以下类型:
发送给一个通道的所有数据都必须首先放到缓冲区中,同样地,从通道中读取的任何数据都要先读到缓冲区中。也就是说,不会直接对通道进行读写数据,而是要先经过缓冲区。
缓冲区实质上是一个数组,但它不仅仅是一个数组。缓冲区提供了对数据的结构化访问,而且还可以跟踪系统的读/写进程。
缓冲区包括以下类型:
① 新建一个大小为 8 个字节的缓冲区,此时 position 为 0,而 limit = capacity = 8。capacity 变量不会改变,下面的讨论会忽略它。
② 从输入通道中读取 5 个字节数据写入缓冲区中,此时 position 移动设置为 5,limit 保持不变。
③ 在将缓冲区的数据写到输出通道之前,需要先调用 flip() 方法,这个方法将 limit 设置为当前 position,并将 position 设置为 0。
④ 从缓冲区中取 4 个字节到输出缓冲中,此时 position 设为 4。
⑤ 最后需要调用 clear() 方法来清空缓冲区,此时 position 和 limit 都被设置为最初位置。
NIO 常常被叫做非阻塞 IO,主要是因为 NIO 在网络通信中的非阻塞特性被广泛使用。
NIO 实现了 IO 多路复用中的 Reactor 模型,一个线程 Thread 使用一个选择器 Selector 通过轮询的方式去监听多个通道 Channel 上的事件,从而让一个线程就可以处理多个事件。
通过配置监听的通道 Channel 为非阻塞,那么当 Channel 上的 IO 事件还未到达时,就不会进入阻塞状态一直等待,而是继续轮询其它 Channel,找到 IO 事件已经到达的 Channel 执行。
因为创建和切换线程的开销很大,因此使用一个线程来处理多个事件而不是一个线程处理一个事件,对于 IO 密集型的应用具有很好地性能。
应该注意的是,只有套接字 Channel 才能配置为非阻塞,而 FileChannel 不能,为 FileChannel 配置非阻塞也没有意义。
NIO在基础的IO流上发展出新的特点,分别是:内存映射技术,字符及编码,非阻塞I/O和文件锁定。下面我们分别就这些技术做一些说明:
<1> 内存映射
这个功能主要是为了提高大文件的读写速度而设计的。内存映射文件(memory-mappedfile)能让你创建和修改那些大到无法读入内存的文件。有了内存映射文件,你就可以认为文件已经全部读进了内存,然后把它当成一个非常大的数组来访问了。将文件的一段区域映射到内存中,比传统的文件处理速度要快很多。
内存映射文件它虽然最终也是要从磁盘读取数据,但是它并不需要将数据读取到OS内核缓冲区,而是直接将进程的用户私有地址空间中的一部分区域与文件对象建立起映射关系,就好像直接从内存中读、写文件一样,速度当然快了。
NIO中内存映射主要用到以下两个类:
[1] java.nio.MappedByteBuffer
[2] java.nio.channels.FileChannel
下面我们通过一个例子来看一下内存映射读取文件和普通的IO流读取一个大文件(文件大小为102603KB)的速度对比:
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;
/**
* @author lm
* @create 2018-09-09 8:32
* @desc NIO特性之内存映射
**/
public class MemoryMapper {
public static void main(String[] args) {
try {
RandomAccessFile file = new RandomAccessFile("F:\\IDEAWorkSpace\\AlgorithmTrain\\src\\com\\lm\\source\\triplets.txt","rw");
FileChannel channel = file.getChannel();
MappedByteBuffer buffer = channel.map(FileChannel.MapMode.READ_ONLY,0,channel.size());
ByteBuffer buffer1 = ByteBuffer.allocate(1024);
byte[] b = new byte[1024];
long len = file.length();
long startTime = System.currentTimeMillis();
//读取内存映射文件
for(int i=0;i 1024) {
buffer.get(b);
} else {
buffer.get(new byte[(int)(len - i)]);
}
}
long endTime = System.currentTimeMillis();
System.out.println("使用内存映射方式读取文件总耗时: "+(endTime - startTime));
//普通IO流方式
long startTime1 = System.currentTimeMillis();
while(channel.read(buffer1) > 0){
buffer1.flip();
buffer1.clear();
}
long endTime1 = System.currentTimeMillis();
System.out.println("使用普通IO流方式读取文件总耗时: "+(endTime1 - startTime1));
} catch (Exception e) {
e.printStackTrace();
}
}
}
运行结果:
效果对比还是挺明显的。我们看到在上面程序中调用FileChannel类的map方法进行内存映射,第一个参数设置映射模式,现在支持3种模式:
1)FileChannel.MapMode.READ_ONLY:只读缓冲区,在缓冲区中如果发生写操作则会产生ReadOnlyBufferException;
2)FileChannel.MapMode.READ_WRITE:读写缓冲区,任何时刻如果通过内存映射的方式修改了文件则立刻会对磁盘上的文件执行相应的修改操作。别的进程如果也共享了同一个映射,则也会同步看到变化。
而不是像标准IO那样每个进程有各自的内核缓冲区,比如JAVA代码中,没有执行 IO输出流的 flush() 或者 close() 操作,那么对文件的修改不会更新到磁盘去,除非进程运行结束;
3)FileChannel.MapMode.PRIVATE :这个比较狠,可写缓冲区,但任何修改是缓冲区私有的,不会回到文件中。所以尽情的修改吧,结局跟突然停电是一样的。
内存映射文件的优点:
1、用户进程将文件数据视为内存,因此不需要发出read()或write()系统调用。
2、当用户进程触摸映射的内存空间时,将自动生成页面错误,以从磁盘引入文件数据。 如果用户修改映射的内存空间,受影响的页面将自动标记为脏,并随后刷新到磁盘以更新文件。
3、操作系统的虚拟内存子系统将执行页面的智能缓存,根据系统负载自动管理内存。
4、数据始终是页面对齐的,不需要缓冲区复制。
5、可以映射非常大的文件,而不消耗大量内存来复制数据。
对比使用NIO的内存映射 和使用传统IO流复制文件(文件大小为463486KB)性能:
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;
/**
* @author lm
* @create 2018-09-09 14:51
* @desc NIO使用内存映射进行文件读写
**/
public class MemMapReadWrite {
private static int len;
/**
* 读文件
*
* @param fileName
* @return
*/
public static ByteBuffer readFile(String fileName) {
try {
RandomAccessFile file = new RandomAccessFile(fileName, "rw");
len = (int) file.length();
FileChannel channel = file.getChannel();
MappedByteBuffer buffer = channel.map(FileChannel.MapMode.READ_ONLY, 0, len);
return buffer.get(new byte[(int) file.length()]);
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
/**
* 写文件
*
* @param readFileName
* @param writeFileName
*/
public static void writeFile(String readFileName, String writeFileName) {
try {
RandomAccessFile file = new RandomAccessFile(writeFileName, "rw");
FileChannel channel = file.getChannel();
ByteBuffer buffer = readFile(readFileName);
MappedByteBuffer bytebuffer = channel.map(FileChannel.MapMode.READ_WRITE, 0, len);
long startTime = System.currentTimeMillis();
for (int i = 0; i < len; i++) {
bytebuffer.put(i, buffer.get(i));
}
bytebuffer.flip();
long endTime = System.currentTimeMillis();
System.out.println("写文件耗时: " + (endTime - startTime));
} catch (Exception e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
String readFileName = "F:\\IDEAWorkSpace\\AlgorithmTrain\\src\\com\\lm\\source\\20180701_nt.txt";
String writeFileName = "F:\\IDEAWorkSpace\\AlgorithmTrain\\src\\com\\lm\\source\\copy_20180701_nt.txt";
writeFile(readFileName, writeFileName);
}
}
运行时间为:
使用传统IO流复制:
import java.io.*;
/**
* @author lm
* @create 2018-09-09 14:57
* @desc IO流进行文件读写
**/
public class ReadWrite {
public static void main(String[] args) {
try {
//读取源文件文件
String filePath = "F:\\IDEAWorkSpace\\AlgorithmTrain\\src\\com\\lm\\source\\20180701_nt.txt";
File file = new File(filePath);
InputStreamReader inputStreamReader = new InputStreamReader(new FileInputStream(file), "UTF-8");
BufferedReader bufferedReader = new BufferedReader(inputStreamReader);
String line = bufferedReader.readLine();
String targetFilePath = "F:\\IDEAWorkSpace\\AlgorithmTrain\\src\\com\\lm\\source\\copyWithIO_20180701_nt.txt";
//输出流
File fileOut = new File(targetFilePath);
OutputStreamWriter outputStreamWriter = new OutputStreamWriter(new FileOutputStream(fileOut), "UTF-8");
BufferedWriter bufferedWriter = new BufferedWriter(outputStreamWriter);
long startTime = System.currentTimeMillis();
while (bufferedReader.readLine() != null) {
bufferedWriter.write(line);
bufferedWriter.newLine();
}
long endTime = System.currentTimeMillis();
System.out.println("写文件耗时: " + (endTime - startTime));
//关闭输出流
bufferedWriter.close();
outputStreamWriter.close();
//关闭输入流
bufferedReader.close();
inputStreamReader.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
时间为:
二、多路复用IO
在多路复用IO模型中,会有一个线程不断去轮询多个socket的状态,只有当socket真正有读写事件时,才真正调用实际的IO读写操作。因为在多路复用IO模型中,只需要使用一个线程就可以管理多个socket,系统不需要建立新的进程或者线程,也不必维护这些线程和进程,并且只有在真正有socket读写事件进行时,才会使用IO资源,所以它大大减少了资源占用。
NIO 的非阻塞 I/O 机制是围绕 选择器和 通道构建的。
Channel 类表示服务器和客户机之间的一种通信机制。
Selector 类是 Channel 的多路复用器。 Selector 类将传入客户机请求多路分用并将它们分派到各自的请求处理程序。NIO 设计背后的基石是反应器(Reactor)设计模式。
Reactor负责IO事件的响应,一旦有事件发生,便广播发送给相应的handler去处理。而NIO的设计则是完全按照Reactor模式来设计的。Selector发现某个channel有数据时,会通过SelectorKey来告知,然后实现事件和handler的绑定。
在Reactor模式中,包含如下角色:
1)Reactor 将I/O事件发派给对应的Handler
2)Acceptor 处理客户端连接请求
3)Handlers 执行非阻塞读/写
一个利用了Reactor模式的NIO服务端:
public class NIOServer {
private static final Logger LOGGER = LoggerFactory.getLogger(NIOServer.class);
public static void main(String[] args) throws IOException {
Selector selector = Selector.open();
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.configureBlocking(false);
serverSocketChannel.bind(new InetSocketAddress(1234));
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
while (true) {
if(selector.selectNow() < 0) {
continue;
}
//获取注册的channel
Set keys = selector.selectedKeys();
//遍历所有的key
Iterator iterator = keys.iterator();
while(iterator.hasNext()) {
SelectionKey key = iterator.next();
iterator.remove();
//如果通道上有事件发生
if (key.isAcceptable()) {
//获取该通道
ServerSocketChannel acceptServerSocketChannel = (ServerSocketChannel) key.channel();
SocketChannel socketChannel = acceptServerSocketChannel.accept();
socketChannel.configureBlocking(false);
LOGGER.info("Accept request from {}", socketChannel.getRemoteAddress());
//同时将SelectionKey标记为可读,以便读取。
SelectionKey readKey = socketChannel.register(selector, SelectionKey.OP_READ);
//利用SelectionKey的attache功能绑定Acceptor 如果有事情,触发Acceptor
//Processor对象为自定义处理请求的类
readKey.attach(new Processor());
} else if (key.isReadable()) {
Processor processor = (Processor) key.attachment();
processor.process(key);
}
}
}
}
}
/**
* Processor类中设置一个线程池来处理请求,
* 这样就可以充分利用多线程的优势
*/
class Processor {
private static final Logger LOGGER = LoggerFactory.getLogger(Processor.class);
private static final ExecutorService service = Executors.newFixedThreadPool(16);
public void process(final SelectionKey selectionKey) {
service.submit(new Runnable() {
@Override
public void run() {
ByteBuffer buffer = null;
SocketChannel socketChannel = null;
try {
buffer = ByteBuffer.allocate(1024);
socketChannel = (SocketChannel) selectionKey.channel();
int count = socketChannel.read(buffer);
if (count < 0) {
socketChannel.close();
selectionKey.cancel();
LOGGER.info("{}\t Read ended", socketChannel);
} else if(count == 0) {
}
} catch (IOException e) {
e.printStackTrace();
}
LOGGER.info("{}\t Read message {}", socketChannel, new String(buffer.array()));
}
});
}
}
这种方式带来的好处也是不言而喻的。利用多路复用机制避免了线程的阻塞,提高了连接的数量。
一个线程就可以管理多个socket,只有当socket真正有读写事件发生才会占用资源来进行实际的读写操作。虽然多线程+ 阻塞IO 达到类似的效果,但是由于在多线程 + 阻塞IO 中,每个socket对应一个线程,这样会造成很大的资源占用,并且尤其是对于长连接来说,线程的资源一直不会释放,如果后面陆续有很多连接的话,就会造成性能上的瓶颈。
另外多路复用IO为何比非阻塞IO模型的效率高是因为:在非阻塞IO中,不断地询问socket状态是通过用户线程去进行的,而在多路复用IO中,轮询每个socket状态是内核在进行的,这个效率要比用户线程要高的多。
3、文件锁定
NIO中的文件通道(FileChannel)在读写数据的时候主要使用了阻塞模式,它不支持非阻塞模式的读写,而且FileChannel的对象是不能够直接实例化的, 他的实例只能通过getChannel()从一个打开的文件对象上边读取(RandomAccessFile、 FileInputStream、FileOutputStream),并且通过调用getChannel()方法返回一个 Channel对象去连接同一个文件,也就是针对同一个文件进行读写操作。
文件锁的出现解决了很多Java应用程序和非Java程序之间共享文件数据的问题,在以前的JDK版本中,没有文件锁机制使得Java应用程序和其他非Java进程程序之间不能够针对同一个文件共享数据,有可能造成很多问题,JDK1.4里面有了FileChannel,它的锁机制使得文件能够针对很多非 Java应用程序以及其他Java应用程序可见。但是Java里面 的文件锁机制主要是基于共享锁模型,在不支持共享锁模型的操作系统上,文件锁本身也起不了作用,JDK1.4使用文件通道读写方式可以向一些文件发送锁请求,
FileChannel的 锁模型主要针对的是每一个文件,并不是每一个线程和每一个读写通道,也就是以文件为中心进行共享以及独占,也就是文件锁本身并不适合于同一个JVM的不同 线程之间。
相应api:
// 如果请求的锁定范围是有效的,阻塞直至获取锁
public final FileLock lock()
// 尝试获取锁非阻塞,立刻返回结果
public final FileLock tryLock()
// 第一个参数:要锁定区域的起始位置
// 第二个参数:要锁定区域的尺寸,
// 第三个参数:true为共享锁,false为独占锁
public abstract FileLock lock (long position, long size, boolean shared)
public abstract FileLock tryLock (long position, long size, boolean shared)
锁定区域的范围不一定要限制在文件的size值以内,锁可以扩展从而超出文件尾。因此,我们可以提前把待写入数据的区域锁定,我们也可以锁定一个不包含任何文件内容的区域,比如文件最后一个字节以外的区域。如果之后文件增长到达那块区域,那么你的文件锁就可以保护该区域的文件内容了。相反地,如果你锁定了文件的某一块区域,然后文件增长超出了那块区域,那么新增加 的文件内容将不会受到您的文件锁的保护。
文件锁示例:
public class NIOLock {
private static final Logger LOGGER = LoggerFactory.getLogger(NIOServer.class);
public static void main(String[] args) throws IOException {
FileChannel fileChannel = new RandomAccessFile("c://1.txt", "rw").getChannel();
// 写入4个字节
fileChannel.write(ByteBuffer.wrap("abcd".getBytes()));
// 将前2个字节区域锁定(共享锁)
FileLock lock1 = fileChannel.lock(0, 2, true);
// 当前锁持有锁的类型(共享锁/独占锁)
lock1.isShared();
// IOException 不能修改只读的共享区域
// fileChannel.write(ByteBuffer.wrap("a".getBytes()));
// 可以修改共享锁之外的区域,从第三个字节开始写入
fileChannel.write(ByteBuffer.wrap("ef".getBytes()), 2);
// OverlappingFileLockException 重叠的文件锁异常
// FileLock lock2 = fileChannel.lock(0, 3, true);
// FileLock lock3 = fileChannel.lock(0, 3, false);
//得到创建锁的通道
lock1.channel();
//锁的起始位置
long position = lock1.position();
//锁的范围
long size = lock1.size();
//判断锁是否与指定文件区域有重叠
lock1.overlaps(position, size);
// 记得用try/catch/finally{release()}方法释放锁
lock1.release();
}
}
完整的NIO Socket客户端和服务端代码示例:
服务端:
public class Server {
//标识数字/
private int flag = 0;
//缓冲区大小/
private int BLOCK = 4096;
//接受数据缓冲区/
private ByteBuffer sendbuffer = ByteBuffer.allocate(BLOCK);
//发送数据缓冲区/
private ByteBuffer receivebuffer = ByteBuffer.allocate(BLOCK);
private Selector selector;
public static void main(String[] args) throws IOException {
// TODO Auto-generated method stub
int port = 7788;
Server server = new Server(port);
server.listen();
}
public Server(int port) throws IOException {
// 打开服务器套接字通道
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
// 服务器配置为非阻塞
serverSocketChannel.configureBlocking(false);
// 检索与此通道关联的服务器套接字
ServerSocket serverSocket = serverSocketChannel.socket();
// 进行服务的绑定
serverSocket.bind(new InetSocketAddress(port));
// 通过open()方法找到Selector
selector = Selector.open();
// 注册到selector,等待连接
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
System.out.println("Server Start----7788:");
}
// 监听
private void listen() throws IOException {
while (true) {
// 选择一组键,并且相应的通道已经打开
selector.select();
// 返回此选择器的已选择键集。
Set selectionKeys = selector.selectedKeys();
Iterator iterator = selectionKeys.iterator();
while (iterator.hasNext()) {
SelectionKey selectionKey = iterator.next();
iterator.remove();
handleKey(selectionKey);
}
}
}
// 处理请求
private void handleKey(SelectionKey selectionKey) throws IOException {
// 接受请求
ServerSocketChannel server = null;
SocketChannel client = null;
String receiveText;
String sendText;
int count = 0;
// 测试此键的通道是否已准备好接受新的套接字连接。
if (selectionKey.isAcceptable()) {
// 返回为之创建此键的通道。
server = (ServerSocketChannel) selectionKey.channel();
// 接受到此通道套接字的连接。
// 此方法返回的套接字通道(如果有)将处于阻塞模式。
client = server.accept();
// 配置为非阻塞
client.configureBlocking(false);
// 注册到selector,等待连接
client.register(selector, SelectionKey.OP_READ);
} else if (selectionKey.isReadable()) {
// 返回为之创建此键的通道。
client = (SocketChannel) selectionKey.channel();
//将缓冲区清空以备下次读取
receivebuffer.clear();
//读取服务器发送来的数据到缓冲区中
count = client.read(receivebuffer);
if (count > 0) {
receiveText = new String(receivebuffer.array(), 0, count);
System.out.println("服务器端接受客户端数据--:" + receiveText);
client.register(selector, SelectionKey.OP_WRITE);
}
} else if (selectionKey.isWritable()) {
//将缓冲区清空以备下次写入
sendbuffer.clear();
// 返回为之创建此键的通道。
client = (SocketChannel) selectionKey.channel();
sendText = "message from server--" + flag++;
//向缓冲区中输入数据
sendbuffer.put(sendText.getBytes());
//将缓冲区各标志复位,因为向里面put了数据标志被改变要想从中读取数据发向服务器,就要复位
sendbuffer.flip();
//输出到通道
client.write(sendbuffer);
System.out.println("服务器端向客户端发送数据--:" + sendText);
client.register(selector, SelectionKey.OP_READ);
}
}
}
客户端:
public class Client {
//标识数字/
private static int flag = 0;
//缓冲区大小/
private static int BLOCK = 4096;
//接受数据缓冲区/
private static ByteBuffer sendbuffer = ByteBuffer.allocate(BLOCK);
//发送数据缓冲区/
private static ByteBuffer receivebuffer = ByteBuffer.allocate(BLOCK);
//服务器端地址/
private final static InetSocketAddress SERVER_ADDRESS = new InetSocketAddress(
"localhost", 7788);
public static void main(String[] args) throws IOException {
// TODO Auto-generated method stub
// 打开socket通道
SocketChannel socketChannel = SocketChannel.open();
// 设置为非阻塞方式
socketChannel.configureBlocking(false);
// 打开选择器
Selector selector = Selector.open();
// 注册连接服务端socket动作
socketChannel.register(selector, SelectionKey.OP_CONNECT);
// 连接
socketChannel.connect(SERVER_ADDRESS);
// 分配缓冲区大小内存
Set selectionKeys;
Iterator iterator;
SelectionKey selectionKey;
SocketChannel client;
String receiveText;
String sendText;
int count = 0;
while (true) {
//选择一组键,其相应的通道已为 I/O 操作准备就绪。
//此方法执行处于阻塞模式的选择操作。
selector.select();
//返回此选择器的已选择键集。
selectionKeys = selector.selectedKeys();
//System.out.println(selectionKeys.size());
iterator = selectionKeys.iterator();
while (iterator.hasNext()) {
selectionKey = iterator.next();
if (selectionKey.isConnectable()) {
System.out.println("client connect");
client = (SocketChannel) selectionKey.channel();
// 判断此通道上是否正在进行连接操作。
// 完成套接字通道的连接过程。
if (client.isConnectionPending()) {
client.finishConnect();
System.out.println("完成连接!");
sendbuffer.clear();
sendbuffer.put("Hello,Server".getBytes());
sendbuffer.flip();
client.write(sendbuffer);
}
client.register(selector, SelectionKey.OP_READ);
} else if (selectionKey.isReadable()) {
client = (SocketChannel) selectionKey.channel();
//将缓冲区清空以备下次读取
receivebuffer.clear();
//读取服务器发送来的数据到缓冲区中
count = client.read(receivebuffer);
if (count > 0) {
receiveText = new String(receivebuffer.array(), 0, count);
System.out.println("客户端接受服务器端数据--:" + receiveText);
client.register(selector, SelectionKey.OP_WRITE);
}
} else if (selectionKey.isWritable()) {
sendbuffer.clear();
client = (SocketChannel) selectionKey.channel();
sendText = "message from client--" + (flag++);
sendbuffer.put(sendText.getBytes());
//将缓冲区各标志复位,因为向里面put了数据标志被改变要想从中读取数据发向服务器,就要复位
sendbuffer.flip();
client.write(sendbuffer);
System.out.println("客户端向服务器端发送数据--:" + sendText);
client.register(selector, SelectionKey.OP_READ);
}
}
selectionKeys.clear();
}
}
}
Java之NIO(非阻塞IO)