首页 > 解决方案 > 对单例类方法的并发调用会产生不一致的结果

问题描述

我有一个单例类,它有一个从目录中读取所有文件的方法。configRootDir传入and ContentType(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());
    }
}

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

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

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

标签: javaconcurrencysingleton

解决方案


您已经声明了两个共享变量:

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

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

使用线程安全映射不会解决语义问题,因为所有调用都getMyTypeMap返回相同的映射实例并对其进行操作,因此调用者无法可靠地使用返回的映射,因为仍在执行的其他线程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();
        });
    }
}

推荐阅读