大家好,我是大明哥,一个专注于【死磕 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。

  1. public abstract FileLock lock(long position, long size, boolean shared) throws IOException;
  2. public final FileLock lock() throws IOException;
  3. public abstract FileLock tryLock(long position, long size, boolean shared) throws IOException;
  4. public final FileLock tryLock() throws IOException
  • lock() 是阻塞式的,它要阻塞进程直到锁可以获得,或调用lock()的线程中断,或调用lock()的通道关闭。
  • tryLock()是非阻塞式的,它设法获取锁,但如果不能获得,例如因为其他一些进程已经持有相同的锁,而且不共享时,它将直接从方法调用返回。

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

  • position:锁定文件中的开始位置
  • size:锁定文件中的内容长度
  • shared:是否使用共享锁。true为共享锁;false为独占锁。

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

示例

不使用文件锁来读写文件

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

  • 进程1
  1. RandomAccessFile randomAccessFile = new RandomAccessFile("/Users/chenssy/Downloads/filelock.txt","rw");
  2. FileChannel fileChannel = randomAccessFile.getChannel();
  3. // 这里是独占锁
  4. //FileLock fileLock = fileChannel.lock();
  5. System.out.println("进程 1 开始写内容:" + LocalTime.now());
  6. for(int i = 1 ; i <= 10 ; i++) {
  7. randomAccessFile.writeChars("chenssy_" + i);
  8. // 等待两秒
  9. TimeUnit.SECONDS.sleep(2);
  10. }
  11. System.out.println("进程 1 完成写内容:" + LocalTime.now());
  12. // 完成后要释放掉锁
  13. //fileLock.release();
  14. fileChannel.close();
  15. randomAccessFile.close();
  • 进程2
  1. RandomAccessFile randomAccessFile = new RandomAccessFile("/Users/chenssy/Downloads/filelock.txt","rw");
  2. FileChannel fileChannel = randomAccessFile.getChannel();
  3. // 这里是独占锁
  4. //FileLock fileLock = fileChannel.lock();
  5. System.out.println("开始读文件的时间:" + LocalTime.now());
  6. for(int i = 0 ; i < 10 ; i++) {
  7. // 这里直接读文件的大小
  8. System.out.println("文件大小为:" + randomAccessFile.length());
  9. // 这里等待 1 秒
  10. TimeUnit.SECONDS.sleep(1);
  11. }
  12. System.out.println("结束读文件的时间:" + LocalTime.now());
  13. // 完成后要释放掉锁
  14. //fileLock.release();
  15. fileChannel.close();
  16. randomAccessFile.close();

运行结果

  • 进程1

  • 进程2

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

使用文件锁读写文件

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

  • 进程1

  • 进程2

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

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

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

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

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

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

  • 线程1先获取文件锁

  • 线程2先获取文件锁

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

源码分析

下面我们以 FileLock lock(long position, long size, boolean shared)为例简单分析下文件锁的源码。lock()方法是由FileChannel的子类 FileChannelImpl来实现的。

  1. public FileLock lock(long position, long size, boolean shared) throws IOException {
  2. // 确认文件已经打开 , 即判断open标识位
  3. ensureOpen();
  4. if (shared && !readable)
  5. throw new NonReadableChannelException();
  6. if (!shared && !writable)
  7. throw new NonWritableChannelException();
  8. // 创建 FileLock 对象
  9. FileLockImpl fli = new FileLockImpl(this, position, size, shared);
  10. // 创建 FileLockTable 对象
  11. FileLockTable flt = fileLockTable();
  12. flt.add(fli);
  13. boolean completed = false;
  14. int ti = -1;
  15. try {
  16. // 标记开始IO操作 , 可能会导致阻塞
  17. begin();
  18. ti = threads.add();
  19. if (!isOpen())
  20. return null;
  21. int n;
  22. do {
  23. // 开始锁住文件
  24. n = nd.lock(fd, true, position, size, shared);
  25. } while ((n == FileDispatcher.INTERRUPTED) && isOpen());
  26. if (isOpen()) {
  27. // 如果返回结果为RET_EX_LOCK的话
  28. if (n == FileDispatcher.RET_EX_LOCK) {
  29. assert shared;
  30. FileLockImpl fli2 = new FileLockImpl(this, position, size,
  31. false);
  32. flt.replace(fli, fli2);
  33. fli = fli2;
  34. }
  35. completed = true;
  36. }
  37. } finally {
  38. // 释放锁
  39. if (!completed)
  40. flt.remove(fli);
  41. threads.remove(ti);
  42. try {
  43. end(completed);
  44. } catch (ClosedByInterruptException e) {
  45. throw new FileLockInterruptionException();
  46. }
  47. }
  48. return fli;
  49. }

