JAVA基础-NIO
- 传统IO:使用阻塞IO模型,基于字节流和字符流,进行文件读写,使用Socket和ServerSocket进行网络传输。
- 存在的问题:
- 当线程在等待IO完成的时候,是无法执行其他任务,导致资源空等浪费。
- 一个Socket对应一个线程,Socket是基于字节流进行文件传输,如果大量的Socket,就会存在大量的上下文切换和阻塞,降低系统性能
- NIO:使用非阻塞模型,基于通道(Channel)和缓冲区(Buffer)进行文件读写,以及使用SocketChannel和ServerSocketChannel进行网络传输
- 优势:允许线程在等待IO的时候执行其他任务。这种模式通过使用选择器(Selector)来监控多个通道上的事件,实现更高的性能和伸缩性
- 实际上传统的IO包已经使用NIO重新实现过,即使我们不显示的使用NIO变成,也能从中获益
package org.example.code_case.javaIo;import org.junit.jupiter.api.Test;import java.io.*;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;public class 传统IO和NIO的对比 {String sourcePath = "/src/main/java/org/example/code_case/javaIo/testFile.iso";String targetPath1 = "/src/main/java/org/example/code_case/javaIo/testFileIO.iso";String targetPath2 = "/src/main/java/org/example/code_case/javaIo/testFileNIO.iso";@Testpublic void testDemo() {Thread ioThread = new Thread(() -> {long startTime = System.currentTimeMillis();try (BufferedInputStream bufferedInputStream = new BufferedInputStream(new FileInputStream(sourcePath));BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(new FileOutputStream(targetPath1));) {byte[] bytes = new byte[8192];int len;while ((len = bufferedInputStream.read(bytes)) != -1) {bufferedOutputStream.write(bytes, 0, len);}} catch (IOException e) {throw new RuntimeException(e);}long endTime = System.currentTimeMillis();System.out.println("传统IO time:" + (endTime - startTime));});Thread nioThread = new Thread(() -> {long startTime = System.currentTimeMillis();try (RandomAccessFile readRw = new RandomAccessFile(sourcePath, "rw");RandomAccessFile writeRw = new RandomAccessFile(targetPath2, "rw");) {try (FileChannel readChannel = readRw.getChannel();FileChannel writeChannel = writeRw.getChannel();) {// 创建并使用 ByteBuffer 传输数据ByteBuffer byteBuffer = ByteBuffer.allocate(8192);while (readChannel.read(byteBuffer) > 0) {byteBuffer.flip();writeChannel.write(byteBuffer);byteBuffer.clear();}}} catch (IOException e) {throw new RuntimeException(e);}long endTime = System.currentTimeMillis();System.out.println("NIO time:" + (endTime - startTime));});ioThread.start();nioThread.start();try {ioThread.join();nioThread.join();} catch (InterruptedException e) {throw new RuntimeException(e);}}}
NIO time:24266
传统IO time:24267
当文件越大,传统IO和NIO的速度差异越小
- 既然如此可以不使用NIO吗?
- 答案:不行,IO应用的场景:文件IO、网络IO
- 而NIO的主战场为网络IO
- NIO设计的目的就是为了解决传统IO在大量并发连接时的性能瓶颈问题。传统IO在网络通信中使用阻塞式IO模型,并且会为每个连接分配一个线程,所以系统资源称为关键瓶颈。而NIO采用非阻塞式IO和IO多路复用,可以在单个线程中处理多个并发连接,从而提高网络传输的性能
- NIO特点:
- 非阻塞IO:在等待IO完成时,不会阻塞,可以执行其他任务,节省系统资源
- NIO的非阻塞性体现在:读或者写的时候,如果通道无数据,则会先返回,当数据准备好后告知selector,不会在读或写时由于数据未准备好而阻塞。
- IO多路复用:一个线程能够同时监控多个IO通道,,并在IO事件准备好的时处理他们
- 提供ByteBuffer类,可以高效的管理缓冲区
NIO 和传统 IO 在网络传输中的差异
package org.example.code_case.javaIo.NIO和IO的对比;import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;public class IOServer {public static void main(String[] args) {try (ServerSocket serverSocket = new ServerSocket(8888);) {while (true) {try (Socket accept = serverSocket.accept();InputStream in = accept.getInputStream();OutputStream out = accept.getOutputStream();) {byte[] bytes = new byte[1024];int len;while ((len = in.read( bytes)) != -1){//模拟操作耗时TimeUnit.MILLISECONDS.sleep(10);out.write(bytes, 0, len);}}catch (InterruptedException e) {throw new RuntimeException(e);}}} catch (IOException e) {throw new RuntimeException(e);}
}
package org.example.code_case.javaIo.NIO和IO的对比;import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;public class NIOServer {public static void main(String[] args) {ExecutorService executorService = Executors.newFixedThreadPool(5);try (ServerSocketChannel open = ServerSocketChannel.open();) {//绑定端口open.bind(new InetSocketAddress(9999));//设置非阻塞模式open.configureBlocking(false);//创建selectorSelector selector = Selector.open();open.register(selector, SelectionKey.OP_ACCEPT);//事件类型:服务端接受客户端连接//处理事件while (true) {//阻塞等待事件发生selector.select();Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();//遍历就绪事件while (iterator.hasNext()) {SelectionKey key = iterator.next();//处理完成之后删除iterator.remove();//判断事件类型if (key.isAcceptable()) {//有新连接请求ServerSocketChannel channel = (ServerSocketChannel) key.channel();SocketChannel accept = channel.accept();//设置为非阻塞模式accept.configureBlocking(false);//为新连接附加写队列Queue<ByteBuffer> byteBufferQueue = new ConcurrentLinkedQueue<>();accept.register(selector, SelectionKey.OP_READ, byteBufferQueue);//事件类型:通道可读}if (key.isReadable()) {//通道可读SocketChannel channel = (SocketChannel) key.channel();ByteBuffer allocate = ByteBuffer.allocate(1024);//从SocketChannel中读取数据写入缓冲区,//当通道没有数据可读立即返回0,而读取完返回-1int read = channel.read(allocate);if (read > 0) {//切换读模式allocate.flip();byte[] data = new byte[allocate.remaining()];allocate.get(data);executorService.execute(() -> processData(data, channel, key));}if (read == -1) {//关闭通道closeChannel(key);}}if (key.isValid() && key.isWritable()) {//通道可写SocketChannel channel = (SocketChannel) key.channel();Queue<ByteBuffer> queue = (Queue<ByteBuffer>) key.attachment();synchronized (queue) {ByteBuffer buffer;while ((buffer = queue.peek()) != null) {channel.write(buffer);if (buffer.hasRemaining()) break;//未写完,等待下次写事件queue.poll();//已写完,移除缓冲区}if (queue.isEmpty()) {key.interestOps(SelectionKey.OP_READ);//取消写事件监听,监听读事件closeChannel(key);}}}}}} catch (IOException e) {throw new RuntimeException(e);}}private static void processData(byte[] data, SocketChannel channel, SelectionKey key) {try {//模拟耗时操作TimeUnit.MILLISECONDS.sleep(10);//生成相应数据ByteBuffer wrap = ByteBuffer.wrap(data);Queue<ByteBuffer> queue = (Queue<ByteBuffer>) key.attachment();synchronized (queue) {queue.add(wrap);key.interestOps(SelectionKey.OP_READ | SelectionKey.OP_WRITE);//同时监听写事件和读事件,OP_READ是因为数据可没有一次性读完}key.selector().wakeup();//唤醒selector立即处理新事件} catch (InterruptedException e) {throw new RuntimeException(e);}}private static void closeChannel(SelectionKey key) {try {key.cancel();key.channel().close();} catch (IOException e) {e.printStackTrace();}}
}
@Test
public void ioTest() throws InterruptedException {ExecutorService ioService = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());Runnable ioThread = () -> {try (Socket socket = new Socket("127.0.0.1", 8888);InputStream in = socket.getInputStream();OutputStream out = socket.getOutputStream();) {out.write("hello world,".getBytes());in.read(new byte[1024]);} catch (IOException e) {throw new RuntimeException(e);}};long start = System.currentTimeMillis();for (int i = 0; i < 10000; i++) {ioService.execute(ioThread);}ioService.shutdown();ioService.awaitTermination(1, TimeUnit.DAYS);long end = System.currentTimeMillis();System.out.println("传统IO,处理10000个请求 time:" + (end - start));
}@Test
public void nioTest() throws InterruptedException {ExecutorService nioService = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());Runnable nioThread = () -> {try (SocketChannel open = SocketChannel.open();) {open.connect(new InetSocketAddress("localhost", 9999));//给服务端发消息ByteBuffer buffer = ByteBuffer.wrap("hello world,".getBytes());open.write(buffer);buffer.clear();//接收服务端消息open.read(buffer);} catch (IOException e) {throw new RuntimeException(e);}};long start1 = System.currentTimeMillis();for (int i = 0; i < 10000; i++) {nioService.execute(nioThread);}nioService.shutdown();nioService.awaitTermination(1, TimeUnit.DAYS);long end1 = System.currentTimeMillis();System.out.println("NIO,处理10000个请求 time:" + (end1 - start1));
}
传统IO,处理10000个请求 time:122803ms
NIO,处理10000个请求 time:23950ms
从运行结果能够很明显的看出NIO在处理网络高并发请求的优势
JavaIO:BIO、NIO、AIO分别是什么?有什么区别?
- I/O:即输入/输出,通常用于文件读写或网络通信
- 随着Java的发展,共有3种I/O模型,BIO、NIO、AIO
- BIO:同步阻塞IO模型,也称传统IO,是面向数据流的,即一个流产生一个字节数据。效率低、网络并发低
- 同步阻塞IO模型:发送请求,等待对方应答,期间不能做任何事情
- 适用场景:连接少,并发低的架构,例如文件读写
- NIO:同步非阻塞IO模型,也称NewIO,是面向数据块的,即每次操作在一步中产生或消费一个数据块。引进Channel通道、BtyeBuffer缓冲区、Selector选择器组件。效率高、网络并发高
- 同步非阻塞IO模型:发送请求,通过轮询和选择器检查IO状态,期间可以做其他事情。
- 适用场景:连接数目多,但连接时间短的场景,例如聊天室
- AIO:异步非阻塞IO模型,建立在NIO的基础上实现的异步
- 异步非阻塞IO模型:发送请求,然后去其他事情,请求完成自动通知
- 适用场景:连接数目多,但连接时间长的场景,例如图片服务器
@Test
public void test2() throws IOException, ExecutionException, InterruptedException {//读取文件AsynchronousFileChannel open = AsynchronousFileChannel.open(path, StandardOpenOption.READ);ByteBuffer allocate = ByteBuffer.allocate(1024);open.read(allocate, 0, allocate, new CompletionHandler<Integer, ByteBuffer>() {@Overridepublic void completed(Integer result, ByteBuffer attachment) {attachment.flip();System.out.println("读取到的数据" + StandardCharsets.UTF_8.decode(attachment));attachment.clear();try {open.close();} catch (IOException e) {throw new RuntimeException(e);}}@Overridepublic void failed(Throwable exc, ByteBuffer attachment) {System.out.println("读取失败");exc.printStackTrace();}});//写入文件AsynchronousFileChannel open1 = AsynchronousFileChannel.open(path, StandardOpenOption.CREATE,StandardOpenOption.WRITE);ByteBuffer wrap = ByteBuffer.wrap("hello".getBytes(StandardCharsets.UTF_8));Future<Integer> write = open1.write(wrap, 0);write.get();open1.close();System.out.println("写入完成");}
Buffer和Channel
- 在NIO中有两个重要的组件:Buffer缓冲区和Channel通道
- 其中Channel就像轨道,只负责运送。Buffer就像火车,负责承载数据
Buffer
- Buffer:IntBuffer、CharBuffer、ByteBuffer、LongBuffer
- 缓冲区的核心方法就是put和get方法
- Buffer类维护了4个核心参数:
- 容量Capacity:缓冲区的最大容量,在创建时确定,不能改变,底层是数组
- 上界limit:缓冲区中数据的总数,代表当前缓冲区有多少数据,默认为0,只有在调用
flip()
方法之后才有意义 - 位置Position:下一个要被读或写的位置,Position会自动由相应的get和put方法更新
- 标记mark:用于记录上一次读写的位置
- 如果想要从Buffer中读取数据,需要调用
flip()
,此方法将Postion的值给了limit,将Postion置为0
Channel
- 通道分为两类:文件通道和套接字通道
- FileChannel:用于文件IO的通道,支持对文件的读写和追加操作。无法设置非阻塞模式
- SocketChannel:用于TCP套接字的网络通信,支持非阻塞模式,与Selector一同使用提供高效的网络通信。与之匹配的是ServerSocketChannel,用于监听TCP套接字的通道,也支持非阻塞模式,ServerSocketChannel负责监听新的连接请求,接受到请求后创建一个新的SocketChannel处理数据传输
- DatagramChannel:用于 UDP 套接字 I/O 的通道。DatagramChannel 支持非阻塞模式,可以发送和接收,数据报包,适用于无连接的、不可靠的网络通信。
文件通道FileChannel
- 使用内存映射文件
MappedByteBuffer
当方式实现文件复制功能:
/*** MappedByteBuffer 使用ByteBuffer的子类,它可以直接将文件映射到内存中,以便通过直接操作内存来实现对文件的读写。* 这种方式可以提高文件 I/O 的性能,因为操作系统可以直接在内存和磁盘之间传输数据,无需通过Java 应用程序进行额外的数据拷贝。* 需要注意:MappedByteBuffer进行文件操作时,数据的修改不会立即写入磁盘,可以通过调用force()方法将数据立即写入*/
@Test
public void test3() throws IOException {try (FileChannel sourceChannel = FileChannel.open(path, StandardOpenOption.READ);FileChannel targetChannel = FileChannel.open(targetPath, StandardOpenOption.CREATE, StandardOpenOption.WRITE, StandardOpenOption.READ);) {long size = sourceChannel.size();MappedByteBuffer sourceMappedByteBuffer = sourceChannel.map(FileChannel.MapMode.READ_ONLY, 0, size);MappedByteBuffer targetMappedByteBuffer = targetChannel.map(FileChannel.MapMode.READ_WRITE, 0, size);targetMappedByteBuffer.put(sourceMappedByteBuffer);}
}
- 通道之间通过
transferTo()
实现数据的传输:
/*** 这种传输方式可以避免将文件数据在用 户空间和内核空间之间进行多次拷贝,提高了文件传输的性能。* transferTo()方法可以将数据从一个通道传输到另一个通道,而不需要通过中间缓冲区。* position:源文件中开始传输的位置* count:传输的字节数* targetChannel:目标通道* <p>* 注意:transferTo()可能无法一次传输所有请求的字节,需要使用循环来确保所有的字节都被传输*/
@Test
public void test4() throws IOException {try (FileChannel sourceChannel = FileChannel.open(path, StandardOpenOption.READ);FileChannel targetChannel = FileChannel.open(targetPath, StandardOpenOption.CREATE, StandardOpenOption.WRITE, StandardOpenOption.READ);) {long size = sourceChannel.size();long position = 0;while (position < size) {long offer = sourceChannel.transferTo(position, size - position, targetChannel);position += offer;}}
}
直接缓冲区与非直接缓冲区
- 直接缓冲区:
ByteBuffer.allocateDirect(int size)
、FileChannel.map()
创建的MappedByteBuffer
都是直接缓冲区- 直接分配在JVM堆内存之外的本地内存中,由操作系统直接管理
- 在读写时直接操作本地内存,避免了数据复制,提高性能,但是创建和销毁成本高,占用物理内存
- 适用场景:网络IO操作、频繁的IO操作
- 非直接缓冲区
ByteBuffer.allocate(int size)
- 分配在JVM的堆内存中,由JVM管理
- 在读写操作时,需要将数据从堆内存复制到操作系统的本地内存,在进行I/O操作,但是创建成本低,内存管理简单
- 适用场景:临时数据处理、小文件操作、数据转换处理
异步文件通道AsynchronousFileChannel
- AsynchronoursFileChannel是一个异步文件通道,提供了使用异步的方式对文件读写、打开关闭等操作
Path file = Paths.get("example.txt");
AsynchronousFileChannel fileChannel = AsynchronousFileChannel.open(file,
StandardOpenOption.READ, StandardOpenOption.WRITE);
- 两种异步操作:
- Future方式:使用Future对象来跟踪异步操作的完成情况,当我们调用read()和write()方法的时候,会返回一个Future对象,可以检查任务是否完成,进而操作结果
ByteBuffer buffer = ByteBuffer.allocate(1024);
long position = 0;
Future<Integer> result = fileChannel.read(buffer, position);
while (!result.isDone()) {// 执行其他操作
}
int bytesRead = result.get();
System.out.println("Bytes read: " + bytesRead);
- CompletionHandler方法:需要实现CompletionHandler接口来定义异步操作成功或失败的操作。相当于回调函数
//读取文件
AsynchronousFileChannel open = AsynchronousFileChannel.open(path, StandardOpenOption.READ);
ByteBuffer allocate = ByteBuffer.allocate(1024);AtomicInteger position = new AtomicInteger(0);
CountDownLatch countDownLatch = new CountDownLatch(1);
open.read(allocate, position.get(), allocate, new CompletionHandler<Integer, ByteBuffer>() {@Overridepublic void completed(Integer result, ByteBuffer attachment) {try {TimeUnit.SECONDS.sleep(1);} catch (InterruptedException e) {throw new RuntimeException(e);}if(result > 0){position.addAndGet(result);attachment.flip();System.out.println("读取到的数据" + StandardCharsets.UTF_8.decode(attachment));attachment.clear();open.read(allocate, position.get(), allocate, this);}else{try {countDownLatch.countDown();System.out.println("关闭");open.close();} catch (IOException e) {throw new RuntimeException(e);}}}@Overridepublic void failed(Throwable exc, ByteBuffer attachment) {System.out.println("读取失败");exc.printStackTrace();}
});countDownLatch.await();
Paths类
- Paths类主要用于操作文件和目录的路径。
//获取当前工作路径
String currentPath = System.getProperty("user.dir");
System.out.println("当前工作目录:" + currentPath);
//获取指定路径
Path resolve = Paths.get(currentPath).resolve("src/main/resources/不同IO的使用方式.txt");
System.out.println("获取文件名:" + resolve.getFileName());
System.out.println("获取父目录:" + resolve.getParent());
System.out.println("获取根目录:" + resolve.getRoot());
System.out.println("简化路径:" + resolve.normalize());
System.out.println("将相对路径转化为绝对路径:" + resolve.toAbsolutePath());
Files类
- Files类提供了大量的静态方法,用于处理文件系统中的文件和目录。包括文件的创建、删除、复制、移动等操作,以及读取和设置文件属性
exists(Path path, LinkOption... options);
检查文件或目录是否存在
- LinkOption是一个枚举类,定义了如何处理文件系统的符号链接的选项。符号链接是一种特殊类型的文件,在Unix和类Unix系统常见,在Win系统中类似快捷方式
createFile(Path path, FileAttribute<?>... attrs);
创建一个新的空文件
- FileAttribute是一个泛型接口,用于处理各种不同类型的属性
Path parentPath = Paths.get(System.getProperty("user.dit")).resolve("src/main/resources");
Path resolve = parentPath.resolve("FileTest.txt");//设置文件权限:
//rw- 文件所有者可读写不能执行
//r-- 文件所在组可读不能写不能执行
//--- 其他用户不能读写不能执行
Set<PosixFilePermission> posixFilePermissions = PosixFilePermissions.fromString("rw-r-----");
FileAttribute<Set<PosixFilePermission>> fileAttribute = PosixFilePermissions.asFileAttribute(posixFilePermissions);
Files.createFile(resolve, fileAttribute);
createDirectory(Path dir, FileAttribute<?>... attrs);
创建一个新的文件夹delete(Path path);
删除文件或目录copy(Path source, Path target, CopyOption... options);
复制文件或目录
- 在Java NIO中,有两个实现了CopyOption接口的枚举类:StandardCopyOption和LinkOption
- StandardCopyOption:
REPLACE_EXISTING
如果文件已存在,copy会替换目标文件,如果不指定,则抛异常。COPY_ATTRIBUTES
复制时同样复制文件属性,如果不指定,则文件具有默认属性
move(Path source, Path target, CopyOption... options);
移动或重命名文件或目录readAllLines(Path path, Charset cs);
读取文件的所有行到一个字符串列表write(Path path, Iterable<? extends CharSequence> lines, Charset cs, OpenOption... options);
将字符串列表写入文件
- OpenOption是一个用于配置文件操作的接口,它提供了在使用 Files.newByteChannel() 、 Files.newInputStream() 、 Files.newOutputStream() 、 AsynchronousFileChannel.open() 和 FileChannel.open() 方法时定制行为的选项。
- StandardOpenOption实现了OpenOption接口的枚举类:
- read、write、append、truncate_existing、create、create_new、delete_on_close、sparse、sync、dsync
newBufferedReader(Path path, Charset cs) 和 newBufferedWriter(Path path, Charset cs, OpenOption... options);
创建BufferedrReader和BufferedWriteFiles.walkFileTree();
递归访问目录结构中的所有文件和目录,并允许对这些文件和目录执行自定义操作
@Test
public void test4() throws IOException {Path parentPathDir = Paths.get(System.getProperty("user.dir")).resolve("src/main/java/org/example/code_case");Files.walkFileTree(parentPathDir, new SimpleFileVisitor<Path>() {//在访问目录之前调用的@Overridepublic FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {System.out.println("访问目录之前:" + dir);return super.preVisitDirectory(dir, attrs);}//在访问目录之后调用的@Overridepublic FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {System.out.println("访问目录之后:" + dir);return super.postVisitDirectory(dir, exc);}//在访问文件时调用的@Overridepublic FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {System.out.println("访问文件:" + file);return super.visitFile(file, attrs);}//在访问文件时发生异常时调用的@Overridepublic FileVisitResult visitFileFailed(Path file, IOException exc) throws IOException {System.out.println("访问文件时发生异常:" + file);return super.visitFileFailed(file, exc);}});
}
- 其中FileVisitResult枚举包括:continue、terminate、skip_siblings(跳过兄弟节点,然后继续)、skip_subtree(跳过子树,然后继续)仅在preVisitDirectory方法返回时才有意义
@Test
public void test5() throws IOException {//文件搜索Path parentPathDir = Paths.get(System.getProperty("user.dir")).resolve("src/main/java/org/example/code_case");class MyFileVisitor extends SimpleFileVisitor<Path> {public boolean exsits = false;final String fileName = "README.txt";@Overridepublic FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {String currentFileName = file.getFileName().toString();if (fileName.equalsIgnoreCase(currentFileName)) {exsits = true;return FileVisitResult.TERMINATE;}return super.visitFile(file, attrs);}};MyFileVisitor myFileVisitor = new MyFileVisitor();Files.walkFileTree(parentPathDir, myFileVisitor);if(myFileVisitor.exsits){System.out.println("文件存在");}else {System.out.println("文件不存在");}}