Apache Commons logo Commons Configuration

Configurations and Concurrent Access

Configuration objects are often central resources of an application and are accessed by multiple components. If multiple threads are involved which read or even update configuration data, care has to be taken that access to a Configuration object is properly synchronized to avoid data corruption or spurious exceptions. This section of the user's guide deals with concurrency and describes the actions necessary to make a Configuration work in a multi-threaded environment.

Synchronizers

Whether a Configuration object has to be thread-safe or not strongly depends on a concrete use case. For an application which only reads some configuration properties in its main() method at startup, it does not matter whether this configuration can safely be accessed from multiple threads. In this case, the overhead of synchronizing access to the configuration is not needed, and thus operations on the Configuration object can be more efficient. On the other hand, if the Configuration object is accessed by multiple components running in different threads it should better be thread-safe.

To support these different use cases, Commons Configuration takes a similar approach as the Java Collections framework. Here collections are per default not thread-safe (and thus more efficient). If an application needs a thread-safe collection, it can "upgrade" an existing one by calling a method of the Collections class.

Objects implementing the Configuration interface can be associated with a Synchronizer object. This synchronizer is triggered on each access to the configuration (distinguishing between read and write access). It can decide whether access is allowed or block the calling thread until it is safe to continue. Per default, a Configuration object uses a NoOpSynchronizer instance. As the name implies, this class does nothing to protect its associated configuration against concurrent access; its methods are just empty dummies. It is appropriate for use cases in which a configuration is only accessed by a single thread.

If multiple threads are involved, Configuration objects have to be thread-safe. For this purpose, there is another implementation of Synchronizer: ReadWriteSynchronizer. This class is based on the ReentrantReadWriteLock class from the JDK. It implements the typical behavior desired when accessing a configuration in a multi-threaded environment:

  • An arbitrary number of threads can read the configuration simultaneously.
  • Updates of a configuration can only happen with an exclusive lock; so if a thread changes configuration data, all other threads (readers and writers) are blocked until the update operation is complete.

The synchronizer associated with a Configuration can be changed at any time by calling the setSynchronizer() method. The following example shows how this method is used to make a Configuration instance thread-safe:

config.setSynchronizer(new ReadWriteSynchronizer());

Rather than setting the synchronizer on an existing Configuration instance, it is usually better to configure the configuration builder responsible for the creation of the configuration to set the correct synchronizer directly after a new instance has been created. This is done in the usual way by setting the corresponding property of a parameters object passed to the builder's configure() method, for instance:

