A bug in JDK, be careful when monitoring file changes

background

In some business scenarios, we need to implement the function of monitoring file content changes by ourselves, such as monitoring whether a file has changed, and reloading the content of the file when it changes.

It seems to be a relatively simple function, but under certain JDK versions, unexpected bugs may appear.

This article will take you to a simple implementation of a corresponding function, and analyze the corresponding bugs and advantages and disadvantages.

Initial realization ideas

To monitor file changes and read files, the simple idea is as follows:

  • Start a single thread and regularly obtain the timestamp of the last update of the file (unit: milliseconds);
  • Compare the last time stamp, if it is inconsistent, it means that the file has been changed, and then reload it;

Here is a demo of a simple function implementation (excluding the timed task part):

public class FileWatchDemo {
​
  /**
   * 上次更新时间
   */
  public static long LAST_TIME = 0L;
​
  public static void main(String[] args) throws IOException {
​
    String fileName = "/Users/zzs/temp/1.txt";
    // 创建文件,仅为实例,实践中由其他程序触发文件的变更
    createFile(fileName);
​
    // 执行2次
    for (int i = 0; i < 2; i++) {
      long timestamp = readLastModified(fileName);
      if (timestamp != LAST_TIME) {
        System.out.println("文件已被更新:" + timestamp);
        LAST_TIME = timestamp;
        // 重新加载,文件内容
      } else {
        System.out.println("文件未更新");
      }
    }
  }
​
  public static void createFile(String fileName) throws IOException {
    File file = new File(fileName);
    if (!file.exists()) {
      boolean result = file.createNewFile();
      System.out.println("创建文件:" + result);
    }
  }
​
  public static long readLastModified(String fileName) {
    File file = new File(fileName);
    return file.lastModified();
  }
}

In the above code, first create a file (convenient for testing), then read the modification time of the file twice, and record the last modification time with LAST_TIME. If the latest change time of the file is inconsistent with the last time, the modification time is updated and business processing is performed.

The for loop twice in the sample code is to demonstrate the two cases of change and no change. Execute the program and print the log as follows:

文件已被更新:1653557504000
文件未更新

The execution result is as expected.

This solution obviously has two disadvantages:

  • It is impossible to perceive the changes of files in real time. After all, there is a time difference in program rotation;
  • The time unit returned by lastModified is milliseconds. If two changes occur in the same millisecond, and the scheduled task query happens to fall between the two changes, the latter change cannot be perceived.

The first disadvantage has little impact on the business; the probability of the second disadvantage is relatively small and can be ignored;

JDK bug debut

The above code is implemented, there is no problem under normal circumstances, but if the Java version you are using is 8 or 9, unexpected bugs may occur, which are caused by the bugs of the JDK itself.

The bug numbered JDK-8177809 is described as follows:

JDK-8177809

The bug address is: bugs.java.com/bugdatabase…

The basic description of this bug is: in some versions of Java 8 and 9, the timestamp returned by the lastModified method is not milliseconds, but seconds, which means that the last three digits of the returned result are always 0.

Let's write a program to verify:

public class FileReadDemo {
​
  public static void main(String[] args) throws IOException, InterruptedException {
​
    String fileName = "/Users/zzs/temp/1.txt";
    // 创建文件
    createFile(fileName);
​
    for (int i = 0; i < 10; i++) {
      // 向文件内写入数据
      writeToFile(fileName);
      // 读取文件修改时间
      long timestamp = readLastModified(fileName);
      System.out.println("文件修改时间:" + timestamp);
      // 睡眠100ms
      Thread.sleep(100);
    }
  }
​
  public static void createFile(String fileName) throws IOException {
    File file = new File(fileName);
    if (!file.exists()) {
      boolean result = file.createNewFile();
      System.out.println("创建文件:" + result);
    }
  }
​
  public static void writeToFile(String fileName) throws IOException {
    FileWriter fileWriter = new FileWriter(fileName);
    // 写入随机数字
    fileWriter.write(new Random(1000).nextInt());
    fileWriter.close();
  }
​
  public static long readLastModified(String fileName) {
    File file = new File(fileName);
    return file.lastModified();
  }
}

在上述代码中,先创建一个文件,然后在for循环中不停的向文件写入内容,并读取修改时间。每次操作睡眠100ms。这样,同一秒就可以多次写文件和读修改时间。