首先会判断文件是否已打开,然后创建FileLock和FileLockTable 对象,其中FileLockTable是用于存放 FileLock的table。

  • 调用 begin()设置中断触发
  1. protected final void begin() {
  2. if (interruptor == null) {
  3. interruptor = new Interruptible() {
  4. public void interrupt(Thread target) {
  5. synchronized (closeLock) {
  6. if (!open)
  7. return;
  8. open = false;
  9. interrupted = target;
  10. try {
  11. AbstractInterruptibleChannel.this.implCloseChannel();
  12. } catch (IOException x) { }
  13. }
  14. }};
  15. }
  16. blockedOn(interruptor);
  17. Thread me = Thread.currentThread();
  18. if (me.isInterrupted())
  19. interruptor.interrupt(me);
  20. }
  • 调用 FileDispatcher.lock()开始锁住文件
  1. int lock(FileDescriptor fd, boolean blocking, long pos, long size,
  2. boolean shared) throws IOException
  3. {
  4. BlockGuard.getThreadPolicy().onWriteToDisk();
  5. return lock0(fd, blocking, pos, size, shared);
  6. }

lock0()的实现是在 FileDispatcherImpl.c 中,源码如下:

  1. JNIEXPORT jint JNICALL
  2. FileDispatcherImpl_lock0(JNIEnv *env, jobject this, jobject fdo,
  3. jboolean block, jlong pos, jlong size,
  4. jboolean shared)
  5. {
  6. // 通过fdval函数找到fd
  7. jint fd = fdval(env, fdo);
  8. jint lockResult = 0;
  9. int cmd = 0;
  10. // 创建flock对象
  11. struct flock64 fl;
  12. fl.l_whence = SEEK_SET;
  13. // 从position位置开始
  14. if (size == (jlong)java_lang_Long_MAX_VALUE) {
  15. fl.l_len = (off64_t)0;
  16. } else {
  17. fl.l_len = (off64_t)size;
  18. }
  19. fl.l_start = (off64_t)pos;
  20. // 如果是共享锁 , 则只读
  21. if (shared == JNI_TRUE) {
  22. fl.l_type = F_RDLCK;
  23. } else {
  24. // 否则可读写
  25. fl.l_type = F_WRLCK;
  26. }
  27. // 设置锁参数
  28. // F_SETLK : 给当前文件上锁(非阻塞)。
  29. // F_SETLKW : 给当前文件上锁(阻塞,若当前文件正在被锁住,该函数一直阻塞)。
  30. if (block == JNI_TRUE) {
  31. cmd = F_SETLKW64;
  32. } else {
  33. cmd = F_SETLK64;
  34. }
  35. // 调用fcntl锁住文件
  36. lockResult = fcntl(fd, cmd, &fl);
  37. if (lockResult < 0) {
  38. if ((cmd == F_SETLK64) && (errno == EAGAIN || errno == EACCES))
  39. // 如果出现错误 , 返回错误码
  40. return sun_nio_ch_FileDispatcherImpl_NO_LOCK;
  41. if (errno == EINTR)
  42. return sun_nio_ch_FileDispatcherImpl_INTERRUPTED;
  43. JNU_ThrowIOExceptionWithLastError(env, "Lock failed");
  44. }
  45. return 0;
  46. }

所以,其实文件锁的核心就是调用Linux的fnctl来从内核对文件进行加锁。

