【死磕NIO】— 跨进程文件锁:FileLock

本文详细介绍了Java中FileChannel的FileLock机制,演示了如何使用FileLock解决多进程并发访问同一文件的安全问题,并对比了使用与不使用锁的情况。

点击上方“服务端思维”,选择“设为星标”

回复”669“获取独家整理的精选资料集

回复”加群“加入全国服务端高端社群「后端圈」

c2d56158cbe366ae1fb24e82823e277e.png

作者 | chenssy

出品 | Java技术驿站

53c60c4b88f9dee3e25d393399e32037.png

了解了FileChannel是用来读写和映射一个系统文件的 Channel,其实他还有很牛逼的功能就是:跨进程文件锁。

说一个场景有多个进程同时操作某一个文件,并行往文件中写数据,请问如何保证写入文件的内容是正确的?可能有小伙伴说加分布式锁,可以解决问题,但是有点儿重了。

有没有更加轻量级的方案呢?多进程文件锁:FileLock

FileLock

FileLock是文件锁,它能保证同一时间只有一个进程(程序)能够修改它,或者都只可以读,这样就解决了多进程间的同步文件,保证了安全性。但是需要注意的是,它进程级别的,不是线程级别的,他可以解决多个进程并发访问同一个文件的问题,但是它不适用于控制同一个进程中多个线程对一个文件的访问。这也是为什么它叫做 多进程文件锁,而不是 多线程文件锁

FileLock一般都是从FileChannel 中获取,FileChannel 提供了三个方法用以获取 FileLock。

public abstract FileLock lock(long position, long size, boolean shared) throws IOException;

    public final FileLock lock() throws IOException;

    public abstract FileLock tryLock(long position, long size, boolean shared) throws IOException;
    
    public final FileLock tryLock() throws IOException;
  • lock() 是阻塞式的,它要阻塞进程直到锁可以获得,或调用lock()的线程中断,或调用lock()的通道关闭。

  • **tryLock()**是非阻塞式的,它设法获取锁,但如果不能获得,例如因为其他一些进程已经持有相同的锁,而且不共享时,它将直接从方法调用返回。

lock()tryLock()方法有三个参数,如下:

  • position:锁定文件中的开始位置

  • size:锁定文件中的内容长度

  • shared:是否使用共享锁。true为共享锁;false为独占锁。

共享锁和独占锁的区别,大明哥就不解释了。

示例

不使用文件锁来读写文件

首先我们不使用文件锁来进行多进程间文件读写,进程1往文件中写数据,进程2读取文件的大小。

  • 进程1

RandomAccessFile randomAccessFile = new RandomAccessFile("/Users/chenssy/Downloads/filelock.txt","rw");
        FileChannel fileChannel = randomAccessFile.getChannel();
        // 这里是独占锁
        //FileLock fileLock = fileChannel.lock();
        System.out.println("进程 1 开始写内容:" + LocalTime.now());
        for(int i = 1 ; i <= 10 ; i++) {
            randomAccessFile.writeChars("chenssy_" + i);
            // 等待两秒
            TimeUnit.SECONDS.sleep(2);
        }
        System.out.println("进程 1 完成写内容:" + LocalTime.now());
        // 完成后要释放掉锁
        //fileLock.release();
        fileChannel.close();
        randomAccessFile.close();
  • 进程2

RandomAccessFile randomAccessFile = new RandomAccessFile("/Users/chenssy/Downloads/filelock.txt","rw");
        FileChannel fileChannel = randomAccessFile.getChannel();
        // 这里是独占锁
        //FileLock fileLock = fileChannel.lock();
        System.out.println("开始读文件的时间:" + LocalTime.now());

        for(int i = 0 ; i < 10 ; i++) {
            // 这里直接读文件的大小
            System.out.println("文件大小为:" + randomAccessFile.length());
            // 这里等待 1 秒
            TimeUnit.SECONDS.sleep(1);
        }

        System.out.println("结束读文件的时间:" + LocalTime.now());
        // 完成后要释放掉锁
        //fileLock.release();
        fileChannel.close();
        randomAccessFile.close();

运行结果

  • 进程1

befc52cd89f762cd6f838704153252f0.png
  • 进程2

c6f6c284e2bf477fc5bca3c3d807f7e3.png

从这个结果可以非常清晰看到,进程1和进程2是同时执行的。进程1一边往文件中写,进程2是一边在读的

使用文件锁读写文件

这里我们使用文件锁来进行多进程间文件读写,依然使用上面的程序,只需要将对应的注释放开即可。执行结果

  • 进程1

5c1c6110617893080f87f72cef1d97af.png
  • 进程2

2d9865e69c60c6d4c06ffa0a80c306ee.png

从这里可以看到,进程2是等进程1释放掉锁后才开始执行的。同时由于进程1已经将数据全部写入文件了,所以进程2读取文件的大小是一样的。从这里可以看出 ** FileLock确实是可以解决多进程访问同一个文件的并发安全问题。**

同进程不同线程进行文件读写

在开始就说到,FileLock是不适用同一进程不同线程之间文件的访问。因为你根本无法在一个进程中不同线程同时对一个文件进行加锁操作,如果线程1对文件进行了加锁操作,这时线程2也来进行加锁操作的话,则会直接抛出异常:java.nio.channels.OverlappingFileLockException

f90ff6e9ec919d320d354aa5ea380420.png

当然我们可以通过另外一种方式来规避,如下:

FileLock fileLock;
            while (true){
                try{
                    fileLock = fileChannel.tryLock();
                    break;
                } catch (Exception e) {
                    System.out.println("其他线程已经获取该文件锁了,当前线程休眠 2 秒再获取");
                    TimeUnit.SECONDS.sleep(2);
                }
            }

将上面获取锁的部分用这段代码替换,执行结果又如下两种:

  • 线程1先获取文件锁

8ef2a50f5465b144758dcbf3dd0fdfff.png

  • 线程2先获取文件锁

d681a87003bc21f144014e7905990d13.png

这种方式虽然也可以实现多线程访问同一个文件,但是不建议这样操作!!!

源码分析

下面我们以 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.youkuaiyun.com/zwz1984/article/details/44809017

— 本文结束 —

5ea92c34485857ba82e8523f7f53dd2f.gif

● 漫谈设计模式在 Spring 框架中的良好实践

● 颠覆微服务认知:深入思考微服务的七个主流观点

● 人人都是 API 设计者

● 一文讲透微服务下如何保证事务的一致性

● 要黑盒测试微服务内部服务间调用,我该如何实现?

1ea4409cb21fc4014c37d74315b5c4d8.png

关注我,回复 「加群」 加入各种主题讨论群。

对「服务端思维」有期待,请在文末点个在看

喜欢这篇文章,欢迎转发、分享朋友圈

19fd1e936c0c78be9b432214b2e4ad4d.png

在看点这里

73d7198aac949bd46fbba3c8bef3b258.gif

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值