File Lock in Java

Table of Contents

1. JDK 文件锁简介

JDK 1.4 中引入了类 java.nio.channels.FileLock ,可以实现对文件进行加锁,允许我们同步访问某个共享的文件。java.nio.channels.FileLock 直接利用了操作系统提供的加锁设施,所以这个文件锁对操作系统中的其他进程(不要求是 Java 进程)是可见的(比如,在 Linux 系统中,用 java.nio.channels.FileLock 对文件加锁,另外一个 C 程序用 lockf/fcntl 尝试对同一个文件加锁时会等待 Java 进程释放文件锁)。

java.nio.channels.FileLock 支持“记录锁”,即可仅对文件的部分内容进行加锁。

参考:UNIX 环境高级编程(第 3 版),14.3 记录锁

1.1. Exclusive and shared locks

共享锁(读锁):获得共享锁后,其它进程还可以获得共享锁,但其它进程无法获得独占锁。对共享文件进行“读”操作时可以仅使用共享锁。
独占锁(写锁):获得独占锁后,其它进程无法获得独占锁或共享锁。对共享文件进行“写”操作时需要独占锁。

要对文件加锁,可以在文件的 FileChannel 上调用 lock()或者 tryLock()方法。 通过参数可以指定共享锁或独占锁,但如果操作系统不支持共享锁,则会自动转换为独占锁,用 isShared()可以测试 FileLock 是共享锁还是独占锁。

例如,下面程序对文件 test.txt 第 11 字节开始的 100 字节内容加共享锁。

import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.channels.FileChannel;
import java.nio.channels.FileLock;

public class Main {
  public static void main(String[] args) throws Exception {
    RandomAccessFile raf = new RandomAccessFile("test.txt", "rw");
    FileChannel fileChannel = raf.getChannel();
    FileLock lock = null;
    try {
      lock = fileChannel.lock(11, 100, true);   // 对文件test.txt第11字节开始的100字节内容加共享锁。
    } catch (IOException e) {
      // Handle the exception
    } finally {
      if (lock != null) {
        try {
          lock.release();
        } catch (IOException e) {
          // Handle the exception
        }
      }
    }
  }
}

2. 文件锁测试(多进程环境)

2.1. Java 进程和 C 进程对同一个文件加锁

前面介绍过 java.nio.channels.FileLock 直接利用了操作系统提供的加锁设施,所以这个文件锁对操作系统中的其他进程(不要求是 Java 进程)是可见的,下面通过程序简单地验证一下。

下面有两个程序(一个是在 C 语言中使用 lockf,另一个是在 Java 中使用 FileLock),功能类似,为对同一个文件/tmp/tmp.txt 加独占锁。

C 语言实现:

/* 用 lockf 对整个文件 /tmp/tmp.txt 加独占锁。 */
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/file.h>
int main(int argc, char *argv[])
{
  int fd, ret;
  int pid;
  fd = open("/tmp/tmp.txt", O_RDWR);     /* please create /tmp/tmp.txt manually in advance */
  ret = lockf(fd, F_LOCK, 0);
  printf("lockf return ret: %d\n", ret);

  printf("begin sleep\n");
  sleep(20);
  return 0;
}

Java 实现:

// file FileLockTest.java
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.channels.FileChannel;
import java.nio.channels.FileLock;