关于Linux 文件锁,大明哥推荐这两篇博客,小伙伴可以了解下:

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

  1. 【死磕NIO】— 阻塞、非阻塞、同步、异步,傻傻分不清楚

    万事从最基本的开始. 要想完全掌握 NIO,并不是掌握上面文章([死磕NIO]- NIO基础详解)中的三大组件就可以了,我们还需要掌握一些基本概念,如什么是 IO,5 种IO模型的区别,什么是阻塞&a ...

  2. 【死磕NIO】— 阻塞IO,非阻塞IO,IO复用,信号驱动IO,异步IO,这你真的分的清楚吗?

    通过上篇文章([死磕NIO]- 阻塞.非阻塞.同步.异步,傻傻分不清楚),我想你应该能够区分了什么是阻塞.非阻塞.异步.非异步了,这篇文章我们来彻底弄清楚什么是阻塞IO,非阻塞IO,IO复用,信号驱动 ...

  3. 【死磕 NIO】— Reactor 模式就一定意味着高性能吗?

    大家好,我是大明哥,我又来了. 为什么是 Reactor 一般所有的网络服务,一般分为如下几个步骤: 读请求(read request) 读解析(read decode) 处理程序(process s ...

  4. 【死磕 NIO】— Proactor模式是什么?很牛逼吗?

    大家好,我是大明哥. 上篇文章我们分析了高性能 IO模型Reactor模式,了解了什么是Reactor 模式以及它的三种常见的模式,这篇文章,大明再介绍另外一种高性能IO模型: Proactor. 为 ...

  5. 【死磕 NIO】— 深入分析Buffer

    大家好,我是大明哥,今天我们来看看 Buffer. 上面几篇文章详细介绍了 IO 相关的一些基本概念,如阻塞.非阻塞.同步.异步的区别,Reactor 模式.Proactor 模式.以下是这几篇文章的 ...

  6. 【死磕NIO】— 探索 SocketChannel 的核心原理

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

  7. 【死磕NIO】— NIO基础详解

    Netty 是基于Java NIO 封装的网络通讯框架,只有充分理解了 Java NIO 才能理解好Netty的底层设计.Java NIO 由三个核心组件组件: Buffer Channel Sele ...

  8. NIO文件锁FileLock

    目录 <linux文件锁flock> <NIO文件锁FileLock> <java程序怎么在一个电脑上只启动一次,只开一个进程> 文件锁可以是shared(共享锁) ...

  9. JAVA NIO 简介 (netty源码死磕1.1)

    [基础篇]netty 源码死磕1.1:  JAVA NIO简介 1. JAVA NIO简介 Java 中 New I/O类库 是由 Java 1.4 引进的异步 IO.由于之前老的I/O类库是阻塞I/ ...

随机推荐

  1. rtsp监控直播转码用到EasyNVR

    第一 下载EasyNVR_win_v2.6.18.0320 第二 安装启动 第三 配置转码路径 http://localhost:10800/ 最后转码播放 /hls/stream_1/stream_ ...

  2. 数据分析之客户价值模型(RFM)技术总结

    作者 | leo 管理学中有一个重要概念那就是客户关系管理(CRM),它核心目的就是为了提高企业的核心竞争力,通过提高企业与客户间的交互,优化客户管理方式,从而实现吸引新客户.保留老客户以及将已有客户 ...

  3. 【琉忆分享】新手如何学习PHP?附上PHP知识导图。

    你好,是我--琉忆.PHP程序员面试系列图书作者. 作为一名PHP开发者过来人,也是经历了菜鸟到老手的过程,在此给那些想学PHP的同学指条路,即使你是转行学PHP一样可以学会PHP. (如果觉得下面这 ...

  4. 基于XC7Z100+AD9361的双收双发无线电射频板卡

    一.板卡概述 板卡基于Xilinx公司的SoC架构(ARM+FPGA)的ZYNQ7100芯片和ADI公司高集成度的捷变射频收发器AD9361,实现频谱范围70MHz~6GHz,模拟带宽200KHz~5 ...

  5. Solution -「CTSC 2018」「洛谷 P4602」混合果汁

    \(\mathcal{Description}\)   Link.   \(n\) 种果汁,第 \(i\) 种美味度为 \(d_i\),每升价格 \(p_i\),一共 \(l_i\) 升.\(m\) ...

  6. nginx负载均衡初体验

    本例采取简单的轮询策略进行nginx的负载均衡处理. 在反向代理(参考:https://www.cnblogs.com/ilovebath/p/14771571.html)的基础上增加负载均衡处理的n ...

  7. 转:Minikuberar的含义很不错可以看看

    Kubernetes的主要意图是通过杂乱的负载均衡和资源分配功用跨服务器集群保管使用程序.即使某些服务器呈现毛病,也能够保证使用程序平稳运转.因而在出产布置中,有必要为Kubernetes装备多个服务 ...

  8. elk监听Java日志发送微信报警

    一年前写过logstash根据日志关键词报警 ,今年重温一下.并且记录一下遇到的问题解决办法. Java错误日志一般出现一大坨,如下图: 所以我们的filebeat日志收集器就要改成多行匹配模式,以日 ...

  9. Clickhouse 分布式表&本地表 &ClickHouse实现时序数据管理和挖掘

    一.CK 分布式表和本地表 (1)CK是一个纯列式存储的数据库,一个列就是硬盘上的一个或多个文件(多个分区有多个文件),关于列式存储这里就不展开了,总之列存对于分析来讲好处更大,因为每个列单独存储,所 ...

  10. 3款大数据bi工具,让企业数据分析更简单

    ​企业数据可视化的髙速发展趋势让互联网时代的数据分析及可视化拥有全新的面貌.企业针对信息内容的数据分析及可视化,的要求在日益严格,那么有哪些在企业数据分析方面做得好的大数据bi工具呢? 一.大数据bi ...