If we write code that runs on more than one thread, sooner or later we meet the idea of immutable classes. The lack of ‘write’ functions makes them very useful in multi-threaded applications. In this article I’m going to show what immutable classes are in Java and Kotlin. We will also look at the practical example, where we use them to optimize the access to the mutable state. Finally, we will see how we can document immutability.

tl;dr;

Immutable classes help writing concurrent programs, because using them does not require locks. We can also use them to optimize the access to the mutable state. We can and we should document immutability and concurrency in general with JCIP annotations.

What are immutable classes?

Neither Kotlin nor Java have a keyword that mark immutable classes. Instead, we distinguish them by 3 attributes:

  1. all fields are read-only: marked with final (Java) or val (Kotlin)
  2. the object state is set by a constructor (possibly hidden behind a builder), and it does not change later,
  3. we cannot extend the class: in Java, it must have final modifier, in Kotlin all classes are final by default.

Below, we can see examples of the same immutable class in Java and Kotlin:

public final class ExchangeTable {
   private final String sourceCurrency;
   private final Map<String, BigInteger> conversionRates;

   public ExchangeTable(String sourceCurrency, Map<String, BigInteger> conversionRates) {
      this.sourceCurrency = sourceCurrency;
      this.conversionRates = new LinkedHashMap<>();
      this.conversionRates.putAll(conversionRates);
   }

   public BigInteger convert(String destination, BigInteger amount) {
      BigInteger rate = conversionRates.get(destination);
      if (null == rate) {
         throw new IllegalArgumentException("No such currency: " + destination);
      }
      return rate.multiply(amount);
   }
}
class ExchangeTable(
   private val sourceCurrency: String,
   conversionRates: Map<String, BigInteger>
) {
   private val _conversionRates = LinkedHashMap<String, BigInteger>().run {
      it.putAll(conversionRates)
   }

   fun convert(destination: String, amount: BigInteger): BigInteger {
      val rate = _conversionRates[destination] ?: throw IllegalArgumentException(
         "No such currency: $destination"
      )
      return rate.multiply(amount)
   }
}

We might ask why we should not extend immutable classes. It’s due to the nature of class inheritance. Suppose that A is immutable and we extend it with B. B can add some mutable state. At the same time, we can use objects of B in place of A which could be dangerous for the users of A.

Checking

Let’s see if the example meets all the criteria. When it comes to points 1 and 3, they are quite obvious. In both cases, all fields are read-only, and we cannot extend the class. The point 2 is trickier, because there are two ways leading to it. If our fields are primitive values or references to known immutable classes, we are fine: our class is still immutable. Here, sourceCurrency is such a field, because String is known to be immutable. The map of conversion rates represents the second way. We use LinkedHashMap which is a mutable data structure. However, we do 3 things that prevent changing it:

  1. we use write functions only in the constructor,
  2. the reference to the map never leaks out,
  3. in the constructor, we copy the input data into the actual map.

One might ask: what about lazy evaluation? Is it fine to use it in immutable classes? Some developers say: yes. Personally, I’m a bit cautious with that. For me, immutability is also a guarantee that the thread won’t block on e.g. a lock. With lazily computed state, we might need to add some locks internally.

In short…

Immutable classes can use mutable data structures internally, but we need to use additional defensive programming techniques that prevent changing their state or leaking out the references.

Properties of immutable classes

Immutable classes are essential for writing concurrent programs. It is the write access that requires synchronization. If nobody can change the data, we do not need locks anymore:

/**
 * Note: ImmutableMap<K, V> comes from Guava library
 */
data class Dictionary(private val entries: ImmutableMap<String, String>) {
    /**
     * Concurrent access from multiple threads is safe
     */
    fun find(key: String) = entries[key] ?: ""
}

Not only this class does not need locks. We no longer need to make defensive copies. If nobody can change the data, we can keep a single copy in the memory and use it safely in all places. Unfortunately, many data structures in our programs require write access. Can we still benefit from immutability then? Let’s find out.

Updating the immutable

The less obvious thing about immutable classes is that we can actually use them also for cases with mutable state. Let’s consider a configuration that drives the core logic of our service. Here’s what we know:

  • the service runs the core logic on several threads,
  • much of the logic needs the configuration data to complete the tasks,
  • the users can update the configuration on the fly…
  • … but they don’t do it too often (once per week).

