1

I'm a novice at developing thread-safe methods. I have a configuration service, implemented as a Singleton class, that needs to be thread-safe. When the service is started, a collection of configuration files are read and stored in a map. This only needs to happen once. I have used AtomicBoolean for the isStarted state field, but I am not sure if I have done this properly:

public class ConfigServiceImpl implements ConfigService {
    public static final URL PROFILE_DIR_URL =
           ConfigServiceImpl.class.getClassLoader().getResource("./pageobject_config/");

    private AtomicBoolean isStarted;
    private Map<String,ConcurrentHashMap<String,LoadableConfig>> profiles = new ConcurrentHashMap<>();

    private static final class Loader {
        private static final ConfigServiceImpl INSTANCE = new ConfigServiceImpl();
    }

    private ConfigServiceImpl() { }

    public static ConfigServiceImpl getInstance() {
        return Loader.INSTANCE;
    }

    @Override
    public void start() {
        if(!isStarted()) {
            try {
                if (PROFILE_DIR_URL != null) {
                    URI resourceDirUri = PROFILE_DIR_URL.toURI();
                    File resourceDir = new File(resourceDirUri);
                    @SuppressWarnings("ConstantConditions")
                    List<File> files = resourceDir.listFiles() != null ?
                            Arrays.asList(resourceDir.listFiles()) : new ArrayList<>();

                    files.forEach(this::addProfile);
                    isStarted.compareAndSet(false, true);
                }
            } catch (URISyntaxException e) {
                throw new IllegalStateException("Could not generate a valid URI for " + PROFILE_DIR_URL);
            }
        }
    }

    @Override
    public boolean isStarted() {
        return isStarted.get();
    }

    ....
}

I wasn't sure if I should set isStarted to true before populating the map, or even if this matters at all. Would this implementation be reasonably safe in a multi-threaded environment?

UPDATE:

Using zapl's suggestion to perform all initialization in the private constructor and JB Nizet's suggestion to use getResourceAsStream():

public class ConfigServiceImpl implements ConfigService {
    private static final InputStream PROFILE_DIR_STREAM =
            ConfigServiceImpl.class.getClassLoader().getResourceAsStream("./pageobject_config/");

    private Map<String,HashMap<String,LoadableConfig>> profiles = new HashMap<>();

    private static final class Loader {
        private static final ConfigServiceImpl INSTANCE = new ConfigServiceImpl();
    }

    private ConfigServiceImpl() {
        if(PROFILE_DIR_STREAM != null) {
            BufferedReader reader = new BufferedReader(new InputStreamReader(PROFILE_DIR_STREAM));
            String line;

            try {
                while ((line = reader.readLine()) != null) {
                    File file = new File(line);
                    ObjectMapper mapper = new ObjectMapper().registerModule(new Jdk8Module());
                    MapType mapType = mapper.getTypeFactory()
                            .constructMapType(HashMap.class, String.class, LoadableConfigImpl.class);

                    try {
                        //noinspection ConstantConditions
                        profiles.put(file.getName(), mapper.readValue(file, mapType));
                    } catch (IOException e) {
                        throw new IllegalStateException("Could not read and process profile " + file);
                    }

                }

                reader.close();
            } catch(IOException e) {
                throw new IllegalStateException("Could not read file list from profile directory");
            }
        }
    }

    public static ConfigServiceImpl getInstance() {
        return Loader.INSTANCE;
    }

    ...
}
Selena
  • 2,208
  • 8
  • 29
  • 49
  • So, all the callers will have to call `start()` before calling any other method? Why don't you put the initialization code in the getInstance() method, to make sure that the instance you get is **always** initialized? Also, what's the point of isStarted(), since two threads calling start() in parallel will both read the files and populate the map anyway? Have you considered using a dependency injection framework, which would allow avoiding the singleon anti-pattern? – JB Nizet Sep 19 '15 at 16:32
  • @JB Nizet They could, but they don't necessarily have to. They could first call 'isStarted()' and if the value is false, then attempt to start the service. I can't definitively know whether they will do one or the other, so I have to assume they can do either one. I like your idea of putting all this in the getInstance() method though. That would be pretty safe, I think. – Selena Sep 19 '15 at 16:35
  • As long as it's synchronized, yes. Or you could use the singleton holder idiom: https://en.wikipedia.org/wiki/Initialization-on-demand_holder_idiom – JB Nizet Sep 19 '15 at 16:36
  • Would you like to post this as an answer tot he question? – Selena Sep 19 '15 at 16:36

2 Answers2

2

The simplest threadsafe singleton is

public class ConfigServiceImpl implements ConfigService {
    private static final ConfigServiceImpl INSTANCE = new ConfigServiceImpl();
    private ConfigServiceImpl() {
        // all the init code here.
        URI resourceDirUri = PROFILE_FIR_URL.toURI();
        File resourceDir = new File(resourceDirUri);
        ...
    }

    // not synchronized because final field
    public static ConfigService getInstance() { return INSTANCE; }
}

