JAVA 中 NIO 再深入
在上一章节的 JAVA 中的 I/O 和 NIO 我们学习了如何使用 NIO, 接下来再深入了解一下关于 NIO 的知识.
缓冲器内部的细节
Buffer 由数据和可以高效地访问及操作这些数据的四个索引组成. 这四个索引是
mark: 标记, 就像游戏中设置了一个存档一样, 可以调用 reset()方法进行回归到 mark 标记的地方.
position: 位置, 其实缓冲器实际上就是一个美化过的数组, 从通道中读取数据就是放到了底层的数组. 所以其实就像索引一样. 所以 positon 变量跟踪已经写了多少数据.
limit: 界限, 即表明还有多少数据需要取出, 或者还有多少空间能够写入.
capacity: 容量, 表明缓冲器中可以存储的最大容量.
在缓冲器中每一个读写操作都会改变缓冲器的状态, 用于反应所发生的变化. 通过记录和跟踪这些变化, 缓冲器就能够内部地管理自己的资源. 下面是用于设置和复位索引以及查询其索引值的方法
方法名 | 解释 |
---|---|
capacity() | 返回缓冲器的容量 |
clear() | 清空缓冲器,将 position 设置为 0,limit 设置容量。调用此方法复写缓冲器 |
flip() | 将 limit 设置为 position,position 设置为 0. 此方法用于准备从缓冲区读取已经写入的数据 |
limit() | 返回 limit 值 |
limit(int lim) | 设置 limit 值 |
mark() | 将 mark 设置为 positon |
position() | 返回 position 的值 |
position(int pos) | 设置 postion 的值 |
remaining() | 返回 limit-position 的值 |
接下来我们写个例子模拟这四个索引的变化情况, 例如有一个字符串 BuXueWuShu. 我们交换相邻的字符.
- private static void symmetricScranble(CharBuffer buffer){
- while (buffer.hasRemaining()){
- buffer.mark();
- char c1 = buffer.get();
- char c2 = buffer.get();
- buffer.reset();
- buffer.put(c2).put(c1);
- }
- }
- public static void main(String[] args) {
- char [] data = "BuXueWuShu".toCharArray();
- ByteBuffer byteBuffer = ByteBuffer.allocate(data.length*2);
- CharBuffer charBuffer = byteBuffer.asCharBuffer();
- charBuffer.put(data);
- System.out.println(charBuffer.rewind());
- symmetricScranble(charBuffer);
- System.out.println(charBuffer.rewind());
- symmetricScranble(charBuffer);
- System.out.println(charBuffer.rewind());
- }
rewind()方法是将 position 设为 0 ,mark 设为 - 1
在刚进入 symmetricScranble ()方法时的各个索引如下图所示
1
然后第一次调用了 mark()方法以后就相当于给 mark 赋值了, 相当于在此设置了一个回档点. 此时索引如下所示
2
然后每次调用 get()方法 Position 索引都会改变, 在第一次调用了两次 get()方法以后, 各个索引如下
3
然后调用了 reset()方法另 Position=Mark, 此时的索引如下
4
然后每次调用 put()方法也会改变 Position 索引的值,
5
注意此时前两个字符已经互换了位置. 然后在第二轮 while 开始再次改变了 Mark 索引的值, 各个索引如下
6
此时我们应该就知道前面我们说的调用 clear()方法并不会清除缓冲器里面的数据的原因了, 因为只是将其索引变了而已.
内存映射文件
内存映射文件不是 Java 引入的概念, 而是操作系统提供的一种功能, 大部分操作系统都支持.
内存映射文件允许我们创建和修改那些因为太大而不能放入内存的文件. 有了内存映射文件, 我们就可以假定整个文件都放在内存中, 而且可以完全将其视为非常大的数组进行访问. 所以对于文件的操作就变为了对于内存中的字节数组的操作, 然后对于字节数组的操作会映射到文件中. 这种映射可以映射整个文件, 也可以只映射文件中的一部分. 什么时候字节数组中的的操作会映射到文件上呢? 这是由操作系统内部决定的.
内存放不下整个文件也不要紧, 操作系统会自动进行处理, 将需要的内容读到内存, 将修改的内容保存到硬盘, 将不再使用的内存释放.
如何用 NIO 将文件映射到内存中呢? 下面有个小例子表示将文件的前 1024 个字节映射到内存中.
- FileChannel fileChannel = new FileInputStream("").getChannel();
- MappedByteBuffer map = fileChannel.map(FileChannel.MapMode.READ_WRITE, 0, 1024);
创建一个内存映射文件只需要在通道中调用 map()方法即可, MapMode 有以下三个参数
READ_ONLY: 创建一个只读的映射文件
READ_WRITE: 创建一个既能读也能写的映射文件
PRIVATE: 创建一个写时拷贝 (copy-on-write) 的映射文件
我们可以简单的对比一下用内存映射文件对文件进行读写操作和用缓存 Buffer 对文件进行读写操作的速度比较.
- public static void main(String[] args) throws IOException {
- String fileName="/Users/hupengfei/Downloads/a.sql";
- long t1=System.currentTimeMillis();
- FileChannel fileChannel = new RandomAccessFile(fileName,"rw").getChannel();
- IntBuffer map = fileChannel.map(FileChannel.MapMode.READ_WRITE, 0, fileChannel.size()).asIntBuffer();
- map.put(0);
- for (int i = 1; i < 50590; i++) {
- map.put(map.get(i-1));
- }
- fileChannel.close();
- long t=System.currentTimeMillis()-t1;
- System.out.println("Mapped Read/Write:"+t);
- long t2=System.currentTimeMillis();
- RandomAccessFile randomAccessFile = new RandomAccessFile(new File(fileName),"rw");
- randomAccessFile.writeInt(1);
- for (int i = 0 ; i<50590;i++){
- randomAccessFile.seek(randomAccessFile.length()-4);
- randomAccessFile.writeInt(randomAccessFile.readInt());
- }
- randomAccessFile.close();
- long t22=System.currentTimeMillis()-t2;
- System.out.println("Stream Read/Write:"+t22);
- }
发现打印如下
- Mapped Read/Write:29
- Stream Read/Write:2439
文件越大, 那么这个差异会更明显.
文件加锁
在 JDK1.4 中引入了文件加锁的机制, 它允许我们同步的访问某个作为共享资源的文件. 对于同一文件竞争的两个线程可能是来自于不同的操作系统, 也可能是不同的进程, 也可能是相同的进程, 例如 Java 中两个线程对于文件的竞争. 文件锁对于其他的操作系统的进程是可见的, 因为文件加锁是直接映射到了本地操作系统的加锁工具.
下面举了一个简单的关于文件加锁的例子
- public static void main(String[] args) throws IOException, InterruptedException {
- FileOutputStream fileOutputStream = new FileOutputStream("/Users/hupengfei/Downloads/a.sql");
- FileLock fileLock = fileOutputStream.getChannel().tryLock();
- if (fileLock != null){
- System.out.println("Locked File");
- TimeUnit.MICROSECONDS.sleep(100);
- fileLock.release();
- System.out.println("Released Lock");
- }
- fileLock.close();
- }
通过对 FileChannel 调用 tryLock()或者 lock()方法, 就可以获得整个文件的 FileLock
tryLock(): 是非阻塞的, 如果不能获得锁, 那么他就会直接从方法调用中返回
lock(): 是阻塞的, 它要阻塞进程直到锁可以获得为止
调用 FileLock.release()可以释放锁.
当然也可以通过以下的方式对于文件的部分进行上锁
- tryLock(long position,long size,boolean shared)
- lock(long position,long size,boolean shared)
对于加锁的区域是通过 position 和 size 进行限定的, 而第三个参数指定是否为共享锁. 无参数的加锁方法会对整个文件进行加锁, 甚至文件变大以后也是如此. 其中锁的类型是独占锁还是共享锁可以通过 FileLock.isShared()进行查询.
参考文章
Java 编程思想
https://juejin.im/post/58626ac361ff4b006cf14faf
来源: http://www.jianshu.com/p/3132940f2a58