Parameters params = new Parameters();
BasicConfigurationBuilder<PropertiesConfiguration> builder =
        new BasicConfigurationBuilder<PropertiesConfiguration>(
                PropertiesConfiguration.class)
                .configure(params.basic()
                        .setSynchronizer(new ReadWriteSynchronizer());
PropertiesConfiguration config = builder.getConfiguration();

It is also possible to set the synchronizer to null. In this case, the default NoOpSynchronizer is installed, which means that the configuration is no longer protected against concurrent access.

With the two classes NoOpSynchronizer and ReadWriteSynchronizer the Commons Configuration library covers the basic use cases of no protection and full protection of multi-threaded access. As the Synchronizer interface is pretty simple, applications are free to provide their own implementations according to their specific needs. However, this requires a certain understanding of internal mechanisms in Configuration implementations. Some caveats are provided in the remaining of this chapter.

Basic operations and thread-safety

AbstractConfiguration already provides a major part of the implementation of correctly interacting with a Synchronizer object. Methods for reading configuration data (such as getProperty(), isEmpty(), or getKeys()) and for changing properties (e.g. setProperty(), addProperty(), or clearProperty()) already call the correct methods of the Synchronizer. These methods are declared final to avoid that subclasses accidently break thread-safety by incorrectly usage of the Synchronizer.

Classes derived from AbstractConfiguration sometimes offer specific methods for accessing properties. For instance, hierarchical configurations offer operations on whole subtrees, or INIConfiguration allows querying specific sections. These methods are also aware of the associated Synchronizer and invoke it correctly.

There is another pair of methods available for each Configuration object allowing direct control over the Synchronizer: lock() and unlock(). Both methods expect an argument of type LockMode which tells them whether the configuration is to be locked for read or write access. These methods can be used to extend the locking behavior of standard methods. For instance, if multiple properties are to be added in an atomic way, lock() can be called first, then all properties are added, and finally unlock() is called. Provided that a corresponding Synchronizer implementation is used, other threads will not interfere with this sequence. Note that it is important to always call unlock() after a lock() call; this is done best in a finally block as shown in the following example:

config.lock(LockMode.WRITE);
try
{
    config.addProperty("prop1", "value1");
    ...
    config.addProperty("prop_n", "value_n");
}
finally
{
    config.unlock(LockMode.WRITE);
}

So, in a nutshell: When accessing configuration data from standard configuration classes all operations are controlled via the configuration's Synchronizer object. Client code is only responsible for setting a correct Synchronizer object which is suitable for the intended use case.

Other flags

In addition to the actual configuration data, each Configuration object has some flags controlling its behavior. One example for such a flag is the boolean throwExceptionOnMissing property. Other helper objects like the object responsible for interpolation or the expression engine for hierarchical configurations fall into the same category. The manipulation of those flags and helper objects is also related to thread-safety.

In contrast to configuration data, access to flags is not guarded by the Synchronizer. This means that when changing a flag in a multi-threaded environment, there is no guarantee that this change is visible to other threads.

The reason for this design is that the preferred way to create a Configuration object is using a configuration builder. The builder is responsible for fully initializing the configuration; afterwards, no behavioral changes should be performed any more. Because builders are always synchronized the values of all flags are safely published to all involved threads.

If there really is the need to change a flag later on in the life-cycle of a Configuration object, the lock() and unlock() methods described in the previous section should be used to do the change with a write lock held.

Special cases

Thread-safety is certainly a complex topic. This section describes some corner cases which may occur when some of the more advanced configuration classes are involved.

  • All hierarchical configurations derived from BaseHierarchicalConfiguration internally operate on a nodes structure implemented by immutable nodes. This is beneficial for concurrent access. It is even possible to share (sub) trees of configuration nodes between multiple configuration objects. Updates of these structures are implemented in a thread-safe and non-blocking way - even when using the default NoOpSynchronizer. So the point to take is that when using hierarchical configurations it is not required to set a special synchronizer because safe concurrent access is already a basic feature of these classes. The only exception is that change events caused by updates of a configuration's data are not guaranteed to be delivered in a specific order. For instance, if one thread clears a configuration and immediately afterwards another thread adds a property, it may be the case that the clear event arrives after the add property event at an event listener. If the listener relies on the fact that the configuration is empty now, it may be up for a surprise. In cases in which the sequence of generated configuration events is important, a fully functional synchronizer object should be set.
  • CombinedConfiguration is a bit special regarding lock handling. Although derived from BaseHierarchicalConfiguration, this class is not thread-safe per default. So if accessed by multiple threads, a suitable synchronizer has to be set. An instance manages a node tree which is constructed dynamically from the nodes of all contained configurations using the current node combiner. When one of the child configurations is changed the node tree is reset so that it has to be re-constructed on next access. Because this operation changes the configuration's internal state it is performed with a write lock held. So even if only data is read from a CombinedConfiguration, it may be the case that temporarily a write lock is obtained for constructing the combined node tree. Note that the synchronizers used for the children of a combined configuration are independent. For instance, if configuration objects derived from BaseHierarchicalConfiguration are added as children to a CombinedConfiguration, they can continue using a NoOpSynchronizer.
  • Derived from CombinedConfiguration is DynamicCombinedConfiguration which extends its base class by the ability to manage multiple combined configuration instances. The current instance is selected based on a key constructed by a ConfigurationInterpolator instance. If this yields a key which has not been encountered before, a new CombinedConfiguration object is created. Here again it turns out that even a read access to a DynamicCombinedConfiguration may cause internal state changes which require a write lock to be held. When creating a new child combined configuration it is passed the Synchronizer of the owning DynamicCombinedConfiguration; so there is actually only a single Synchronizer controlling the access to all involved configurations.

Read-only configurations

Objects that are not changed typically play well in an environment with multiple threads - provided that they are initialized in a safe way. For the safe initialization of Configuration objects specialized builders are responsible. These are classes derived from BasicConfigurationBuilder. Configuration builders are designed to be thread-safe: their getConfiguration() method is synchronized, so that configurations can be created and initialized in a safe way even if multiple threads are interacting with the builder. Synchronization also ensures that all values stored in member fields of newly created Configuration objects are safely published to all involved threads.

As long as a configuration returned freshly from a builder is not changed in any way, it can be used without a special Synchronizer (this means that the default NoOpSynchronizer is used). As was discussed in the previous section, there are special cases in which read-only access to Configuration objects causes internal state changes. This would be critical without a fully functional Synchronizer object. However, the builders dealing with affected classes are implemented in a way that they take care about these special cases and perform extra initialization steps which make write locks for later read operations unnecessary.

For instance, the builder for combined configurations explicitly accesses a newly created CombinedConfiguration object so that it is forced to construct its node tree. This happens in the builder's getConfiguration() method which is synchronized. So provided that the combined configuration is not changed (no other child configurations are added, no updates are performed on existing child configurations), no protection against concurrent access is needed - a simple NoOpSynchronizer can do the job.

Situation is similar for the other special cases described in the previous section. One exception is DynamicCombinedConfiguration: Whether an instance can be used in a read-only manner without a fully functional Synchronizer depends on the way it constructs its keys. If the keys remain constant during the life time of an instance (for instance, they are based on a system property specified as startup option of the Java virtual machine), NoOpSynchronizer is sufficient. If the keys are more dynamic, a fully functional Synchronizer is required for concurrent access - even if only reads are performed.

So to sum up, except for very few cases configurations can be read by multiple threads without having to use a special Synchronizer. For this to be safe, the configurations have to be created through a builder, and they must not be updated by any of these threads. A good way to prevent updates to a Configuration object is to wrap it by an immutable configuration.