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中不一样,后一个程序等待前一个程序退出(释放锁)后才能得到锁。