Java I/O 系统
《Java 编程思想》第四版,第18章笔记。
对程序语言的设计者来说,创建一个好的输入/输出(I/O)系统是一项艰难的任务。
现有的大量不同方案已经说明了这一点。挑战似乎来自于要涵盖所有的可能性。不仅存在各种 I/O 源端的想要与之通信的接收端(文件、控制台、网络链接等),而且还需要以多种不同的方式与它们进行通信(顺序、随机存取、缓冲、二进制、按字符、按行、按字等)。
Java 类库的设计者通过创建大量的类来解决这个难题。一开始,可能会对 Java I/O 系统提供了如此多的类而感到不知所措(具有讽刺意味的是,Java I/O 设计的初衷是为了避免过多的类)。自从 Java 1.0 版本依赖,Java的 I/O 类库发生了明显改变,在原来的面向字节的类中添加了面向字符和基于 Unicode 的类。在 JDK 1.4 中,添加了 nio 类(对于 “ 新 I/O ” 来说,这是一个从现在起我们将要使用若干年的名称,即使它们在 JDK 1.4 中就已经被引入了,因此它们已经 “ 旧 ” 了)添加进来时为了改进性能及功能。因此,在充分理解 Java I/O 系统以便正确地运用之前,我们需要学习相当数量的类。另外,很有必要理解 I/O 类库的演化过程,即使我们的第一反应是 “ 不要用历史打扰我,只需告诉我怎么用 ”。问题是,如果缺乏历史的眼光,很快我们就会对什么时候该使用哪些类,以及什么时候不该使用它们而感到迷惑。
接下来就介绍 Java 标准类库中各种各样的类以及它们的用法。
1. File 类
File(文件)类这个名字有一定的误导性;我们可能会认为它指代的是文件,实际上却并非如此。它既能代表一个特定文件的名称,又能代表一个目录下的一组文件的名称。如果它指的是一个文件集,我们就可以对此集合调用 list() 方法,这个方法会返回一个字符数组。我们很容易就可以理解返回的是一个数组而不是某个更具灵活性的类容器,因为元素的个数是固定的,所以如果我们想取得不同得目录列表,只需要再创建一个不同的 File 对象就可以了。实际上,FilePath (文件路径)对这个类来说是一个更好的名字。
File file= new File("D:\\test\\mmm");
2. 输入和输出
编程语言的 I/O 类库中常使用的流这个抽象概念,它代表任何有能力产生数据的数据源对象或者是有能力接收数据的接收端对象。“ 流 ” 屏蔽了实际的 I/O 设备中处理数据的细节。
Java 类库中的 I/O 类分成输入和输出两部分,可以再 JDK 文档里的类层次结构中查看到。通过基础,任何自 InputStream 或 Reader 派生而来的类都含有名为 read() 的基础方法,用于读取单个字节或者字节数组。同样,任何自 OutputStream 或者 Writer 派生而来的类都含有名为 write() 的基本方法,用于写单个字节或者字节数组。但是,我们通常不会用到这些方法,它们之所以存在是因为别的类可以使用它们,以便提供更有用的接口。因此,我们很少使用单一的类来创建流对象,而是通过叠合多个对象来提供所期望的功能(这是装饰器设计模式)。实际上,java 中 “ 流 ” 类库让人迷惑的主要原因就在于:创建单一的结果流,却需要创建多个对象。
有必要按照这些类的功能对它们进行分类。在 Java 1.0 中,类库的设计者首先限定和输入有关的所有类都应该从 InputStream 继承,而与输出有关的所有类都应该从 OutputStream 继承。
2.1 InputStream 类型
InputStream 的作用是用来表示那些从不同数据源产生输入的类。如下表。
类 | 功能 | 构造器参数 如何使用 |
---|---|---|
ByteArrayInputStream | 允许将内存的缓冲区当作 InputStream 使用 | 缓冲区,字节将从中取出 作为一种数据源:将其与 FilterInputStream 对象相连以提供有用接口 |
StringBufferInputStream | 将 String 转换成 InputStream | 字符串。底层实现实际使用 StringBuffer 作为一种数据源:将其与 FilterInputStream 对象相连以提供有用接口 |
FileInputStream | 用于从文件中读取信息 | 字符串,表示文件名、文件或者 FileDescriptor 对象 作为一种数据源:将其与 FilterInputStream 对象相连以提供有用接口 |
PipedInputStream | 产生用于写入相关 PipedOutputStream 的数据,实现 “ 管道化 ” 概念 | PipedOutputStream 作为多线程中的数据源:将其与 FilterInputStream 对象相连以提供有用接口 |
SequenceInputStream | 将两个或多个 InputStream 对象转换成单一 InputStream | 两个 InputStream 对象或一个容纳 InputStream 对象的容器 Enumeration 作为一种数据源:将其与 FilterInputStream 对象相连以提供有用接口 |
FilterInputStream | 抽象类,作为 “ 装饰器 ” 的接口。其中,“ 装饰器 ” 为其他的 InputStream 类提供有用功能 |
这些数据源包括:
- 字节数组。
- String 对象。
- 文件。
- “ 管道 ”,工作方式与实际管道相似,即,从一段输入,从另一端输出。
- 一个有其他类型的流组成的序列,以便我们可以将它们收集合并到一个流内。
- 其他数据源,如 Internet 连接等。
2.2 OutputStream 类型
类 | 功能 | 构造器参数 如何使用 |
---|---|---|
ByteArrayOutputStream | 在内存中创建缓冲区。所有送往 “ 流 ” 的数据都要放置在此缓冲区 | 缓冲区初始化尺寸(可选的) 用于指定数据的目的地:将其与 FilterOutputStream 对象相连以提供有用接口 |
FileOutputStream | 用于将信息写至文件 | 字符串,表示文件名、文件或 FileDescriptor 对象 用于指定数据的目的地:将其与 FilterOutputStream 对象相连以提供有用接口 |
PipedOutputStream | 任何写入其中的信息都会自动作为相关 PipedInputStream 的输出。实现 “ 管道化 ” 概念 | PipedInputStream 指定用于多线程的数据的目的地:将其与 FilterOutputStream 对象相连以提供有用接口 |
FilterOutputStream | 抽象类,作为 “ 装饰器 ” 的接口。其中, “ 装饰器 ” 为其他 OutputStream 提供有用功能 |
该类别的类决定了输出所要去往的目标:字节数组(但不是 String,不过你当然可以用字节数组自己创建)、文件或管道。
3. 添加属性和有用的接口
装饰器的引入。Java I/O 类库需要多种不同功能的组合,这正是装饰器模式的理由所在。这也是 Java I/O 类库里存在 filter(过滤器)类的原因所在抽象类 filter 是所有装饰器类的基类。装饰器必须具有和它所装饰的对象相同的接口,但它也可以扩展接口,而这种情况只发生在个别 filter 类中。
但是,装饰器模式也有一个缺点:在编写程序时,它给我们提供了相当多的灵活性,但是它同时也增加了代码的复杂性。Java I/O 类库操作不便的原因就在于:我们必须创建许多类—— “ 核心 ” I/O 类型加上所有的装饰器,才能得到我们所希望的单个 I/O 对象。
FilterInputStream 和 FilterOutputStream 是用来提供装饰器类接口以控制特定输入流(InputStream)和输出流(OutputStream)的两个类,它们的名字并不是很直观。FilterInputStream 和 FilterOutputStream 分别自 I/O 类库中的基类 InputStream 和 OutputStream 派生而来,这两个类是装饰器的必要条件(以便能为所有正在被修饰的对象提供通用接口)。
FilterInputStream 类型:
类 | 功能 | 构造器参数 如何使用 |
---|---|---|
DataInputStream | 与 DataOutputStream 搭配使用,因此我们可以按照可移植方式从流读取基本数据类型(int,char,long 等) | InputStream 包含用于读取基本类型数据的全部接口 |
BufferedInputStream | 使用它可以防止每次读取时都得进行实际写操作。代表 “ 使用缓冲区 ” | InputStream,可以指定缓存区大小(可选的) 本质上不提供接口,只不过是向进程中添加缓冲区所必需得。与接口对象搭配 |
LineNumberInputStream | 根据输入流中的行号;可调用 getLineNumber() 和 setLineNumber() | InputStream 仅增加了行号,因此可能要与接口对象搭配使用 |
PushbackInputStream | 具有 “ 能弹出一个字节的缓冲区 ” 。因此可以将读到的最后一个字符回退 | InputStream 通常作为编译器的扫描器,之所以包含在内是因为 Java 编译器的需要,我们可能永远不会用到 |
FilterOutputStream 类型:
类 | 功能 | 构造器参数 如何使用 |
---|---|---|
DataOutputStream | 与 DataInputStream 搭配使用,因此可以按照可移植方式向流中写入基本类型数据(int,char,long等) | OutputStream 包含用于写入基本类型数据的全部接口 |
PrintStream | 用于产生格式化输出。其中 DataOutputStream 处理数据的存储,PrintStream 处理显示 | OutputStream 可以用 boolean 值指示是否在每次换行时清空缓冲区(可选的)应该是对 OutputStream 对象的 “ final ” 封装。 可能会经常使用 |
BufferedOutputStream | 使用它以避免每次发送数据时都进行实际的写操作。代表 “ 使用缓冲区 ”。可以调用 flush() 清空缓冲区 | OutputStream,可以指定缓冲区大小(可选的) 本质上并不提供接口,只不过是向进程中添加缓冲区所必需的。与接口对象搭配 |
4. Reader 和 Writer
Java 1.1 对基本的 I/O 流类库进行了重大的修改。当我们初次看到 Reader 和 Writer 类时,可能会以为这时两个用来替代 InputStream 和 OutputStream 的类;但实际上并非如此。尽管一些原始的 “ 流 ” 类库不再被使用,但是 InputStream 和 OutputStream 在以面向字节形式的 I/O 中仍额可以提供极有价值的功能,Reader 和 Writer 则提供兼容 Unicode 与面向字符的 I/O 功能。
另外:
- Java 1.1 向 InputStream 和 OutputStream 继承层次结构中添加了一些新类,所以显然这两个类是不会被取代的。
- 有时我们必须把来自 “ 字节 ” 层次结构中的类和 “ 字符 ” 层次结构中的类结合起来使用。为了实现这个目的,要用到 “ 适配器 ”(adapter)类:InputStreamReader 可以把 InputStream 转换为 Reader,而 OutputStream 可以把 OutputStream 转换为 Writer。
设计 Reader 和 Writer 继承层次结构主要是为了国际化。老的 I/O 流继承层次结构仅支持 8 位字节流,并且不能很好地处理 16 位的 Unicode 字符。由于 Unicode 用于字符国际化,所以添加 Reader 和 Writer 继承层次结构就是为了在所有的 I/O 操作中都支持 Unicode 。另外,新类库的的设计使得它的操作比旧类库更快。
4.1 数据的来源和去处
几乎所有原始的 Java I/O 流类都有响应的 Reader 和 Writer 类来提供天然的 Unicode 操作。然而在某些场合,面向字节的 InputStream 和 OutputStream 才是正确的解决方案;特别是,java.util.zip 类库就是面向字节的而不是面向字符的。因此,最明智的做法是尽量尝试使用 Reader 和 Writer,一旦程序代码无法成功编译,我们就会发现自己不得不使用面向字节的类库。
下面的表展示了在两个继承层次结构中,信息的来源和取出之间的对应关系:
来源和去处:Java 1.0 类 | 相应的 Java 1.1 类 |
---|---|
InputStream | Reader 适配器:InputStreamReader |
OutputStream | Writer 适配器:OutputStreamWriter |
FileInputStream | FileReader |
FileOutputStream | FileWriter |
StringBufferInputStream(已弃用) | StringReader |
无 | StringWriter |
ByteArrayInputStream | CharArrayReader |
ByteArrayOutputStream | CharArrayWriter |
PipedInputStream | PipedReader |
PipedOutputStream | PipedWriter |
4.2 更改流的行为
对于 InputStream 和 OutputStream 来说,我们会使用 FilterInputStream 和 FilterOutputStream 的装饰器子类来修改 “ 流 ” 已满足特殊需要。Reader 和 Writer 的类继承层次结构沿用相同的思想——但是并不完全相同。
在下表中,相对于前一个表格来说,左右之间的对应关系的近似程度更加粗略一些。造成这种差别的原因是因为类的组织形式不同;尽管 BufferedOutputStream 是 FilterOutputStream 的子类,但是 BufferedWriter 并不是 FilterWriter 的子类。然而,这些类的接口却十分相似。
过滤器:Java 1.0 类 | 相应的 Java 1.1 类 |
---|---|
FilterInputStream | FilterReader |
FilterOutputStream | FilterWriter(抽象类,没有子类) |
BufferedInputStream | BufferedRreader(也有 readLine()) |
BufferdOutputStream | BufferedWriter |
DataInputStream | 使用 DataInputStream(除了当需要使用 readLine() 时以外,这时应该使用 BufferedReader) |
PrintStream | PrintWriter |
LineNumberInputStream | LineNumberReader |
StreamTokenizer | StreamTokenizer(使用接受 Reader 的构造器) |
PushbackInputStream | PushbackReader |
4.3 未发生变化的类
以下这些 Java 1.0 类在 Java 1.1 中没有相应的类 |
---|
DataOutputStream |
File |
RandomAccessFile |
SequenceInputStream |
5. 自我独立的类: RandomAccessFile
RandomAccessFile 适用于由大小已知的记录组成的文件,所以我们可以适用 seek() 将记录从一处转移到另一处,然后读取或者修改记录。文件中记录的大小不一定都相同,只要我们能够确定那些记录有多大以及它们的文件中的位置即可。
最初,我们可能难以相信 RandomAccessFile 不是 InputStream 或者 OutputStream 继承层次结构中的一部分。除了实现了 DataInput 和 DataOutput 接口( DataInputStream 和 DataOutputStream 也实现了这两个接口)之外,它和这两个继承层次没有任何关联。它们甚至不适用 InputStream 和 OutputStream 类中已有的任何功能。它是一个完全独立的类,从头开始编写其所有的方法(大多数是本地的)。这么做是因为 RandomAccessFile 拥有和别的 I/O 类型本质不同的行为,因为我们可以在一个文件内向前和向后移动。在任何情况下,它都是自我独立的,直接从 Object 派生而来。
从本质上来说,RandomAccessFile 的工作方式类似于把 DataInputStream 和 DataOutputStream 组合起来使用,还添加了一些方法。其中方法 getFilePointer() 用于查找当前所处的文件位置,seek() 用于在文件内移至新的位置,length() 用于判断文件的最大尺寸。另外,其构造器还需要第二个参数(和 C 中的 fogen() 相同)用来指示我们 “ 随机读 ” (r) 还是 “ 既读又写 ” (rw)。它并不支持只写文件,这表明 RandomAccessFile 若是从 DataInputStream 继承而来也可能会运行得很好。
只有 RandomAccessFile 支持搜寻方法,并且只适用于文件。BufferedInputStream 却能允许标注(mark() )位置和重新设定位置 (reset()),但这些功能很有限,不是非常有用。
在 JDK 1.4 中,RandomAccessFile 的大部分功能(但不是全部)由 nio 存储映射文件所取代。稍后会做介绍。
6. NIO (新 I/O)
JDK 1.4 的 java.nio.*
包中引入了新的 Java I/O 类库,其目的在于提高速度。实际上,旧的 I/O 包已经使用 nio 重新实现过,以便充分利用这种速度提高,因此,即使我们不显式地用 nio 编写代码,也能从中受益。速度的提高在文件 I/O 和网络 I/O 中都有可能发生,我们在这里只研究前者。
描述:
速度的提升来自于所使用的结构更接近于操作系统执行 I/O 的方式:管道和缓冲器。
我们可以把它想象成一个煤矿,通道是一个包含煤层(数据)的矿藏,而缓冲器则是派送矿藏的卡车。卡车载满煤炭而归,我们再从卡车上获得煤炭。也就是说,我们并没有直接和管道交互;我们只是和缓冲器交互,并把缓冲器派送给通道。通道要么从缓冲器获得数据,要么向缓冲器发送数据。
唯一直接交互缓冲器 ByteBuffer:
唯一直接和通道交互的缓冲器是 ByteBuffer
——也就是说,可以存储未加工字节的缓冲器。当我们查询 JDK 文档中的 java.nio.ByteBuffer 时,会发现它是相当继承的类;通过告知分配多少存储空间来创建一个 ByteBuffer 对象,并且还有一个方法选择集,用于以原始的字节形式或基本数据类型输出和读取数据。但是,没办法输出或读取对象,即使是字符串对象也不行。这种处理虽然很低级,但却正好,因为这是大多数操作系统中更有效的映射方式。
获取:
旧 I/O 类库中有三个类被修改了,用以产生 FileChannel。这三个被修改的类是 FileInputStream 、FileOutputStream 以及用于既读又写的 RandomAccessFile 。注意这些是字节操作流,与底层的 nio 性质一致。Reader 和 Writer 这种字符串模式类不能用于产生通道;但是 java.nio.channels.Channels 类提供了使用方法,用以在通道中产生 Reader 和 Writer。
大小:
对于只读访问,我们必须显式地使用静态的 allocate() 方法来分配 ByteBuffer。nio 的目标就是快速移动大量数据,因此 ByteBuffer 的大小就显得尤为重要,必须通过实际运行应用程序来找到最佳尺寸。
甚至达到更高的速度也是有可能,方法就是使用 allocateDirect() 而不是 allocate() ,以产生一个与操作系统有更高耦合性的 “ 直接 ” 缓冲器。但是,这种分配的开支会更大,并且具体实现也随操作系统的不同而不同,因此必须再次实际运行应用程序来查看直接缓冲是否可以使用我们获得速度上的优势。
使用:
一旦调用 read() 来告知 FileChannel 向 ByteBuffer 存储字节,就必须调用缓冲器上的 flip(),让它做好让别人读取字节的准备(使得,这似乎有一点拙劣,但是请记住,它是很拙劣的,但却适用于获得最大的速度)。如果我们打算使用缓冲器执行进一步的 read() 操作,我们也必须得调用 clear() 来位每个 read() 做好准备。
FileChannel in = new FileInputStream("in.path").getChannel();
FileChannel out = new FileOutputStream("out.path").getChannel();
ByteBuffer buffer = ByteBuffer.allocate(BSZIE);
while(in.read(buffer) != -1){
buffer.flip(); // prepare for writing
out.write(buffer);
buffer.clear(); // prepare for reading
}
转换数据:
缓冲器容纳的是普通的字节,为了把它们转换成字符,我们要么在输入它们的时候对其进行编码,要么在将其从缓冲器输出时对它们进行解码。可以使用 java.nio.charset.Charset 类实现这些功能,该类提供了数据编码成多种不同类型的字符集的工具。
Charset.forName(encoding).decode(buff);
StandardCharsets.UTF_8.decode(buff);
视图缓冲器:
利用 asCharBuffer()、asShortBuffer() 等方法获取该缓冲器上的视图。
视图缓冲器(view buffer)可以让我们通过某个特定的基本数据类型的视图查看其底层的 ByteBuffer。ByteBuffer 依然时实际存储数据的地方,“ 支持 ” 着前面的视图,因此,对视图的任何修改都会映射成为对 ByteBuffer 中数据的修改。
ByteBuffer 通过一个被 “ 包装 ” 过的 8 字节数组产生,然后通过各种不同的基本类型的视图缓冲器显示了出来。我们可以在下图中看到,当从不同类型的缓冲器读取时,数据显示的方式也不同。
6.1 缓冲器的细节
Buffer 由数据和可以高效访问及操纵这些数据的四个索引组成,这四个索引是:
- mark(标记)
- position(位置)
- limit(界限)
- capacity(容量)
下面是用于设置和复位索引以及查询它们的值得方法。
方法 | 说明 |
---|---|
capacity() | 返回缓冲区容量 |
clear() | 清空缓冲区,将 position 设置为 0,limit 设置为容量。我们可以调用此方法覆写缓冲区 |
flip() | 将 limit 设置为 position,position 设置为 0。此方法用于准备从缓冲区读取已经写入的数据 |
limit() | 返回 limit 的值 |
limit(int lim) | 设置 limit 的值 |
mark() | 将 mark 设置为 position |
position() | 返回 position 值 |
position(int pos) | 设置 position 值 |
remaining() | 返回 (limit - position) |
hasRemaining() | 若有介于 position 和 limit 之间的元素,则返回 ture |
在缓冲器中插入和提取数据的方法会更新这些索引,用于反映所发生的变化。
下面的示例用到一个很简单的算法(交换相邻字符),以对 CharBuffer 中的字符进行编码(scramble)和译码(unscramble)。
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
public class UsingBuffers {
private static void symmetricScramble(CharBuffer charBuffer ){
while (charBuffer.hasRemaining()){
charBuffer.mark();
char c1 = charBuffer.get();
char c2 = charBuffer.get();
charBuffer.reset();
charBuffer.put(c2).put(c1);
}
}
public static void main(String[] args) {
char[] data = "UsingBuffers".toCharArray();
ByteBuffer bb = ByteBuffer.allocate(data.length*2);
CharBuffer cb = bb.asCharBuffer();
cb.put(data);
System.out.println(cb.rewind());
symmetricScramble(cb);
System.out.println(cb.rewind());
symmetricScramble(cb);
System.out.println(cb.rewind());
}
}
输出:
UsingBuffers
sUniBgfuefsr
UsingBuffers
下面是进入 symmetricScramble() 方法时缓冲器的样子:
position 指针指向缓冲器中的第一个元素,capacity 和 limit 则指向最后一个元素。
在程序的 symmetricScramble() 方法中,迭代执行 while 循环直到 position 等于 limit。一旦调用缓冲器上相对的 get() 和 put() 函数, position 指针就会随之相应改变。我们也可以调用绝对的、包含一个索引参数的 get() 和 put() 方法。不过这些方法不会改变缓冲器的 position 指针。
当操纵到 while 循环时,使用 mark() 调用来设置 mark 的值。此时,缓冲器状态如下:
两个相对的 get() 调用把前两个字符保存到变量 c1 和 c2 中,调用完这两个方法后,缓冲器如下:
为了实现交换,我们要在 position = 0 时写入 c2, position = 1 时写入 c1。我们也可以使用绝对的 put() 方法来实现,或者使用 reset() 把 position 的值设为 mark 的值:
这两个 put() 方法先写 c2,接着写 c1:
在下一个循环迭代期间,将 mark 设置为 position 的当前值:
这个过程将会持续到遍历完整个缓冲器。在 while 循环的最后,position 指向缓冲器的末尾。如果要打印缓冲器,只能打印 position 和 limit 之间的字符。因此,如果想显示缓冲器的全部内容,必须使用 rewind() 把 position 设置到缓冲器的开始位置。下面是调用 rewind() 之后缓冲器的状态(mark的值则变得不明确):
当再次调用 symmetricScramble() 功能时,会对 CharBuffer 进行同样的处理,并将其恢复到初始状态。
6.2 内存映射文件
内存映射文件允许我们创建和修改那些因为太大而不能放入内存的文件。
有了内存映射文件,我们就可以假定整个文件都放在内存中,而且可以完全把它当作非常达的数组来访问。这种方法极大地简化了用于修改文件的代码。
public class LargeMappedFiles {
static int length = 0x8FFFFFF;
static FileChannel fc;
public static void main(String[] args) throws Exception {
fc = new RandomAccessFile("D:\\test\\largeFile","rw").getChannel();
MappedByteBuffer out =fc.map(FileChannel.MapMode.READ_WRITE,0,length);
for (int i = 0; i < length; i++) {
out.put((byte)'a');
}
for (int i = length / 2; i < length / 2 + 6; i++) {
System.out.println((char) out.get(i));
}
}
}
为了既能写又能读,我们先由 RandomAccessFile 开始,获得该文件上的通道,然后调用 map() 产生 MappedByteBuffer,这是一种特殊类型的直接缓冲器。注意我们必须指定映射文件的初始位置和映射区域的长度,这意味着我们可以映射某个大文件的较小的部分。
MappedByteBuffer 由 ByteBuffer 继承而来,因此它具有 ByteBuffer 的所有方法。这里,我们仅仅展示了非常简单的 put() 和 get(),但是我们同样可以使用像 asCharBuffer() 等这样的用法。
前面那个程序创建的文件为 128MB,这可能比操作系统所允许一次载入内存的空间大。但似乎我可以一次访问到整个文件,因为只有一部分文件放入了内存,文件的其他部分被交换了出去。用这种方式,很大的文件(可达2GB)也可以很容易的修改。注意底层操作系统的文件映射工具是用来最大化地提高性能。
尽管 “ 旧 ” 的 I/O 流在用 nio 实现后性能有所提高,但是 “ 映射文件访问 ” 往往可以更加显著地加快速度。
其它资料:
深入浅出MappedByteBuffer
6.3 文件加锁
JDK 1.4 引入了文件加锁机制,它允许我们同步访问某个作为共享资源的文件。不过,竞争同一文件的两个线程可能在不同的 java 虚拟机上;或者一个是 java 线程,另一个是操作系统中其他的某个本地线程。文件锁对其他的操作系统进程是可见的,因为 Java 的文件加锁直接映射到了本地操作系统的加锁工具。
FileChannel fc = new RandomAccessFile("D:\\test\\largeFile","rw").getChannel();
FileLock fl = fc.tryLock();
fl.release();
tryLock() 是非阻塞式的。
lock() 是阻塞式的。
public abstract FileLock tryLock(long position, long size, boolean shared)
throws IOException;
public abstract FileLock lock(long position, long size, boolean shared)
throws IOException;
加锁区域由 size ,position 决定的。第三个参数指定是否是共享锁。
尽管无参数的加锁方法将根据文件尺寸的变化而变化,但是具有固定尺寸的锁不随文件尺寸的变化而变化。如果你获得了某一个区域上的锁,当文件增大超过区域时,超过部分不会锁定。无参数的加锁方法会对整个文件进行加锁,甚至文件变大后也是如此。
对独占锁或者共享锁的支持必须由底层的操作系统提供。可以通过 FileLock.isShared() 进行查询。
7. 其他资料
Java中的零拷贝
IBM-深入分析 Java I/O 的工作机制
美团-Java NIO浅析
IBM-NIO 入门
github-CyC2018-IO
java的IO和NIO