In the naïve approach, we’d make the configuration mutable and protect the access with locks and local copies. However, this approach is optimized for writing, but we mostly want to… read. All read operations would have to pay the locking cost due to something that happens once per week.

Make the configuration

Instances of immutable classes don’t have to live to the end of the program. Once we don’t need them, we can feed the garbage collector with them. This is the trick that we will use here. The only thing that needs to change is the reference to the current version of the configuration. If the configuration changes, we can build the new object in the background and then swap the references. All other tasks should begin with copying just the reference, to ensure that the version does not swap for them in the middle of the execution. This is the illustration of the idea:

Illustration of using immutable classes for updating the configuration.
Making the configuration immutable

The source code:

class ConfigurationManager {
   @field:Volatile
   private var currentConfiguration: Configuration

   fun acquire() = currentConfiguration

   fun update(newConfiguration: Configuration) {
      currentConfiguration = newConfiguration
   }
}

class UpdateTask(
   private val configurationManager: ConfigurationManager,
   private val repository: ConfigurationRepository
) {
   fun execute() {
      val data = repository.readConfigurationData()
      val newConfiguration = Configuration.from(data)
      logger.info { "Swapping the configuration" }
      configurationManager.update(newConfiguration)
   }

   companion object {
      private val logger = KotlinLogging.logger { }
   }
}

class OtherTask(private val configurationManager: ConfigurationManager) {
   fun execute() {
      val configuration = configurationManager.acquire()

      // do your stuff here
   }
}

Key takeouts:

  • in line 3, the reference to the current configuration should be volatile to ensure that all threads see the new configuration immediately. Modern CPU-s can cache the old reference – with volatile, we force them to always read the value from the RAM memory. Bypassing cache is slower, but still much faster than locks.
  • in line 30, we must make sure that each task obtains the configuration exactly once, and then works solely on the obtained copy.

Let’s also notice that when the configuration changes, all the ongoing tasks can complete using the old configuration version. It is not erased from the memory, until the last task using it finishes.

In short…

We can use immutable classes also in scenarios, where we have a mutable state that changes rarely. Even if the cost of rebuilding the immutable instance is high, it pays off with much simpler programming model, and better performance of other tasks. We can also compute the new state in the background.

Documenting immutable classes

To document immutable classes and concurrency in general, we can use annotations introduced by Brian Goetz in his book “Java Concurrency in Practice”. The clean-room implementation is available on Github and uploaded into Maven repositories. Here’s the list of annotations:

  • @Immutable – marks immutable class or an interface that shall be implemented as an immutable.
  • @ThreadSafe – marks classes with mutable state that use locks or other synchronization mechanism. Using them by multiple threads is safe.
  • @NotThreadSafe – those classes can be used only by a single thread, or we need to synchronize the access on our own.
  • @GuardedBy – marks methods which we can call only when holding a particular lock.

In our example, we’d mark our classes like this:

@Immutable
data class Configuration(
   // ...
)

@ThreadSafe
class ConfigurationManager {
   // ...
}

Documenting concurrency matters. This is one of the things that is not obvious when looking at the code, and it greatly affects how we should use the given class. Usually, adding one of the concurrency annotations is sufficient, and for additional information it’s good to drop a short sentence in the javadoc.

In short…

Document your concurrency. JCIP annotations presented above can help you with that.

Conclusion

In this article we saw what immutable classes are. We also learned that immutability does not mean “throughout the whole application lifetime”. We can make immutable snapshots of a generally mutable state, and then use them for a limited time to reduce the complexity of the concurrent access. The question is: how often the state must change to prefer locks or other techniques? There’s no good answer to it. In this article, I presented an extreme example of a configuration that changes once per week. But it can also change as often as every second. I once wrote a renderer that accepted immutable snapshots of the scene descriptions. The scene could be updated very often, but the renderer had to read it 60 times per second to render a frame. As long as you can rebuild your snapshots in reasonable time, and the garbage collector can keep up, you’re fine.

Subscribe
Notify of
guest

0 Comments
Inline Feedbacks
View all comments