The hidden constructor contains all the initialization and since INSTANCE is a final field you're guaranteed by the Java language guarantees that only ever 1 instance is created. And since instance creation means execution of init code in the constructor you're also guaranteed that initialization is done only once. No need for isStarted()/start(). It's basically bad practice to have classes that are complicated to use correctly. Without having to start it you can't forget it.

The "problem" with this code is that the initialization happens as soon as the class is loaded. You sometime want to delay that so it happens only once someone calles getInstance(). For that you can introduce a subclass to hold INSTANCE. That subclass is only loaded by the first call of getInstance.

It's often not even necessary to enforce later loading since typically, the first time you call getInstance is the time your class is loaded anyways. It becomes relevant if you use that class for something else. Like to hold some constants. Reading those loads the class even though you didn't want to initialize all the configs.


Btw a "correct" way to use AtomicBoolean to 1-time init something goes along the lines of:

AtomicBoolean initStarted = new AtomicBoolean();
volatile boolean initDone = false;
Thing thing = null;

public Thing getThing() {
    // only the 1st ever call will do this
    if (initStarted.compareAndSet(false, true)) {
        thing = init();
        initDone = true;
        return thing;
    }

    // all other calls will go here
    if (initDone) {
      return thing;
    } else {
        // you're stuck in a pretty undefined state
        return null;
    }
}
public boolean isInit() {
    return initDone;
}
public boolean needsInit() {
    return !initStarted.get();
}

The big problem is that in practice you want to wait until initialization is done and not return null so you'll probably never see code like that.

zapl
  • 63,179
  • 10
  • 123
  • 154
  • Thanks for this info. This answer will be really helpful in the future when I do need to those these Atomic data types. I have updated my question to show the implementation you suggest -- all initialization happens in the constructor. Once I have a chance to actually test this code, I will accept this as the answer. – Selena Sep 19 '15 at 17:57
  • 1
    @JasonDiplomat `synchronized` adds some overhead on every `getInstance()` call, access to a final fields is guaranteed to be threadsafe without that overhead. – zapl Jul 19 '17 at 14:32
1

Your code is not really thread-safe, since two threads might call start() concurrently and concurrently read the file and populate the map.

It's also unpleasant to use, because callers of your singleton will constantly have to check (or potentially falsely assume) that the singleton has been started already, or call start() before calling any other method to make sure it's started.

I would design it so that getInstance() always returns an initialized instance. Make sure getInstance() is synchronized to avoid two threads concurrently initializing the instance. Or use the initialization on demand holder idiom. Or even better, don't use the singleton anti-pattern, which makes the code hard to unit-test, and use a dependency injection framework instead.

JB Nizet
  • 678,734
  • 91
  • 1,224
  • 1,255
  • Do you have a link to an example of how I would do this via some other means than a singleton instance? My objective is to limit the reading of the configuration files to one time only. – Selena Sep 19 '15 at 16:44
  • You mean, an introduction to dependency injection? You might read the Motivation section of https://github.com/google/guice/wiki/Motivation. – JB Nizet Sep 19 '15 at 16:48
  • Thanks for the link. So, I posted an update to the original question -- would that be a thread-safe implementation that ensures the configuration files are read once? – Selena Sep 19 '15 at 16:52
  • I don't really see the point of your Loader. The `if` condition is incorrect, since you do the initialization is isStarted is true. If the Maps are supposed to be read-only, after the initialization is done, they don't need to be concurrent. And you should wrap them into unmodifiableMaps to guarantee they're never modified. You don't need an AtomicBoolean, since that variable is only accessed from a synchronized method. A simple boolean is sufficient. – JB Nizet Sep 19 '15 at 16:57
  • 1
    Another big unrelated problem is that your code will fail as soon as your code is packaged in a jar or war file. Don't assume resources loaded by the ClassLoader are files. They're not. They're just readable resources, loaded from a file, a jar file entry, or a war file entry. Use getResourceAsStream(). – JB Nizet Sep 19 '15 at 17:02
  • I know about the resources not always being files issue. I was going to handle it as soon as I got a threadsafe implementation for this, but I will add a note to my question to warn others about this unhandled issue. I will update to use unmodifiable map wrappers and get rid of the boolean and if condition. This singleton implementation is one I picked up from this discussion: http://stackoverflow.com/a/11165975/2200733 – Selena Sep 19 '15 at 17:08
  • I have decided to go with zapl's answer since it seems logically sound that it is a thread-safe implementation that does what I want. I also went with your suggestion to use getResourceAsStream. Thanks so much for the time you took with this question. It helps a lot as a total novice to have people willing to spend so much effort to deal with beginners. – Selena Sep 19 '15 at 17:59
  • @Selena I just spelled out the answer here :) – zapl Sep 19 '15 at 20:10
  • @zapl I will accept your answer once I have compiling code. I want to edit my question with corrections to my source before accepting an answer to it. – Selena Sep 19 '15 at 20:27