对单例 class 方法的并发调用产生不一致的结果

Concurrent calls to singleton class method produces inconsistent results

我有一个单例 class,它有一个从目录中读取所有文件的方法。传入configRootDirContentType(An Enum for type reference)。readAllConfigsFromLocalDisk方法列出目录下的所有文件,并根据文件内容逐个处理,将文件内容映射到预期的对象类型到 ContentType 参数。

// Config type reference
public enum ConfigType {
    MY_TYPE, MY_OTHER_TYPE
}

// Singleton class
public class Singleton {
    private static Singleton instance;
    private Map<String, MyType> myTypeMap = new HashMap();
    private Map<String, MyOtherType> myOtherTypeMap = new HashMap();

    private Singleton() {}

    public synchronized static Singleton getSingleton() {
        if (istance == null)
            istance = new Singleton();
        return istance;
    }

    public Map<String,MyType> getMyTypeMap(String filePath, ConfigType configType, String filePattern){
        myTypeMap.clear();
        readAllConfigsFromLocalDisk(configRootDir, configType, filePattern);
        return myTypeMap;
    }

    public Map<String,MyOtherType> getMyOtherTypeMap(String filePath, ConfigType configType, String filePattern){
        myOtherTypeMap.clear();
        readAllConfigsFromLocalDisk(configRootDir, configType, filePattern);
        return myOtherTypeMap;
    }

    /**
     * Get all files in config root directory and parse one by one
     * @param configRootDir Root directory for configurations
     * @param configType Configuration type
     * @param filePattern File pattern
     */
    private void readAllConfigsFromLocalDisk(String configRootDir, ConfigType configType, String filePattern) {
        try (Stream<Path> walk = Files.walk(Paths.get(configRootDir))) {
            Pattern pattern = Pattern.compile(filePattern);
            List<Path> filePaths = getLocalFilePaths(walk, pattern);

            if (!filePaths.isEmpty()) {
                for (Path filePath : filePaths) {
                    String relativePath = filePath.toString();
                    parseConfigFile(relativePath, configType);
                }
            }
        } catch (IOException ex) {
            logger.error("Specified config root directory not found.", ex);
        }
    }

    /**
     * Read a given configuration file  from local disk and map to specified config type
     *
     * @param configFile Relative path to config file on local disk
     * @param configType Configuration type (MY_TYPE or MY_OTHER_TYPE)
     */
    private void parseConfigFile(String filePath, ConfigType configType ){
        String configContent = Files.readString(Paths.get(filePath), Charsets.UTF_8);
        
        // Parse based on config type and overwrite map
        switch (configType) {
            case MY_TYPE:
                MyTypeConf myTypeConf = Core.getMapper().readValue(configContent, MyTypeConf.class);
                List<MyType> myTypeRefs = myTypeConf.getMyTypeList();
                myTypeMap.putAll(myTypeRefs.stream().collect(Collectors.toMap(MyType::getId, Function.identity())));
            case MY_OTHER_TYPE:
                MyOtherTypeConf myOtherTypeConf = Core.getMapper().readValue(configContent, MyOtherTypeConf.class);
                List<MyOtherType> myOtherTypeRefs = myOtherTypeConf.getMyOtherTypeList();
                myOtherTypeMap.putAll(myOtherTypeRefs.stream().collect(Collectors.toMap(MyOtherType::getId, Function.identity())));
        }
    }

    /**
     * Get file paths of all matching files exist in configured streaming directory and sub folders from disk.
     *
     * @param walk    Stream of paths in config root directory.
     * @param pattern Pattern to math when discovering files.
     * @return List of Path objects for all files matching the pattern.
     */
    private List<Path> getLocalFilePaths(Stream<Path> walk, Pattern pattern) {
        return walk.filter(Files::isRegularFile).filter(p -> {
            String fileName = p.getFileName().toString();
            Matcher matcher = pattern.matcher(fileName);
            return matcher.matches();
        }).collect(Collectors.toList());
    }
}

两个 public 方法 getMyTypeMapgetMyOtherTypeMap 被一组 Akka actor 同时调用。在某些情况下,将文件内容映射到对象时,我得到 com.fasterxml.jackson.databind.exc.UnrecognizedPropertyException

