1. 概述
在读取或写入文件时,我们需要确保适当的文件锁定机制到位。这可确保基于并发I/O的应用程序中的数据完整性。
在本教程中,我们将介绍使用Java NIO库实现此目的的各种方法。
2. 文件锁简介
一般来说,有两种类型的锁:
- 独占锁:也称为写锁
- 共享锁:也称为读锁
简而言之,独占锁会在写操作完成时阻止所有其他操作(包括读取)。
相比之下,共享锁允许多个进程同时读取。读锁的作用是防止另一个进程获取写锁。通常,处于一致状态的文件确实应该可以被任何进程读取。
在下一节中,我们将了解Java如何处理这些类型的锁。
3. Java中的文件锁
Java NIO库支持在操作系统级别锁定文件。FileChannel的lock()和tryLock()方法就是为了这个目的。
我们可以通过FileInputStream、FileOutputStream或RandomAccessFile创建一个FileChannel,它们都有一个返回FileChannel的getChannel()方法。
或者,我们可以直接通过静态open方法创建一个FileChannel:
try (FileChannel channel = FileChannel.open(path, openOptions)) {
// write to the channel
}
接下来,我们将回顾在Java中获取独占锁和共享锁的不同选项。要了解有关文件通道的更多信息,请查看我们的Java FileChannel指南教程。
4. 独占锁
正如我们已经了解到的,在写入文件时,我们可以通过使用独占锁来防止其他进程对其进行读取或写入。
我们通过在FileChannel类上调用lock()或tryLock()来获得独占锁。我们也可以使用他们的重载方法:
- lock(long position, long size, boolean shared)
- tryLock(long position, long size, boolean shared)
在这些情况下,shared参数必须设置为false。
要获得独占锁,我们必须使用可写的FileChannel。我们可以通过FileOutputStream或RandomAccessFile的getChannel()方法创建它。或者,如前所述,我们可以使用FileChannel类的静态open方法。我们只需要将第二个参数设置为StandardOpenOption.APPEND:
try (FileChannel channel = FileChannel.open(path, StandardOpenOption.APPEND)) {
// write to channel
}
4.1 使用FileOutputStream的独占锁
从FileOutputStream创建的FileChannel是可写的。因此,我们可以获取排他锁:
try (FileOutputStream fileOutputStream = new FileOutputStream("/tmp/testfile.txt");
FileChannel channel = fileOutputStream.getChannel();
FileLock lock = channel.lock()) {
// write to the channel
}
在这里,channel.lock()要么阻塞直到获得锁,要么抛出异常。例如,如果指定的区域已被锁定,则会抛出OverlappingFileLockException。有关可能的异常的完整列表,请参阅Javadoc。
我们还可以使用channel.tryLock()执行非阻塞锁定。如果由于另一个程序持有重叠的锁而未能获得锁,则它返回null。如果由于任何其他原因未能这样做,则会抛出适当的异常。
4.2 使用RandomAccessFile的独占锁
对于RandomAccessFile,我们需要在构造函数的第二个参数上设置标志。
在这里,我们将以读写权限打开文件:
try (RandomAccessFile file = new RandomAccessFile("/tmp/testfile.txt", "rw");
FileChannel channel = file.getChannel();
FileLock lock = channel.lock()) {
// write to the channel
}
如果我们以只读模式打开文件并尝试写入其通道,它将抛出NonWritableChannelException。
4.3 独占锁需要可写的FileChannel
如前所述,独占锁需要一个可写通道。因此,我们无法通过从FileInputStream创建的FileChannel获得独占锁:
Path path = Files.createTempFile("foo","txt");
Logger log = LoggerFactory.getLogger(this.getClass());
try (FileInputStream fis = new FileInputStream(path.toFile());
FileLock lock = fis.getChannel().lock()) {
// unreachable code
} catch (NonWritableChannelException e) {
// handle exception
}
在上面的示例中,lock()方法将抛出NonWritableChannelException。实际上,这是因为我们在FileInputStream上调用getChannel,它创建的是一个只读通道。
这个例子只是为了演示我们不能写入一个不可写的通道。在真实场景中,我们不会捕获并重新抛出异常。
5. 共享锁
请记住,共享锁也称为读锁。因此,要获得读锁,我们必须使用可读的FileChannel。
这样的FileChannel可以通过调用FileInputStream或RandomAccessFile上的getChannel()方法来获得。同样,另一种选择是使用FileChannel类的静态open方法。在这种情况下,我们将第二个参数设置为StandardOpenOption.READ:
try (FileChannel channel = FileChannel.open(path, StandardOpenOption.READ);
FileLock lock = channel.lock(0, Long.MAX_VALUE, true)) {
// read from the channel
}
这里要注意的一件事是我们选择通过调用lock(0, Long.MAX_VALUE, true)来锁定整个文件。我们也可以通过将前两个参数更改为不同的值来仅锁定文件的特定区域。在共享锁的情况下,第三个参数必须设置为true。
为了简单起见,我们将在下面的所有示例中锁定整个文件,但请记住,我们始终可以锁定文件的特定区域。
5.1 使用FileInputStream的共享锁
从FileInputStream获得的FileChannel是可读的。因此,我们可以获得一个共享锁:
try (FileInputStream fileInputStream = new FileInputStream("/tmp/testfile.txt");
FileChannel channel = fileInputStream.getChannel();
FileLock lock = channel.lock(0, Long.MAX_VALUE, true)) {
// read from the channel
}
在上面的代码片段中,对通道上的lock()的调用将会成功,这是因为共享锁只要求通道是可读的。
5.2 使用RandomAccessFile的共享锁
这一次,我们可以只用读取权限打开文件:
try (RandomAccessFile file = new RandomAccessFile("/tmp/testfile.txt", "r");
FileChannel channel = file.getChannel();
FileLock lock = channel.lock(0, Long.MAX_VALUE, true)) {
// read from the channel
}
在此示例中,我们创建了一个具有读取权限的RandomAccessFile。我们可以从中创建一个可读通道,从而创建一个共享锁。
5.3 共享锁需要可读的FileChannel
因此,我们无法通过从FileOutputStream创建的FileChannel获取共享锁:
Path path = Files.createTempFile("foo","txt");
try (FileOutputStream fis = new FileOutputStream(path.toFile());
FileLock lock = fis.getChannel().lock(0, Long.MAX_VALUE, true)) {
// unreachable code
} catch (NonWritableChannelException e) {
// handle exception
}
在此示例中,对lock()的调用尝试在从FileOutputStream创建的通道上获取共享锁。这样的通道是只写的,它不能满足通道必须可读的需求。这将触发NonWritableChannelException。
同样,此代码段只是为了证明我们不能从不可读的通道中读取。
6. 需要考虑的事情
在实践中,使用文件锁很困难;锁定机制不可移植。我们需要在设计锁定逻辑时考虑到这一点。
在POSIX系统中,锁是建议性的。读取或写入给定文件的不同进程必须就锁定协议达成一致。这将确保文件的完整性。操作系统本身不会强制执行任何锁定。
在Windows上,除非允许共享,否则锁将是独占的。讨论操作系统特定机制的优缺点不在本文的讨论范围之内。但是,在实现锁定机制时了解这些细微差别很重要。
7. 总结
在本教程中,我们回顾了在Java中获取文件锁的几个不同选项。
首先,我们了解了两种主要的锁定机制以及Java NIO库如何促进锁定文件。然后我们通过一系列简单的例子展示了我们可以在应用程序中获得独占锁和共享锁。我们还了解了在使用文件锁时可能遇到的典型异常类型。
与往常一样,本教程的完整源代码可在GitHub上获得。