执行结果如下:

文件修改时间:1653558619000
文件修改时间:1653558619000
文件修改时间:1653558619000
文件修改时间:1653558619000
文件修改时间:1653558619000
文件修改时间:1653558619000
文件修改时间:1653558620000
文件修改时间:1653558620000
文件修改时间:1653558620000
文件修改时间:1653558620000

修改了10次文件的内容,只感知到了2次。JDK的这个bug让这种实现方式的第2个缺点无限放大了,同一秒发生变更的概率可比同一毫秒发生的概率要大太多了。

PS:在官方Bug描述中提到可以通过Files.getLastModifiedTime来实现获取时间戳,但笔者验证的结果是依旧无效,可能不同版本有不同的表现吧。

更新解决方案

Java 8目前是主流版本,不可能因为JDK的该bug就换JDK吧。所以,我们要通过其他方式来实现这个业务功能,那就是新增一个用来记录文件版本(version)的文件(或其他存储方式)。这个version的值,可在写文件时按照递增生成版本号,也可以通过对文件的内容做MD5计算获得。

如果能保证版本顺序生成,使用时只需读取版本文件中的值进行比对即可,如果变更则重新加载,如果未变更则不做处理。

如果使用MD5的形式,则需考虑MD5算法的性能,以及MD5结果的碰撞(概率很小,可以忽略)。

下面以版本的形式来展示一下demo:

public class FileReadVersionDemo {
​
  public static int version = 0;
​
  public static void main(String[] args) throws IOException, InterruptedException {
​
    String fileName = "/Users/zzs/temp/1.txt";
    String versionName = "/Users/zzs/temp/version.txt";
    // 创建文件
    createFile(fileName);
    createFile(versionName);
​
    for (int i = 1; i < 10; i++) {
      // 向文件内写入数据
      writeToFile(fileName);
      // 同时写入版本
      writeToFile(versionName, i);
      // 监听器读取文件版本
      int fileVersion = Integer.parseInt(readOneLineFromFile(versionName));
      if (version == fileVersion) {
        System.out.println("版本未变更");
      } else {
        System.out.println("版本已变化,进行业务处理");
      }
      // 睡眠100ms
      Thread.sleep(100);
    }
  }
​
  public static void createFile(String fileName) throws IOException {
    File file = new File(fileName);
    if (!file.exists()) {
      boolean result = file.createNewFile();
      System.out.println("创建文件:" + result);
    }
  }
​
  public static void writeToFile(String fileName) throws IOException {
    writeToFile(fileName, new Random(1000).nextInt());
  }
​
  public static void writeToFile(String fileName, int version) throws IOException {
    FileWriter fileWriter = new FileWriter(fileName);
    fileWriter.write(version +"");
    fileWriter.close();
  }
​
  public static String readOneLineFromFile(String fileName) {
    File file = new File(fileName);
    String tempString = null;
    try (BufferedReader reader = new BufferedReader(new FileReader(file))) {
      //一次读一行,读入null时文件结束
      tempString = reader.readLine();
    } catch (IOException e) {
      e.printStackTrace();
    }
    return tempString;
  }
}

执行上述代码,打印日志如下:

版本已变化,进行业务处理
版本已变化,进行业务处理
版本已变化,进行业务处理
版本已变化,进行业务处理
版本已变化,进行业务处理
版本已变化,进行业务处理
版本已变化,进行业务处理
版本已变化,进行业务处理
版本已变化,进行业务处理

可以看到,每次文件变更都能够感知到。当然,上述代码只是示例,在使用的过程中还是需要更多地完善逻辑。

小结

本文实践了一个很常见的功能,起初采用很符合常规思路的方案来解决,结果恰好碰到了JDK的Bug,只好变更策略来实现。当然,如果业务环境中已经存在了一些基础的中间件还有更多解决方案。

而通过本篇文章我们学到了JDK Bug导致的连锁反应,同时也见证了:实践见真知。很多技术方案是否可行,还是需要经得起实践的考验才行。赶快检查一下你的代码实现,是否命中该Bug?

博主简介:《SpringBoot技术内幕》技术图书作者,酷爱钻研技术,写技术干货文章。

公众号:「程序新视界」,博主的公众号,欢迎关注~

技术交流:请联系博主微信号:zhuan2quan

Guess you like

Origin juejin.im/post/7117253921130676232