似乎原因是 configContent 在尝试将其映射到 MyOtherType 时实际上 MyType 可解析,反之亦然。

我看了其他几个地方,但无法全面了解它。我试图了解同时调用 readFile 时会发生什么,以及为什么它会混淆文件内容。有人可以帮助我理解这一点吗?提前致谢。

您声明了两个共享变量:

private Map<String, MyType> myTypeMap = new HashMap();
private Map<String, MyOtherType> myOtherTypeMap = new HashMap();

由于 HashMap 不是 thread-safe,最奇怪的事情可能发生在多个线程同时访问它的一个实例(并且至少有一个线程正在修改它)时。

使用线程安全映射不会解决语义问题,因为 getMyTypeMap return 的所有调用都是同一个映射实例并对其进行操作,因此调用者不能使用 returned 映射可靠,因为其他线程仍在执行 getMyTypeMap 正在(再次)更改它。这同样适用于 getMyOtherTypeMap.

的并发调用

由于每个方法都以 clear() 调用开始,似乎不打算在方法的不同调用之间共享数据,因此,这些方法不应共享数据。

看来,你的主要障碍是如何重用代码来获得不同的结果类型。不要使用 enum 类型:

public class Singleton {
    /**
     * Classes are already lazily initialized, on first getSingleton() call
     */
    private static final Singleton instance = new Singleton();

    private Singleton() {}

    public static Singleton getSingleton() {
        return instance;
    }

    public Map<String, MyType> getMyTypeMap(String configRootDir){
        return readAllConfigsFromLocalDisk(configRootDir, "my-type-file-pattern",
            MyTypeConf.class, MyTypeConf::getMyTypeList, MyType::getId);
    }

    public Map<String, MyOtherType> getMyOtherTypeMap(String configRootDir){
        return readAllConfigsFromLocalDisk(configRootDir, "my-other-type-file-pattern",
            MyOtherTypeConf.class,MyOtherTypeConf::getMyOtherTypeList,MyOtherType::getId);
    }

    /**
     * Get all files in config root directory and parse one by one
     * @param configRootDir Root directory for configurations
     * @param filePattern File pattern
     * @param confType Configuration type (MyTypeConf.class or MyOtherTypeConf.class)
     * @param getList Configuration type specific list accessor method
     * @param getId Result type specific Id accessor for the map key
     */
    private <T,C> Map<String,T> readAllConfigsFromLocalDisk(
        String configRootDir, String filePattern,
        Class<C> confType, Function<C,List<T>> getList, Function<T,String> getId) {

        try(Stream<Path> walk = Files.walk(Paths.get(configRootDir))) {
            Pattern pattern = Pattern.compile(filePattern);

            return getLocalFilePaths(walk, pattern)
                .flatMap(p -> this.parseConfigFile(p, confType, getList))
                .collect(Collectors.toMap(getId, Function.identity()));

        } catch(IOException|UncheckedIOException ex) {
            logger.error("Specified config root directory not found.", ex);
            return Collections.emptyMap();
        }
    }

    /**
     * Read a given configuration file from local disk and map to specified config type
     *
     * @param configFile Path to config file on local disk
     * @param configType Configuration type (MyTypeConf.class or MyOtherTypeConf.class)
     * @param getList Configuration type specific list accessor method
     */
    private <T,C> Stream<T> parseConfigFile(
        Path configFile, Class<C> configType, Function<C,List<T>> getList) {

        try {
            C conf=Core.getMapper().readValue(Files.readString(configFile), configType);
            List<T> tRefs = getList.apply(conf);
            return tRefs.stream();
        } catch(IOException ex) {
            throw new UncheckedIOException(ex);
        }
    }

    /**
     * Get file paths of all matching files exist in configured streaming directory
     * and sub folders from disk.
     *
     * @param walk    Stream of paths in config root directory.
     * @param pattern Pattern to math when discovering files.
     * @return Stream of Path objects for all files matching the pattern.
     */
    private Stream<Path> getLocalFilePaths(Stream<Path> walk, Pattern pattern) {
        return walk.filter(Files::isRegularFile).filter(p -> {
            String fileName = p.getFileName().toString();
            Matcher matcher = pattern.matcher(fileName);
            return matcher.matches();
        });
    }
}