死磕nio—跨进程文件锁:filelock(代码片段)

chenssy chenssy     2023-02-16     551

关键词:

大家好,我是大明哥,一个专注于【死磕 Java】系列创作的程序员。
死磕 Java 】系列为作者「chenssy」 倾情打造的 Java 系列文章,深入分析 Java 相关技术核心原理及源码
死磕 Java :https://www.cmsblogs.com/group/1420041599311810560


上篇文章(【死磕 NIO】— 深入分析Channel和FileChannel)已经详细介绍了 FileChannel的核心原理及相关API,了解了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

  • 进程2

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

使用文件锁读写文件

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

  • 进程1

  • 进程2

从这里可以看到,进程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 文件锁,大明哥推荐这两篇博客,小伙伴可以了解下:

nio--filelock,path,files,asynchronousfilechannel,charset(代码片段)

NIOFileLock文件锁分类使用示例获取文件锁方法lock与tryLock的区别:FileLock两个方法:examplePath创建Path实例创建绝对路径创建相对路径Path.normalize()FilesFiles.createDirectory()Files.copy()Files.move()Files.delete()Files.walkFileTree()AsynchronousFileC... 查看详情

Java NIO FileLock 允许其他进程写入锁定的文件

】JavaNIOFileLock允许其他进程写入锁定的文件【英文标题】:JavaNIOFileLockallowsotherprocesstowritetoalockedfile【发布时间】:2019-10-2019:59:24【问题描述】:我正在使用以下代码在一个Java应用程序中获取对文件的锁定:...Filefile=newFile("/some/... 查看详情

java使用filelock实现java进程互斥锁

原理:JDK的nio包中FileLock实现类似Linuxfcntl的文件锁,可使文件被进程互斥访问. 借助此功能,可以实现强大的Java进程互斥锁,从而在应用层面保证同一时间只有惟一的Jar应用进程在运行!避免某些因素导致jar重复执行,多个进程产生... 查看详情

nio相关基础篇二

...,后续还会继续有一到二篇左右与NIO内容相关。文件锁(FileLock)在看RocketMQ源码中,发现有关于文件锁的import,但是具体使用代码里面注释调了[回头看看为什么,理解下,到时候会在某篇 查看详情

java新io_文件锁代码与字符集

importjava.io.File;importjava.io.FileOutputStream;importjava.nio.channels.FileChannel;importjava.nio.channels.FileLock;publicclassFileLockDemo{   publicstaticvoidmain(Stringargs[])throw 查看详情

python演示跨进程共享文件描述符(代码片段)

查看详情

如何在android中实现跨进程锁?

】如何在android中实现跨进程锁?【英文标题】:Howtoimplementcrossprocesslockinandroid?【发布时间】:2014-05-2715:31:58【问题描述】:我正在编写一个库项目供多个APP使用。而且由于某种原因,我必须为不同的APP做一个功能互斥,所以我... 查看详情

android文件存取可以跨进程吗

参考技术A好像不可以 参考技术BActivity的跨进程访问与进程内访问略有不同。虽然它们都需要Intent对象,但跨进程访问并不需要指定Context对象和Activity的Class对象,而需要指定的是要访问的Activity所对应的Action(一个字符串)。有... 查看详情

python跨进程通信可以使用哪些消息中间件?

文章大纲简介数据库作为消息中间件是否能满足性能要求?参考文献与学习路径简介从通信目的的角度来看,我们可以把进程之间的通信分成3种:进程调度:可以通过信号来实现;共享资源:可以通过互斥锁、信号量、读写锁、文件锁... 查看详情

python跨进程通信可以使用哪些消息中间件?

文章大纲简介数据库作为消息中间件是否能满足性能要求?参考文献与学习路径简介从通信目的的角度来看,我们可以把进程之间的通信分成3种:进程调度:可以通过信号来实现;共享资源:可以通过互斥锁、信号量、读写锁、文件锁... 查看详情

python跨进程通信可以使用哪些消息中间件?

文章大纲简介数据库作为消息中间件是否能满足性能要求?参考文献与学习路径简介从通信目的的角度来看,我们可以把进程之间的通信分成3种:进程调度:可以通过信号来实现;共享资源:可以通过互斥锁、信号量、读写锁、文件锁... 查看详情

死磕nio—nio基础详解(代码片段)

文章首发:https://www.cmsblogs.com/article/1435620402348036096Netty是基于JavaNIO封装的网络通讯框架,只有充分理解了JavaNIO才能理解好Netty的底层设计。JavaNIO由三个核心组件组件:BufferChannelSelector缓冲区BufferBuffer是一个数据对象&... 查看详情

死磕nio—nio基础详解(代码片段)

Netty是基于JavaNIO封装的网络通讯框架,只有充分理解了JavaNIO才能理解好Netty的底层设计。JavaNIO由三个核心组件组件:BufferChannelSelector缓冲区BufferBuffer是一个数据对象,我们可以把它理解为固定数量的数据的容器,... 查看详情

死磕nio—nio基础详解(代码片段)

Netty是基于JavaNIO封装的网络通讯框架,只有充分理解了JavaNIO才能理解好Netty的底层设计。JavaNIO由三个核心组件组件:BufferChannelSelector缓冲区BufferBuffer是一个数据对象,我们可以把它理解为固定数量的数据的容器,... 查看详情

死磕nio—深入分析buffer(代码片段)

...几篇文章的链接,有兴趣的同学可以阅读下:【死磕NIO】—阻塞、非阻塞、同步、异步,傻傻分不清楚【死磕NIO】—阻塞IO&#x 查看详情

死磕nio—serversocketchannel的应用实例(代码片段)

大家好,我是大明哥,一个专注于【死磕Java】的程序员。【死磕Java】系列为作者「chenssy」倾情打造的Java系列文章,深入分析Java相关技术核心原理及源码。死磕Java:https://www.cmsblogs.com/group/1420041599311810560上篇文... 查看详情

死磕nio—serversocketchannel的应用实例(代码片段)

大家好,我是大明哥,一个专注于【死磕Java】的程序员。【死磕Java】系列为作者「chenssy」倾情打造的Java系列文章,深入分析Java相关技术核心原理及源码。死磕Java:https://www.cmsblogs.com/group/1420041599311810560上篇文... 查看详情

死磕nio—探索socketchannel的核心原理(代码片段)

大家好,我是大明哥,一个专注于【死磕Java】系列创作的程序员。【死磕Java】系列为作者「chenssy」倾情打造的Java系列文章,深入分析Java相关技术核心原理及源码。死磕Java:https://www.cmsblogs.com/group/142004159931181056... 查看详情