从这里可以看到,进程2是等进程1释放掉锁后才开始执行的。同时由于进程1已经将数据全部写入文件了,所以进程2读取文件的大小是一样的。从这里可以看出 ** FileLock确实是可以解决多进程访问同一个文件的并发安全问题。**
同进程不同线程进行文件读写
在开始就说到,FileLock是不适用同一进程不同线程之间文件的访问。因为你根本无法在一个进程中不同线程同时对一个文件进行加锁操作,如果线程1对文件进行了加锁操作,这时线程2也来进行加锁操作的话,则会直接抛出异常:java.nio.channels.OverlappingFileLockException。
当然我们可以通过另外一种方式来规避,如下:
FileLock fileLock; while (true){ try{ fileLock = fileChannel.tryLock(); break; } catch (Exception e) { System.out.println("其他线程已经获取该文件锁了,当前线程休眠 2 秒再获取"); TimeUnit.SECONDS.sleep(2); } }将上面获取锁的部分用这段代码替换,执行结果又如下两种:
线程1先获取文件锁
线程2先获取文件锁
这种方式虽然也可以实现多线程访问同一个文件,但是不建议这样操作!!!
源码分析下面我们以 FileLock lock(long position, long size, boolean shared)为例简单分析下文件锁的源码。lock()方法是由FileChannel的子类 FileChannelImpl来实现的。
public FileLock lock(long position, long size, boolean shared) throws IOException { // 确认文件已经打开 , 即判断open标识位 ensureOpen(); if (shared && !readable) throw new NonReadableChannelException(); if (!shared && !writable) throw new NonWritableChannelException(); // 创建 FileLock 对象 FileLockImpl fli = new FileLockImpl(this, position, size, shared); // 创建 FileLockTable 对象 FileLockTable flt = fileLockTable(); flt.add(fli); boolean completed = false; int ti = -1; try { // 标记开始IO操作 , 可能会导致阻塞 begin(); ti = threads.add(); if (!isOpen()) return null; int n; do { // 开始锁住文件 n = nd.lock(fd, true, position, size, shared); } while ((n == FileDispatcher.INTERRUPTED) && isOpen()); if (isOpen()) { // 如果返回结果为RET_EX_LOCK的话 if (n == FileDispatcher.RET_EX_LOCK) { assert shared; FileLockImpl fli2 = new FileLockImpl(this, position, size, false); flt.replace(fli, fli2); fli = fli2; } completed = true; } } finally { // 释放锁 if (!completed) flt.remove(fli); threads.remove(ti); try { end(completed); } catch (ClosedByInterruptException e) { throw new FileLockInterruptionException(); } } return fli; }首先会判断文件是否已打开,然后创建FileLock和FileLockTable 对象,其中FileLockTable是用于存放 FileLock的table。
调用 begin()设置中断触发
protected final void begin() { if (interruptor == null) { interruptor = new Interruptible() { public void interrupt(Thread target) { synchronized (closeLock) { if (!open) return; open = false; interrupted = target; try { AbstractInterruptibleChannel.this.implCloseChannel(); } catch (IOException x) { } } }}; } blockedOn(interruptor); Thread me = Thread.currentThread(); if (me.isInterrupted()) interruptor.interrupt(me); }调用 FileDispatcher.lock()开始锁住文件
int lock(FileDescriptor fd, boolean blocking, long pos, long size, boolean shared) throws IOException { BlockGuard.getThreadPolicy().onWriteToDisk(); return lock0(fd, blocking, pos, size, shared); }lock0()的实现是在 FileDispatcherImpl.c 中,源码如下:
JNIEXPORT jint JNICALL FileDispatcherImpl_lock0(JNIEnv *env, jobject this, jobject fdo, jboolean block, jlong pos, jlong size, jboolean shared) { // 通过fdval函数找到fd jint fd = fdval(env, fdo); jint lockResult = 0; int cmd = 0; // 创建flock对象 struct flock64 fl; fl.l_whence = SEEK_SET; // 从position位置开始 if (size == (jlong)java_lang_Long_MAX_VALUE) { fl.l_len = (off64_t)0; } else { fl.l_len = (off64_t)size; } fl.l_start = (off64_t)pos; // 如果是共享锁 , 则只读 if (shared == JNI_TRUE) { fl.l_type = F_RDLCK; } else { // 否则可读写 fl.l_type = F_WRLCK; } // 设置锁参数 // F_SETLK : 给当前文件上锁(非阻塞)。 // F_SETLKW : 给当前文件上锁(阻塞,若当前文件正在被锁住,该函数一直阻塞)。 if (block == JNI_TRUE) { cmd = F_SETLKW64; } else { cmd = F_SETLK64; } // 调用fcntl锁住文件 lockResult = fcntl(fd, cmd, &fl); if (lockResult < 0) { if ((cmd == F_SETLK64) && (errno == EAGAIN || errno == EACCES)) // 如果出现错误 , 返回错误码 return sun_nio_ch_FileDispatcherImpl_NO_LOCK; if (errno == EINTR) return sun_nio_ch_FileDispatcherImpl_INTERRUPTED; JNU_ThrowIOExceptionWithLastError(env, "Lock failed"); } return 0; }所以,其实文件锁的核心就是调用Linux的fnctl来从内核对文件进行加锁。
关于Linux 文件锁,大明哥推荐这两篇博客,小伙伴可以了解下:
linux文件锁flock:https://www.cnblogs.com/kex1n/p/7100107.html
Linux文件锁定:https://blog.csdn.net/zwz1984/article/details/44809017