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

2023-10-29

大家好,我是大明哥,一个专注于【死磕 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 文件锁,大明哥推荐这两篇博客,小伙伴可以了解下:

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

【死磕NIO】— 跨进程文件锁:FileLock 的相关文章

  • 车辆总线-MVB通讯

    概述 MVB Multifunction vehicle bus 为多功能车辆总线 它是列车通信网 TCN Train Communication Network 的一部分 TCN 网络由 WTB Wire Train Bus MVB 构成
  • Q-learning 理解以及简单实现

    强化学习 reinforcement learning 的过程 强化学习中有状态 state 动作 action 奖赏 reward 这三个要素 智能体需要根据当前状态来采取动作 获得相应的奖赏之后 再去改进这些动作 使得下次再到相同状态时

随机推荐

  • MoviePy介绍

    MoivePy是一个用于视频编辑的Python库 可以 剪切 拼接 标题插入 视频合成 视频处理和创建自定义效果 它支持Windows Linux Mac 源码地址 https github com Zulko moviepy 最新发布版本
  • 前端如何调用后端接口进行数据交互(极简)

    前端调用后端接口 获得数据并渲染 一 介绍 一个完善的系统 前后端交互是必不可少的 这个过程可以分成下面几步 前端向后端发起请求 后端接口接收前端的参数后 开始层层调用方法处理数据 后端将最终数据返回给前端接口 前端请求成功后 将数据渲染至
  • ubuntu安装tomcat7

    1 上传tomcat7源码包 百度云盘链接 https pan baidu com s 1Yo4DVOcm667F iKhGwpPBw 密码 ce1h 2 先安装 JDK cd opt tar zxvf jdk 8u161 linux x6
  • 解决GitHub密码授权访问即将失效的问题

    解决GitHub密码授权访问即将失效的问题 前言 1 网络中的解决方法 2 GitHub密码授权弃用通知 3 创建个人访问令牌 4 IDEA设置Token访问GitHub 4 1 忘记密码 4 2 Token授权 5 GitHub 客户端
  • 非Unicode程序创建非本地字符集的文件路径

    开发中需要用到纯MFC的程序完成整包的文件释放 过程大概是根据整包中的文件路径记录在当前执行路径下建立子目录 创建文件 然后将整包中的内存数据写入该文件 正常英文或中文系统下路径创建及文件读写都没有遇到问题 但在路径名称或文件名称出现非本地
  • 前端常见的适配方法

    作为一个真正的前端攻城狮 我们经常会谈到web前端怎么做适配 然而平常经常做的事 一旦问起来突然好像脑子一片空白 只能想到rem flex 媒体查询那些 因此想记录一篇文章以此警醒自己 一 固定布局 pc端 静态布局 以像素作为页面的基本单
  • 谁在为网络安全制造标尺?

    我们想帮助企业往后退一步 去全局的看一下自己的安全能力建设水平如何 以及在当下的阶段最应该做的安全建设是什么 度量 对应的是更清晰的认知 而对企业安全而言 这种认知 也更在成为一把新的标尺 作者 皮爷 出品 产业家 6月开始 吕一平开始频繁
  • MySQL存储引擎:MyISAM和InnoDB区别详解

    1 MyISAM和InnoDB区别 1 1 区别 InnoDB MyISAM 构造 由 frm文件 表空间 分为独立表空间或者共享表空间 和日志文件 redo log 组成 MyISAM在磁盘上存储成三个文件 其中 frm文件存储表定义 M
  • 百度坐标(BD-09)、国测局坐标(火星坐标,GCJ-02)、和 WGS-84 坐标系之间的转换

    百度坐标 BD 09 国测局坐标 火星坐标 GCJ 02 和 WGS 84 坐标系之间的转换 Created by Wandergis on 2015 7 8 提供了百度坐标 BD 09 国测局坐标 火星坐标 GCJ 02 和 WGS 84
  • CentOS 7安装Zabbix 4.4

    我们当前部署Zabbix是在Centos7的基础上部署Zabbix4 4版本 我的服务器配置是双路四核CPU 8GBRAM 以下安装步骤仅供参考 大家也可以参考官方资料 https www zabbix com documentation
  • 142 环形链表

    142 环形链表 给定一个链表的头节点 head 返回链表开始入环的第一个节点 如果链表无环 则返回 null 重点在于如何判断是否有环 采用快慢指针的做法 快指针每次走2步 慢指针每次走1步 快指针相对慢指针每次多走一步 这样确保如果有环
  • Map Reduce和流处理

    欢迎大家前往腾讯云 社区 获取更多腾讯海量技术实践干货哦 本文由 从流域到海域翻译 发表于腾讯云 社区 map 和reduce 是在集群式设备上用来做大规模数据处理的方法 用户定义一个特定的映射 函数将使用该映射对一系列键值对进行处理 直接
  • C++日期累加

    日期累加 题目描述 设计一个程序能计算一个日期加上若干天后是什么日期 输入描述 输入第一行表示样例个数m 接下来m行每行四个整数分别表示年月日和累加的天数 输出描述 输出m行 每行按yyyy mm dd的个数输出 include
  • 深度学习(二):张量和基本运算

    张量是一个类型化的n维数组 tf Tensor 是tensorflow基本数据格式 张量的阶就是数组的维度 张量的属性 图 形状 名字 op print a graph 输出程序所在的内存地址 print a shape 输出张量a的维度
  • QtIFW学习

    1 构建Qt安装程序 1 1 Qt应用程序结构 1 2 不同操作系统常用的打包工具 1 2 1 多平台 GUI 安装程序 跨平台安装工具 1 2 2 windows 1 2 3 Linux 1 3 静态库与动态库的区别 2 程序打包过程 3
  • 一个注解就能下载任意对象?SpringBoot如此强大?

    下载功能应该是比较常见的功能了 虽然一个项目里面可能出现的不多 但是基本上每个项目都会有 而且有些下载功能其实还是比较繁杂的 倒不是难 而是麻烦 如果我说现在只需要一个注解就能帮你下载任意的对象 是不是觉得非常的方便 Download so
  • github一些有趣的使用场景和基本使用方法

    文章目录 github的使用入门 安装 Git 创建 GitHub 帐户 在本地设置 Git 克隆仓库 进行修改和提交 推送更改 拉取更新 删除Github上废弃的仓库 注意 github更多有趣的使用场景 协作和社交编程 文档和知识库 学
  • Spring Data JPA 使用Specification 实现动态查询

    实体对象 package com zzg entity import java util Date import javax persistence Column import javax persistence Entity import
  • 游戏开发UE4杂项系列:更换默认c++编辑器

    更换项目中使用的c 编辑器 编辑器 Edit gt 编辑器偏好设置 Edit Preference gt 通用 General gt 源代码 source code gt 源代码编辑器 source code edit 附加一个UE4使用交
  • 【死磕NIO】— 跨进程文件锁:FileLock

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