材料网站建设,施工企业营销人员培训,遵义建站,网站的专题图怎么做一、NIO三大组件
NIO的三大组件分别是Channel#xff0c;Buffer与Selector
Java NIO系统的核心在于#xff1a;通道(Channel)和缓冲区(Buffer)。通道表示打开到 IO 设备(例如#xff1a;文件、套接字)的连接。若需要使用 NIO 系统#xff0c;需要获取用于连接 IO 设备的通…一、NIO三大组件
NIO的三大组件分别是ChannelBuffer与Selector
Java NIO系统的核心在于通道(Channel)和缓冲区(Buffer)。通道表示打开到 IO 设备(例如文件、套接字)的连接。若需要使用 NIO 系统需要获取用于连接 IO 设备的通道以及用于容纳数据的缓冲区。然后操作缓冲区对数据进行处理
简而言之通道负责传输缓冲区负责存储
常见的Channel有以下四种其中FileChannel主要用于文件传输其余三种用于网络通信
FileChannelDatagramChannelSocketChannelServerSocketChannel
Buffer有以下几种其中使用较多的是ByteBuffer
ByteBuffer MappedByteBufferDirectByteBufferHeapByteBufferShortBufferIntBufferLongBufferFloatBufferDoubleBufferCharBuffer
1、Selector
在使用Selector之前处理socket连接还有以下两种方法
1.使用多线程技术
为每个连接分别开辟一个线程分别去处理对应的socke连接 这种方法存在以下几个问题
内存占用高 每个线程都需要占用一定的内存当连接较多时会开辟大量线程导致占用大量内存线程上下文切换成本高只适合连接数少的场景 连接数过多会导致创建很多线程从而出现问题
2.使用线程池技术
使用线程池让线程池中的线程去处理连接 这种方法存在以下几个问题
阻塞模式下线程仅能处理一个连接 线程池中的线程获取任务task后只有当其执行完任务之后断开连接后才会去获取并执行下一个任务若socke连接一直未断开则其对应的线程无法处理其他socke连接仅适合短连接场景 短连接即建立连接发送请求并响应后就立即断开使得线程池中的线程可以快速处理其他连接
3.使用选择器
selector 的作用就是配合一个线程来管理多个 channelfileChannel因为是阻塞式的所以无法使用selector获取这些 channel 上发生的事件这些 channel 工作在非阻塞模式下当一个channel中没有执行任务时可以去执行其他channel中的任务。适合连接数多但流量较少的场景 若事件未就绪调用 selector 的 select() 方法会阻塞线程直到 channel 发生了就绪事件。这些事件就绪后select 方法就会返回这些事件交给 thread 来处理
2、ByteBuffer
使用案例
使用方式
向 buffer 写入数据例如调用 channel.read(buffer)调用 flip() 切换至读模式 flip会使得buffer中的limit变为positionposition变为0从 buffer 读取数据例如调用 buffer.get()调用 clear() 或者compact()切换至写模式 调用clear()方法时position0limit变为capacity调用compact()方法时会将缓冲区中的未读数据压缩到缓冲区前面重复以上步骤
使用ByteBuffer读取文件中的内容
public class TestByteBuffer {public static void main(String[] args) {// 获得FileChanneltry (FileChannel channel new FileInputStream(stu.txt).getChannel()) {// 获得缓冲区ByteBuffer buffer ByteBuffer.allocate(10);int hasNext 0;StringBuilder builder new StringBuilder();while((hasNext channel.read(buffer)) 0) {// 切换模式 limitposition, position0buffer.flip();// 当buffer中还有数据时获取其中的数据while(buffer.hasRemaining()) {builder.append((char)buffer.get());}// 切换模式 position0, limitcapacitybuffer.clear();}System.out.println(builder.toString());} catch (IOException e) {}}
} 打印结果 核心属性
0123456789abcdef
字节缓冲区的父类Buffer中有几个核心属性如下
// Invariants: mark position limit capacity
private int mark -1;
private int position 0;
private int limit;
private int capacity;
capacity缓冲区的容量。通过构造函数赋予一旦设置无法更改limit缓冲区的界限。位于limit 后的数据不可读写。缓冲区的限制不能为负并且不能大于其容量position下一个读写位置的索引类似PC。缓冲区的位置不能为负并且不能大于limitmark记录当前position的值。position被改变后可以通过调用reset() 方法恢复到mark的位置。
以上四个属性必须满足以下要求
mark position limit capacity
核心方法
ut()方法
put()方法可以将一个数据放入到缓冲区中。进行该操作后postition的值会1指向下一个可以放入的位置。capacity limit 为缓冲区容量的值。 flip()方法
flip()方法会切换对缓冲区的操作模式由写-读 / 读-写进行该操作后 如果是写模式-读模式position 0 limit 指向最后一个元素的下一个位置capacity不变如果是读-写则恢复为put()方法中的值 get()方法
get()方法会读取缓冲区中的一个值进行该操作后position会1如果超过了limit则会抛出异常注意get(i)方法不会改变position的值
如果想通过get方法重复读取数据
可以调用rewind方法讲position重新置为0或者调用get(int i)方法获取索引 i 的内容它不会移动读指针 rewind()方法
该方法只能在读模式下使用rewind()方法后会恢复position、limit和capacity的值变为进行get()前的值 clean()方法
clean()方法会将缓冲区中的各个属性恢复为最初的状态position 0, capacity limit此时缓冲区的数据依然存在处于“被遗忘”状态下次进行写操作时会覆盖这些数据 mark()和reset()方法
mark()方法会将postion的值保存到mark属性中reset()方法会将position的值改为mark中保存的值
compact()方法
此方法为ByteBuffer的方法而不是Buffer的方法
compact会把未读完的数据向前压缩然后切换到写模式数据前移后原位置的值并未清零写时会覆盖之前的值 clear() VS compact()
clear只是对position、limit、mark进行重置而compact在对position进行设置以及limit、mark进行重置的同时还涉及到数据在内存中拷贝会调用arraycopy。所以compact比clear更耗性能。但compact能保存你未读取的数据将新数据追加到为读取的数据之后而clear则不行若你调用了clear则未读取的数据就无法再读取到了 ByteBuffer.allocate() 与 ByteBuffer.allocateDirect()
allocate()方法返回类型是 class java.nio .HeapByteBuffer
HeapByteBuffer 使用的是 java 堆内存读写效率较低受到GC的影响
allocateDirect() 方法返回类型是 class java.nio .DirectByteBufferDirectByteBuffer 使用的是直接内存 读写效率高(少一次拷贝)不会受GC影响但是分配的效率低因为需要调用系统的分配内存相关接口而且使用不当会造成内存泄漏 所以需要根据情况来判断使用哪种方法进行模式切换
方法调用及演示
ByteBuffer调试工具类
需要先导入netty依赖
dependencygroupIdio.netty/groupIdartifactIdnetty-all/artifactIdversion4.1.51.Final/version
/dependency 创建一个工具类便于观察ByteBuffer的内部结构
import java.nio.ByteBuffer;import io.netty.util.internal.MathUtil;
import io.netty.util.internal.StringUtil;
import io.netty.util.internal.MathUtil.*;/*** author qingm* date 2024/1/12 15:59*/
public class ByteBufferUtil {private static final char[] BYTE2CHAR new char[256];private static final char[] HEXDUMP_TABLE new char[256 * 4];private static final String[] HEXPADDING new String[16];private static final String[] HEXDUMP_ROWPREFIXES new String[65536 4];private static final String[] BYTE2HEX new String[256];private static final String[] BYTEPADDING new String[16];static {final char[] DIGITS 0123456789abcdef.toCharArray();for (int i 0; i 256; i) {HEXDUMP_TABLE[i 1] DIGITS[i 4 0x0F];HEXDUMP_TABLE[(i 1) 1] DIGITS[i 0x0F];}int i;// Generate the lookup table for hex dump paddingsfor (i 0; i HEXPADDING.length; i) {int padding HEXPADDING.length - i;StringBuilder buf new StringBuilder(padding * 3);for (int j 0; j padding; j) {buf.append( );}HEXPADDING[i] buf.toString();}// Generate the lookup table for the start-offset header in each row (up to 64KiB).for (i 0; i HEXDUMP_ROWPREFIXES.length; i) {StringBuilder buf new StringBuilder(12);buf.append(StringUtil.NEWLINE);buf.append(Long.toHexString(i 4 0xFFFFFFFFL | 0x100000000L));buf.setCharAt(buf.length() - 9, |);buf.append(|);HEXDUMP_ROWPREFIXES[i] buf.toString();}// Generate the lookup table for byte-to-hex-dump conversionfor (i 0; i BYTE2HEX.length; i) {BYTE2HEX[i] StringUtil.byteToHexStringPadded(i);}// Generate the lookup table for byte dump paddingsfor (i 0; i BYTEPADDING.length; i) {int padding BYTEPADDING.length - i;StringBuilder buf new StringBuilder(padding);for (int j 0; j padding; j) {buf.append( );}BYTEPADDING[i] buf.toString();}// Generate the lookup table for byte-to-char conversionfor (i 0; i BYTE2CHAR.length; i) {if (i 0x1f || i 0x7f) {BYTE2CHAR[i] .;} else {BYTE2CHAR[i] (char) i;}}}/*** 打印所有内容* param buffer*/public static void debugAll(ByteBuffer buffer) {int oldlimit buffer.limit();buffer.limit(buffer.capacity());StringBuilder origin new StringBuilder(256);appendPrettyHexDump(origin, buffer, 0, buffer.capacity());System.out.println(---------------------------- all ----------------------------------------);System.out.printf(position: [%d], limit: [%d]\n, buffer.position(), oldlimit);System.out.println(origin);buffer.limit(oldlimit);}/*** 打印可读取内容* param buffer*/public static void debugRead(ByteBuffer buffer) {StringBuilder builder new StringBuilder(256);appendPrettyHexDump(builder, buffer, buffer.position(), buffer.limit() - buffer.position());System.out.println(---------------------------- read ---------------------------------------);System.out.printf(position: [%d], limit: [%d]\n, buffer.position(), buffer.limit());System.out.println(builder);}private static void appendPrettyHexDump(StringBuilder dump, ByteBuffer buf, int offset, int length) {if (MathUtil.isOutOfBounds(offset, length, buf.capacity())) {throw new IndexOutOfBoundsException(expected: 0 offset( offset ) offset length( length ) buf.capacity( buf.capacity() ));}if (length 0) {return;}dump.append( ------------------------------------------------- StringUtil.NEWLINE | 0 1 2 3 4 5 6 7 8 9 a b c d e f | StringUtil.NEWLINE -------------------------------------------------------------------------);final int startIndex offset;final int fullRows length 4;final int remainder length 0xF;// Dump the rows which have 16 bytes.for (int row 0; row fullRows; row) {int rowStartIndex (row 4) startIndex;// Per-row prefix.appendHexDumpRowPrefix(dump, row, rowStartIndex);// Hex dumpint rowEndIndex rowStartIndex 16;for (int j rowStartIndex; j rowEndIndex; j) {dump.append(BYTE2HEX[getUnsignedByte(buf, j)]);}dump.append( |);// ASCII dumpfor (int j rowStartIndex; j rowEndIndex; j) {dump.append(BYTE2CHAR[getUnsignedByte(buf, j)]);}dump.append(|);}// Dump the last row which has less than 16 bytes.if (remainder ! 0) {int rowStartIndex (fullRows 4) startIndex;appendHexDumpRowPrefix(dump, fullRows, rowStartIndex);// Hex dumpint rowEndIndex rowStartIndex remainder;for (int j rowStartIndex; j rowEndIndex; j) {dump.append(BYTE2HEX[getUnsignedByte(buf, j)]);}dump.append(HEXPADDING[remainder]);dump.append( |);// Ascii dumpfor (int j rowStartIndex; j rowEndIndex; j) {dump.append(BYTE2CHAR[getUnsignedByte(buf, j)]);}dump.append(BYTEPADDING[remainder]);dump.append(|);}dump.append(StringUtil.NEWLINE -------------------------------------------------------------------------);}private static void appendHexDumpRowPrefix(StringBuilder dump, int row, int rowStartIndex) {if (row HEXDUMP_ROWPREFIXES.length) {dump.append(HEXDUMP_ROWPREFIXES[row]);} else {dump.append(StringUtil.NEWLINE);dump.append(Long.toHexString(rowStartIndex 0xFFFFFFFFL | 0x100000000L));dump.setCharAt(dump.length() - 9, |);dump.append(|);}}public static short getUnsignedByte(ByteBuffer buffer, int index) {return (short) (buffer.get(index) 0xFF);}
}
调用ByteBuffer的方法
public class TestByteBuffer {public static void main(String[] args) {ByteBuffer buffer ByteBuffer.allocate(10);// 向buffer中写入1个字节的数据buffer.put((byte)97);// 使用工具类查看buffer状态ByteBufferUtil.debugAll(buffer);// 向buffer中写入4个字节的数据buffer.put(new byte[]{98, 99, 100, 101});ByteBufferUtil.debugAll(buffer);// 获取数据buffer.flip();ByteBufferUtil.debugAll(buffer);System.out.println(buffer.get());System.out.println(buffer.get());ByteBufferUtil.debugAll(buffer);// 使用compact切换模式buffer.compact();ByteBufferUtil.debugAll(buffer);// 再次写入buffer.put((byte)102);buffer.put((byte)103);ByteBufferUtil.debugAll(buffer);}
}
运行结果 // 向缓冲区写入了一个字节的数据此时postition为1
---------------------------- all ----------------------------------------
position: [1], limit: [10]-------------------------------------------------| 0 1 2 3 4 5 6 7 8 9 a b c d e f |
-------------------------------------------------------------------------
|00000000| 61 00 00 00 00 00 00 00 00 00 |a......... |
-------------------------------------------------------------------------// 向缓冲区写入四个字节的数据此时position为5
---------------------------- all ----------------------------------------
position: [5], limit: [10]-------------------------------------------------| 0 1 2 3 4 5 6 7 8 9 a b c d e f |
-------------------------------------------------------------------------
|00000000| 61 62 63 64 65 00 00 00 00 00 |abcde..... |
-------------------------------------------------------------------------// 调用flip切换模式此时position为0表示从第0个数据开始读取
---------------------------- all ----------------------------------------
position: [0], limit: [5]-------------------------------------------------| 0 1 2 3 4 5 6 7 8 9 a b c d e f |
-------------------------------------------------------------------------
|00000000| 61 62 63 64 65 00 00 00 00 00 |abcde..... |
-------------------------------------------------------------------------
// 读取两个字节的数据
97
98// position变为2
---------------------------- all ----------------------------------------
position: [2], limit: [5]-------------------------------------------------| 0 1 2 3 4 5 6 7 8 9 a b c d e f |
-------------------------------------------------------------------------
|00000000| 61 62 63 64 65 00 00 00 00 00 |abcde..... |
-------------------------------------------------------------------------// 调用compact切换模式此时position及其后面的数据被压缩到ByteBuffer前面去了
// 此时position为3会覆盖之前的数据
---------------------------- all ----------------------------------------
position: [3], limit: [10]-------------------------------------------------| 0 1 2 3 4 5 6 7 8 9 a b c d e f |
-------------------------------------------------------------------------
|00000000| 63 64 65 64 65 00 00 00 00 00 |cdede..... |
-------------------------------------------------------------------------// 再次写入两个字节的数据之前的 0x64 0x65 被覆盖
---------------------------- all ----------------------------------------
position: [5], limit: [10]-------------------------------------------------| 0 1 2 3 4 5 6 7 8 9 a b c d e f |
-------------------------------------------------------------------------
|00000000| 63 64 65 66 67 00 00 00 00 00 |cdefg..... |
------------------------------------------------------------------------- 字符串与ByteBuffer的相互转换
方法一
编码字符串调用getByte方法获得byte数组将byte数组放入ByteBuffer中
解码先调用ByteBuffer的flip方法然后通过StandardCharsets的decoder方法解码
public class Translate {public static void main(String[] args) {// 准备两个字符串String str1 hello;String str2 ;ByteBuffer buffer1 ByteBuffer.allocate(16);// 通过字符串的getByte方法获得字节数组放入缓冲区中buffer1.put(str1.getBytes());ByteBufferUtil.debugAll(buffer1);// 将缓冲区中的数据转化为字符串// 切换模式buffer1.flip();// 通过StandardCharsets解码获得CharBuffer再通过toString获得字符串str2 StandardCharsets.UTF_8.decode(buffer1).toString();System.out.println(str2);ByteBufferUtil.debugAll(buffer1);}
} 运行结果
---------------------------- all ----------------------------------------
position: [5], limit: [16]-------------------------------------------------| 0 1 2 3 4 5 6 7 8 9 a b c d e f |
-------------------------------------------------------------------------
|00000000| 68 65 6c 6c 6f 00 00 00 00 00 00 00 00 00 00 00 |hello...........|
-------------------------------------------------------------------------
hello
---------------------------- all ----------------------------------------
position: [5], limit: [5]-------------------------------------------------| 0 1 2 3 4 5 6 7 8 9 a b c d e f |
-------------------------------------------------------------------------
|00000000| 68 65 6c 6c 6f 00 00 00 00 00 00 00 00 00 00 00 |hello...........|
-------------------------------------------------------------------------Copy
方法二
编码通过StandardCharsets的encode方法获得ByteBuffer此时获得的ByteBuffer为读模式无需通过flip切换模式
解码通过StandardCharsets的decoder方法解码
public class Translate {public static void main(String[] args) {// 准备两个字符串String str1 hello;String str2 ;// 通过StandardCharsets的encode方法获得ByteBuffer// 此时获得的ByteBuffer为读模式无需通过flip切换模式ByteBuffer buffer1 StandardCharsets.UTF_8.encode(str1);ByteBufferUtil.debugAll(buffer1);// 将缓冲区中的数据转化为字符串// 通过StandardCharsets解码获得CharBuffer再通过toString获得字符串str2 StandardCharsets.UTF_8.decode(buffer1).toString();System.out.println(str2);ByteBufferUtil.debugAll(buffer1);}
}
运行结果
---------------------------- all ----------------------------------------
position: [0], limit: [5]-------------------------------------------------| 0 1 2 3 4 5 6 7 8 9 a b c d e f |
-------------------------------------------------------------------------
|00000000| 68 65 6c 6c 6f |hello |
-------------------------------------------------------------------------
hello
---------------------------- all ----------------------------------------
position: [5], limit: [5]-------------------------------------------------| 0 1 2 3 4 5 6 7 8 9 a b c d e f |
-------------------------------------------------------------------------
|00000000| 68 65 6c 6c 6f |hello |
-------------------------------------------------------------------------Copy
方法三
编码字符串调用getByte()方法获得字节数组将字节数组传给ByteBuffer的wrap()方法通过该方法获得ByteBuffer。同样无需调用flip方法切换为读模式
解码通过StandardCharsets的decoder方法解码
public class Translate {public static void main(String[] args) {// 准备两个字符串String str1 hello;String str2 ;// 通过StandardCharsets的encode方法获得ByteBuffer// 此时获得的ByteBuffer为读模式无需通过flip切换模式ByteBuffer buffer1 ByteBuffer.wrap(str1.getBytes());ByteBufferUtil.debugAll(buffer1);// 将缓冲区中的数据转化为字符串// 通过StandardCharsets解码获得CharBuffer再通过toString获得字符串str2 StandardCharsets.UTF_8.decode(buffer1).toString();System.out.println(str2);ByteBufferUtil.debugAll(buffer1);}
}
运行结果
---------------------------- all ----------------------------------------
position: [0], limit: [5]-------------------------------------------------| 0 1 2 3 4 5 6 7 8 9 a b c d e f |
-------------------------------------------------------------------------
|00000000| 68 65 6c 6c 6f |hello |
-------------------------------------------------------------------------
hello
---------------------------- all ----------------------------------------
position: [5], limit: [5]-------------------------------------------------| 0 1 2 3 4 5 6 7 8 9 a b c d e f |
-------------------------------------------------------------------------
|00000000| 68 65 6c 6c 6f |hello |
-------------------------------------------------------------------------
粘包与半包
现象
网络上有多条数据发送给服务端数据之间使用 \n 进行分隔 但由于某种原因这些数据在接收时被进行了重新组合例如原始数据有3条为
Hello,world\nI’m Nyima\nHow are you?\n
变成了下面的两个 byteBuffer (粘包半包)
Hello,world\nI’m Nyima\nHow are you?\n
出现原因
粘包
发送方在发送数据时并不是一条一条地发送数据而是将数据整合在一起当数据达到一定的数量后再一起发送。这就会导致多条信息被放在一个缓冲区中被一起发送出去
半包
接收方的缓冲区的大小是有限的当接收方的缓冲区满了以后就需要将信息截断等缓冲区空了以后再继续放入数据。这就会发生一段完整的数据最后被截断的现象
解决办法
通过get(index)方法遍历ByteBuffer遇到分隔符时进行处理。注意get(index)不会改变position的值 记录该段数据长度以便于申请对应大小的缓冲区将缓冲区的数据通过get()方法写入到target中调用compact方法切换模式因为缓冲区中可能还有未读的数据
public class ByteBufferDemo {public static void main(String[] args) {ByteBuffer buffer ByteBuffer.allocate(32);// 模拟粘包半包buffer.put(Hello,world\nIm Nyima\nHo.getBytes());// 调用split函数处理split(buffer);buffer.put(w are you?\n.getBytes());split(buffer);}private static void split(ByteBuffer buffer) {// 切换为读模式buffer.flip();for(int i 0; i buffer.limit(); i) {// 遍历寻找分隔符// get(i)不会移动positionif (buffer.get(i) \n) {// 缓冲区长度int length i1-buffer.position();ByteBuffer target ByteBuffer.allocate(length);// 将前面的内容写入target缓冲区for(int j 0; j length; j) {// 将buffer中的数据写入target中target.put(buffer.get());}// 打印查看结果ByteBufferUtil.debugAll(target);}}// 切换为写模式但是缓冲区可能未读完这里需要使用compactbuffer.compact();}
}
运行结果
---------------------------- all ----------------------------------------
position: [12], limit: [12]-------------------------------------------------| 0 1 2 3 4 5 6 7 8 9 a b c d e f |
-------------------------------------------------------------------------
|00000000| 48 65 6c 6c 6f 2c 77 6f 72 6c 64 0a |Hello,world. |
-------------------------------------------------------------------------
---------------------------- all ----------------------------------------
position: [10], limit: [10]-------------------------------------------------| 0 1 2 3 4 5 6 7 8 9 a b c d e f |
-------------------------------------------------------------------------
|00000000| 49 27 6d 20 4e 79 69 6d 61 0a |Im Nyima. |
-------------------------------------------------------------------------
---------------------------- all ----------------------------------------
position: [13], limit: [13]-------------------------------------------------| 0 1 2 3 4 5 6 7 8 9 a b c d e f |
-------------------------------------------------------------------------
|00000000| 48 6f 77 20 61 72 65 20 79 6f 75 3f 0a |How are you?. |
-------------------------------------------------------------------------
二、文件编程
1、FileChannel
工作模式
FileChannel只能在阻塞模式下工作所以无法搭配Selector
获取
不能直接打开 FileChannel必须通过 FileInputStream、FileOutputStream 或者 RandomAccessFile 来获取 FileChannel它们都有 getChannel 方法
通过 FileInputStream 获取的 channel 只能读通过 FileOutputStream 获取的 channel 只能写通过 RandomAccessFile 是否能读写根据构造 RandomAccessFile 时的读写模式决定
读取
通过 FileInputStream 获取channel通过read方法将数据写入到ByteBuffer中
read方法的返回值表示读到了多少字节若读到了文件末尾则返回-1
int readBytes channel.read(buffer);Copy
可根据返回值判断是否读取完毕
while(channel.read(buffer) 0) {// 进行对应操作...
}
写入
因为channel也是有大小的所以 write 方法并不能保证一次将 buffer 中的内容全部写入 channel。必须需要按照以下规则进行写入
// 通过hasRemaining()方法查看缓冲区中是否还有数据未写入到通道中
while(buffer.hasRemaining()) {channel.write(buffer);
}
关闭
通道需要close一般情况通过try-with-resource进行关闭最好使用以下方法获取strea以及channel避免某些原因使得资源未被关闭
public class TestChannel {public static void main(String[] args) throws IOException {try (FileInputStream fis new FileInputStream(stu.txt);FileOutputStream fos new FileOutputStream(student.txt);FileChannel inputChannel fis.getChannel();FileChannel outputChannel fos.getChannel()) {// 执行对应操作...}}
}
位置
position
channel也拥有一个保存读取数据位置的属性即position
long pos channel.position();
可以通过position(int pos)设置channel中position的值
long newPos ...;
channel.position(newPos);
设置当前位置时如果设置为文件的末尾
这时读取会返回 -1这时写入会追加内容但要注意如果 position 超过了文件末尾再写入时在新内容和原末尾之间会有空洞00
强制写入
操作系统出于性能的考虑会将数据缓存不是立刻写入磁盘而是等到缓存满了以后将所有数据一次性的写入磁盘。可以调用 force(true) 方法将文件内容和元数据文件的权限等信息立刻写入磁盘