Java文件锁用于读写

23
我有一个进程,该进程将经常从 cron 调用以读取一个包含某些移动相关命令的文件。我的进程需要读取和写入此数据文件-并在此期间保持其锁定,以防止其他进程触摸它。能够由用户执行完全分离的进程来(潜在地)写入/追加到同一数据文件中。我希望这两个进程友好相处,并且每次只访问文件一次。
听起来nio FileLock就是我所需要的(除了编写自己的信号量类型文件),但我在尝试为读取创建锁定时遇到了问题。我可以很好地锁定并写入,但是尝试创建读取锁定时却收到NonWritableChannelException。是否可能为读取锁定文件?似乎RandomAccessFile更接近我需要的,但我不知道如何实现它。
以下是失败的代码:
FileInputStream fin = new FileInputStream(f);
FileLock fl = fin.getChannel().tryLock();
if(fl != null) 
{
  System.out.println("Locked File");
  BufferedReader in = new BufferedReader(new InputStreamReader(fin));
  System.out.println(in.readLine());
          ...

在 FileLock 行触发了异常。

java.nio.channels.NonWritableChannelException
 at sun.nio.ch.FileChannelImpl.tryLock(Unknown Source)
 at java.nio.channels.FileChannel.tryLock(Unknown Source)
 at Mover.run(Mover.java:74)
 at java.lang.Thread.run(Unknown Source)

查看JavaDocs,它说:

当尝试向最初未为写入打开的通道写入数据时抛出未经检查的异常。

但我不一定需要写入它。 当我尝试创建一个用于编写目的的FileOutputStream等文件时,它很开心,直到我尝试在同一文件上打开FileInputStream。


你尝试过使用三个参数的方法调用FileLock lock(long position, long size, boolean shared)吗?我以前从未使用过FileLock,所以我不会把它作为答案发布,但我认为使用该方法调用可能会有帮助,因为它听起来像你需要一个共享锁而不是独占锁,因为你想在文件上加锁时写入它。 - ChadNC
我认为这个意图是只锁定文件的部分,但是我想要锁定整个文件以防止任何可能的损坏。 - bobtheowl2
3个回答

21
  1. 您是否知道,除非其他进程也使用锁定,否则锁定文件不会阻止其他进程触及它?
  2. 您必须通过可写通道进行锁定。通过以“rw”模式获取RandomAccessFile的锁定,然后打开FileInputStream。记得关闭两个文件!

3
a) 是的,我将编写两个进程,并计划在两者中实现类似的锁定流程。 b) 我没有意识到您可以直接在RandomAccessFile上获取锁。要使用File [Input | Output] Stream,我需要执行new FileInputStream(raf.getFD())。但是,无论如何,直接使用RandomAccessFile对象的输入流,我仍然可以从文件中读取。谢谢 - bobtheowl2
嗯?(a) 你不能直接从RandomAccessFile获取锁,你必须先获取它的FileChannel;(b) RandomAccessFile没有输入流,但它实现了DataInput,所以你可以直接从中读取。 - user207421
我注意到在Linux(Oracle Linux Server,类似于Red Hat Linux)中,Java文件锁定不会跨越操作系统实例工作。也就是说,它可以在一个操作系统内进行锁定,但不能从另一个盒子上进行锁定。 - Don Smith
@DonSmith 这在 Javadoc 中已经说明了。网络文件锁定始终存在问题,应该避免使用,就像网络文件一样。 - user207421

13

使用tryLock(0L, Long.MAX_VALUE, true)创建锁会更好。

这将创建一个共享锁,对于读取来说是正确的操作。

tryLock()tryLock(0L, Long.MAX_VALUE, false) 的简写形式,也就是请求一个独占写锁。


非常好的回复。这个程序已经上线了,但在下一阶段肯定可以更新。我们现在看到它被广泛使用,在某些情况下独占锁变得很麻烦。我一定会记住这个建议,为下一个更新做准备。 - bobtheowl2
为什么你说共享锁是读取的正确方式,这显然取决于你的用例?如果我想确保只有一个进程读取文件(例如代理监视上传目录以处理新文件),那么我认为共享锁不是我需要的。 - codebox
2
我这样写是因为读取并不是互斥的。实际上,多个线程可以从一个文件中读取而不会相互干扰,而写入则总是互斥的。请记住,这是对原问题的回答,并不意味着适用于每种情况的绝对真理。尽管如此,在99%的使用情况下是正确的。 - Huxi

0

我编写了一个测试程序和bash命令来确认文件锁的有效性:

import java.io.File;
import java.io.RandomAccessFile;
import java.nio.channels.FileLock;

public class FileWriterTest
{
    public static void main(String[] args) throws Exception
    {
        if (args.length != 4)
        {
            System.out.println("Usage: FileWriterTest <filename> <string> <sleep ms> <enable lock>");
            System.exit(1);
        }

        String filename = args[0];
        String data = args[1];
        int sleep = Integer.parseInt(args[2]);
        boolean enableLock = Boolean.parseBoolean(args[3]);

        try (RandomAccessFile raFile = new RandomAccessFile(new File(filename), "rw"))
        {
            FileLock lock = null;
            if (enableLock)
            {
                lock = raFile.getChannel().lock();
            }

            Thread.sleep(sleep);
            raFile.seek(raFile.length());
            System.out.println("writing " + data + " in a new line; current pointer = " + raFile.getFilePointer());
            raFile.write((data+"\n").getBytes());

            if (lock != null)
            {
                lock.release();
            }
        }
    }
}

使用此 Bash 命令运行以检查其是否正常工作:

for i in {1..1000}
do
java FileWriterTest test.txt $i 10 true &
done

你应该看到写入操作每10毫秒发生一次(从输出中),最终所有数字都应该出现在文件中。

输出:

/tmp wc -l test.txt
1000 test.txt
/tmp

没有锁的相同测试显示数据丢失:

for i in {1..1000}
do
java FileWriterTest test.txt $i 10 false &
done

输出:

/tmp wc -l test.txt
764 test.txt
/tmp

将其修改为测试tryLock应该很容易。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接