java中的普通write和buffer read的重要差距?
每次写数据data:“123456789\n”
通过查看追踪系统调用线程文件,发现普通write,每一次write,实质都是一次系统调用,也就是都会发生一次内核态切换:
通过查看追踪系统调用线程文件,发现buffered的write,每一次write,写的是一个缓冲区的若干个“123456789\n”:
NIO
在jdk新版,除了传统IO,又出现了新的IO,也就是NIO,也暴露了新的API,ByteBuffer
public void whatByteBuffer(){
// ByteBuffer buffer = ByteBuffer.allocate(1024);
ByteBuffer buffer = ByteBuffer.allocateDirect(1024);
System.out.println("postition: " + buffer.position());
System.out.println("limit: " + buffer.limit());
System.out.println("capacity: " + buffer.capacity());
System.out.println("mark: " + buffer);
buffer.put("123".getBytes());
System.out.println("-------------put:123......");
System.out.println("mark: " + buffer);
buffer.flip(); //读写交替
System.out.println("-------------flip......");
System.out.println("mark: " + buffer);
buffer.get();
System.out.println("-------------get......");
System.out.println("mark: " + buffer);
buffer.compact();
System.out.println("-------------compact......");
System.out.println("mark: " + buffer);
buffer.clear();
System.out.println("-------------clear......");
System.out.println("mark: " + buffer);
}
打印:
postition: 0
limit: 1024
capacity: 1024
mark: java.nio.DirectByteBuffer[pos=0 lim=1024 cap=1024]
-------------put:123......
mark: java.nio.DirectByteBuffer[pos=3 lim=1024 cap=1024]
-------------flip......
mark: java.nio.DirectByteBuffer[pos=0 lim=3 cap=1024]
-------------get......
mark: java.nio.DirectByteBuffer[pos=1 lim=3 cap=1024]
-------------compact......
mark: java.nio.DirectByteBuffer[pos=2 lim=1024 cap=1024]
-------------clear......
mark: java.nio.DirectByteBuffer[pos=0 lim=1024 cap=1024]
//ByteBuffer buffer = ByteBuffer.allocate(1024);
ByteBuffer buffer = ByteBuffer.allocateDirect(1024);
ByteBuffer,堆上分配或者堆外分配。
System.out.println("postition: " + buffer.position());
System.out.println("limit: " + buffer.limit());
System.out.println("capacity: " + buffer.capacity());
System.out.println("mark: " + buffer);
postition: 0
limit: 1024
capacity: 1024
mark: java.nio.DirectByteBuffer[pos=0 lim=1024 cap=1024]
ByteBuffer有三大关键属性,pos代表偏移量,limit代表大小限制,cap代表文件总大小
put操作:
buffer.put("123".getBytes());
System.out.println("-------------put:123......");
System.out.println("mark: " + buffer);
-------------put:123......
mark: java.nio.DirectByteBuffer[pos=3 lim=1024 cap=1024]
put写入123后,pos偏移量前移3个
flip操作:
buffer.flip(); //读写交替
System.out.println("-------------flip......");
System.out.println("mark: " + buffer);
-------------flip......
mark: java.nio.DirectByteBuffer[pos=0 lim=3 cap=1024]
flip操作,将偏移量置为0,限制指针置为3,这是为了从写模式改为读模式,防止读的超过了可用的总大小,因此将limit限制为当前可读的最大值。
limit变成pos,pos归0
get操作:
buffer.get();
System.out.println("-------------get......");
System.out.println("mark: " + buffer);
-------------get......
mark: java.nio.DirectByteBuffer[pos=1 lim=3 cap=1024]
读取1个字节操作,会移动pos偏移量1
compact操作:
buffer.compact();
System.out.println("-------------compact......");
System.out.println("mark: " + buffer);
-------------compact......
mark: java.nio.DirectByteBuffer[pos=2 lim=1024 cap=1024]
取走开头的pos个字节:
compact后续字节进行前移:
将已经读取的偏移量所属文件内容积压出去,此时,limit指向文件最大。 pos左侧为未读空间,右侧为未写空间
clear操作:
buffer.clear();
System.out.println("-------------clear......");
System.out.println("mark: " + buffer);
-------------clear......
mark: java.nio.DirectByteBuffer[pos=0 lim=1024 cap=1024]
一切重置成最开始的状态
对比文件的几种读取方式
public static void testRandomAccessFileWrite() throws Exception {
RandomAccessFile raf = new RandomAccessFile(path, "rw");
//普通的write操作
raf.write("hello darknessa\n".getBytes());
raf.write("hello seanzhou\n".getBytes());
System.out.println("write------------");
System.in.read();
//基于指定偏移量的write操作
raf.seek(4);
raf.write("ooxx".getBytes());
System.out.println("seek---------");
System.in.read();
//接下来开始NIO的操作:
//获取文件的channel
FileChannel rafchannel = raf.getChannel();
//来得到一个通过mmap与内核pagecache直接映射的文件对象,也就是mmap
//只有文件的channel能进行mmap,因为文件是块设备,所以才能使得pagecache和文件地址映射起来
MappedByteBuffer map = rafchannel.map(FileChannel.MapMode.READ_WRITE, 0, 4096);
//与上面的写入操作不太,这再是系统调用,但是数据会到达内核的pagecache
map.put("@@@".getBytes());
//曾经我们是需要out.write() 这样的系统调用,才能让程序的data 进入内核的pagecache
//系统调用必须有用户态内核态切换的过程
//但mmap的内存映射,依然受内核的pagecache体系所约束的!!!
//换言之,还是会丢数据
//你可以去github上找一些 其他C程序员写的jni扩展库,使用linux内核的Direct IO
//直接IO是忽略linux的pagecache
//。。。但这样会有一系列复杂问题:
//是把pagecache交给了程序自己去开发,开辟一个字节数组当作pagecache,动用代码逻辑来维护一致性/dirty
//而除了更细粒度的控制之外,自己开发的与pagecache无太大区别,像mysql这种一般的使用Direct IO
System.out.println("map--put--------");
System.in.read();
// 相当于传统IO模型的flush操作
// map.force();
raf.seek(0);
ByteBuffer buffer = ByteBuffer.allocate(8192);
// ByteBuffer buffer = ByteBuffer.allocateDirect(1024);
//将刚通过mmap写入的文件内容再写入到ByteBuffer中
int read = rafchannel.read(buffer); //buffer.put()
System.out.println(buffer);
buffer.flip();
System.out.println(buffer);
for (int i = 0; i < buffer.limit(); i++) {
Thread.sleep(200);
System.out.print(((char) buffer.get(i)));
}
}
可以看到txt类型的,就是我们的java代码段。
有2个文件描述符,第一个是基于mmap的生成的文件描述符,第二个是传统IO的需要通过write,read操作的文件描述符。
小结文件的IO模型
在linux中,每个进程都有一份完整独立的text区,data区,堆区,栈区,mmap映射区。
而java其实本质就是一个普通的linux进程,所以自然也有上述东西。
堆内
在linux级别的堆上分配了一块固定大小的专属于JVM使用的堆,这个就叫做堆内空间。 堆内如果想要写IO到磁盘,需要先复制一份到堆外内存。
堆外
在JAVA进程中的堆内存里,除了被JVM管辖固定大小的那块JVM堆外,剩下的堆内存,都叫堆外内存。
而上述两者不管如何,都还是基于用户态程序内存空间中的,想要实现IO调用,依然需要走系统调用,也就是内核态切换!
MMAP内存映射
在一个linux进程中,与堆区这些同等维度存在的,还有一个mmap映射区,通过它调用的IO操作,可以直接一一对应映射到内核中的PageCache中(虽然堆内堆外本质也是会最终缓存在pagecache,但映射关系不是一一对应的),这个过程不存在系统调用,而pagecache再通过与磁盘文件本身的映射关系,进行写入,这个时候的IO操作就是像是直接通过用户态来完成了以往的内核态敏感操作。 本质是内核和用户态的线性地址和逻辑地址映射到同一物理地址
最终,说到底,还是受内核pagecache的约束,也就不难得出结论:没有完全可靠的IO操作,因为pagecache本身的刷写机制本身就存在丢失的风险性。