public class FileLockTest {
    public static void main(String[] args) {
        System.out.println("Test FileLock.");
        try {
            testLock();
        } catch (IOException | InterruptedException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }

    /* 对 /tmp/tmp.txt 整个文件加独占锁。 */
    static void testLock() throws IOException, InterruptedException {
        RandomAccessFile raf = new RandomAccessFile("/tmp/tmp.txt", "rw");
        FileChannel fileChannel = raf.getChannel();
        System.out.println("Begin fileChannel.lock()");
        FileLock lock = fileChannel.lock();                  /* 获取独占锁,如果无法获取则一直等待可以获取时。 */
        System.out.println("End fileChannel.lock()");

        System.out.println("begin sleep");
        Thread.sleep(10 * 1000);

        lock.release();   // 释放锁,最好写到finally里。
        raf.close();
    }
}

测试时,我们先运行 C 程序,再运行 Java 程序。会发现 Java 程序中 fileChannel.lock(); 会阻塞到 C 程序结束(程序结束会自动释放锁)才能拿到锁。

$ java FileLockTest &
[2] 1718
Test FileLock.
Begin fileChannel.lock()
End fileChannel.lock()        # 这一行当C程序结束时,才会打印出来。说明 fileChannel.lock(); 等C程序释放锁才能拿到锁。
begin sleep

在 Linux 中,可以通过文件 /proc/locks 来查看系统中有哪些文件锁。如:

$ cat /proc/locks
1: POSIX  ADVISORY  WRITE 1717 08:01:29563 0 EOF
1: -> POSIX  ADVISORY  WRITE 1718 08:01:29563 0 EOF
2: POSIX  ADVISORY  READ  1157 08:01:18368 128 128
3: POSIX  ADVISORY  READ  1157 08:01:8609 1073741826 1073742335
4: POSIX  ADVISORY  READ  1207 08:01:18368 128 128
5: POSIX  ADVISORY  READ  1207 08:01:8609 1073741826 1073742335
6: POSIX  ADVISORY  READ  1205 08:01:18368 128 128
7: POSIX  ADVISORY  READ  1205 08:01:8609 1073741826 1073742335
8: POSIX  ADVISORY  WRITE 1255 08:01:18380 0 0
9: POSIX  ADVISORY  READ  1245 08:01:18365 128 128
10: POSIX  ADVISORY  READ  1245 08:01:18363 1073741826 1073742335
11: POSIX  ADVISORY  READ  1168 08:01:18368 128 128
12: POSIX  ADVISORY  READ  1168 08:01:8609 1073741826 1073742335
13: POSIX  ADVISORY  READ  1225 08:01:18365 128 128
14: POSIX  ADVISORY  READ  1225 08:01:18363 1073741826 1073742335
15: POSIX  ADVISORY  READ  1232 08:01:18365 128 128
16: POSIX  ADVISORY  READ  1232 08:01:18363 1073741826 1073742335
17: POSIX  ADVISORY  WRITE 971 08:01:29161 0 EOF
18: POSIX  ADVISORY  WRITE 965 08:01:29003 0 EOF
19: POSIX  ADVISORY  WRITE 960 08:01:19085 0 EOF
20: POSIX  ADVISORY  WRITE 951 08:01:17869 0 EOF
21: POSIX  ADVISORY  WRITE 428 00:0f:11774 0 EOF
22: FLOCK  ADVISORY  WRITE 420 00:0f:11134 0 EOF
23: FLOCK  ADVISORY  WRITE 376 00:0f:10252 0 EOF

只需要关注 /proc/locks 前面两行内容。第一行是前面例子中 C 程序(pid 为 1717)文件锁相关信息;第二行是前面例子中 Java 程序(pid 为 1718)文件锁相关信息,其中符号“->”表示正在等待其他进程释放锁。

/proc/locks 文件的说明可参考:https://access.redhat.com/documentation/en-US/Red_Hat_Enterprise_Linux/5/html/Deployment_Guide/s2-proc-locks.html

2.2. “POSIX”锁和“FLOCK”锁

在 Linux 中,文件 /proc/locks 中,第二列为“POSIX”或者“FLOCK”,它表示这个锁的“类型”。用 lockf/fcntl 对文件加的锁为“POSIX”类型,用 flock 对文件加的锁为“FLOCK”类型。这两种类型的锁相互没有关系,彼此不知道对方的存在。也就是说如果某个进程用 lockf/fcntl 对文件加了一个独占锁(“POSIX”类型),那么另外一个进程用 flock 对同一个文件也可以获得独占锁(“FLOCK”类型)。

Linux 中,JDK 的 java.nio.channels.FileLock 所加的锁为“POSIX”锁。如果我们需要在 C 程序和 Java 程序中同时读写某个文件时加锁,则 C 程序中必须使用“POSIX”锁(即使用 lockf/fcntl 对文件加锁)。

注意:在 Mac OS X 和 FreeBSD 中,不区分“POSIX”和“FLOCK”锁,一个进程使用 lockf/fcntl/flock 中任何一个方法对文件加独占锁,另一个进程使用 lockf/fcntl/flock 中任何一个方法对同一文件加独占锁时会等待另一个进程释放锁。

参考:http://stackoverflow.com/questions/22409780/flock-vs-lockf-on-linux

2.2.1. 实例:Linux 中“POSIX”锁和“FLOCK”锁不会互相影响

下面两个程序,分别用 lockf 和 flock 对文件 /tmp/tmp.txt 进行加锁,在 Linux 中,它们不会互相影响(彼此不知道对方存在,能同时成功获得独占锁)。

// file testlockf.c
// 在Linux中,lockf/fcntl 加的锁为“POSIX”锁
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/file.h>
int main(int argc, char *argv[])
{
  int fd, ret;
  int pid;
  fd = open("/tmp/tmp.txt", O_RDWR);           // please create /tmp/tmp.txt manually in advance
  ret = lockf(fd, F_LOCK, 0);
  printf("lockf return ret: %d\n", ret);

  printf("begin sleep\n");
  sleep(20);
  return 0;
}
// file testflock.c
// 在Linux中,flock 加的锁为“FLOCK”锁
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/file.h>
int main(int argc, char *argv[])
{
  int fd, ret;
  int pid;
  fd = open("/tmp/tmp.txt", O_RDWR);           // please create /tmp/tmp.txt manually in advance
  ret = flock(fd, LOCK_EX);
  printf("flock return ret : %d\n", ret);

  printf("begin sleep\n");
  sleep(20);
  return 0;
}

在 Linix 中,同时执行上面两个程序,彼此不会等待,都能获得独占锁。

$ ./testlockf &
[1] 2172
lockf return ret: 0
begin sleep

$ ./testflock &
[2] 2174
flock return ret : 0
begin sleep

查看 /proc/locks ,会发现锁的类型是不一样的(一个为“POSIX”锁,另一个为“FLOCK”锁)。

$ cat /proc/locks
1: FLOCK  ADVISORY  WRITE 2174 08:01:29563 0 EOF
2: POSIX  ADVISORY  WRITE 2172 08:01:29563 0 EOF
3: POSIX  ADVISORY  READ  1157 08:01:18368 128 128
4: POSIX  ADVISORY  READ  1157 08:01:8609 1073741826 1073742335
5: POSIX  ADVISORY  READ  1207 08:01:18368 128 128
6: POSIX  ADVISORY  READ  1207 08:01:8609 1073741826 1073742335
7: POSIX  ADVISORY  READ  1205 08:01:18368 128 128
8: POSIX  ADVISORY  READ  1205 08:01:8609 1073741826 1073742335
9: POSIX  ADVISORY  WRITE 1255 08:01:18380 0 0
10: POSIX  ADVISORY  READ  1245 08:01:18365 128 128
11: POSIX  ADVISORY  READ  1245 08:01:18363 1073741826 1073742335
12: POSIX  ADVISORY  READ  1168 08:01:18368 128 128
13: POSIX  ADVISORY  READ  1168 08:01:8609 1073741826 1073742335
14: POSIX  ADVISORY  READ  1225 08:01:18365 128 128
15: POSIX  ADVISORY  READ  1225 08:01:18363 1073741826 1073742335
16: POSIX  ADVISORY  READ  1232 08:01:18365 128 128
17: POSIX  ADVISORY  READ  1232 08:01:18363 1073741826 1073742335
18: POSIX  ADVISORY  WRITE 971 08:01:29161 0 EOF
19: POSIX  ADVISORY  WRITE 965 08:01:29003 0 EOF
20: POSIX  ADVISORY  WRITE 960 08:01:19085 0 EOF
21: POSIX  ADVISORY  WRITE 951 08:01:17869 0 EOF
22: POSIX  ADVISORY  WRITE 428 00:0f:11774 0 EOF
23: FLOCK  ADVISORY  WRITE 420 00:0f:11134 0 EOF
24: FLOCK  ADVISORY  WRITE 376 00:0f:10252 0 EOF

前面介绍过,在 Mac OS X 和 FreeBSD 中,不区分“POSIX”和“FLOCK”锁。所以在Mac OS X或FreeBSD中同时运行前面测试的两个程序,行为和Linux中不一样,后一个程序等待前一个程序退出(释放锁)后才能得到锁。

Author: cig01

Created: <2016-06-13 Mon>

Last updated: <2017-12-25 Mon>

Creator: Emacs 27.1 (Org mode 9.4)