Sunday, June 13, 2010

Effectively immutable

Following on my previous post, another situation where shared state can be accessed without synchronization is when dealing with effectively immutable objects.

Here is a class to gather simple statistics during a multi-threaded batch process:
public final class Stats {
  public enum Counter { INPUTS, PARSE_ERRORS, PROCESSING_ERRORS, SUCCESSES, };
  
  private final EnumMap<Counter, AtomicInteger> values;
  
  public Stats() {
    values = new EnumMap<Counter, AtomicInteger>(Counter.class);
    // Pre-fill the map:
    for (Counter counter: Counter.values())
      values.put(counter, new AtomicInteger());
  }
  
  public void increment(Counter counter) { values.get(counter).incrementAndGet(); }
  
  @Override
  public String toString() {
    StringBuilder sb = new StringBuilder();
    for (Entry<Counter, AtomicInteger> entry: values.entrySet())
      sb.append(entry.getKey() + " = " + entry.getValue() + "\n");
    return sb.toString();
  }
}
A single instance is shared among all worker threads. Each worker increments the appropriate counters as it progresses:
stats.increment(Stats.Counter.INPUTS);
The statistics are output when all work is done:
System.out.println(stats);

The important point here is that access to the values field is not synchronized, yet Stats is still thread-safe. This is because values is effectively immutable: after it has been pre-filled in the constructor and safely published through a final field, it is never structurally modified. Therefore it is safe to assume that all invocations of increment will see it in a consistent state.


On the other hand, here is an example where the modification of an EnumMap is not safely published:
public class UnsafeEnumMapMutation { // DOESN'T WORK
  static enum Command { STOP }
  private static final EnumMap<Command, Integer> MAP =
    new EnumMap<Command, Integer>(Command.class);
  
  public static void main(final String[] args) throws InterruptedException {
    final ExecutorService executor = Executors.newFixedThreadPool(2);
    final Runnable looping = new Runnable() {
      public void run() {
        int i = 0;
        while (!MAP.containsKey(Command.STOP)) i++;
      }
    };
    executor.submit(looping);
    final Runnable stopper = new Runnable() {
      public void run() { MAP.put(Command.STOP, 1); }
    };
    executor.submit(stopper);
    executor.shutdown();
  }
}
As in my previous post, task stopper is trying to tell task looping to terminate. This time, the signal is the presence of a given key the map. However, this change is not protected by synchronization, and the map is not effectively immutable, since it is mutated after its initial publication as a final field. And again, this example loops when I run it with the -server option.

0 comments: