Skip to main content

Effective Java! Document Thread Safety

Users of classes you write need to know how they behave. One of the attributes of your class that a user needs to know is whether the class is thread-safe or not. Outside of this documentation a user of the class needs to guess about the class's thread-safety. This can lead either to excessive synchronization or insufficient synchronization which can lead to invariant issues.

It is important to know that thread-safety is not black and white. There are levels of thread safety.

As noted above, documenting a conditionally thread-safe class requires care. You must not only indicate that the class is not completely thread-safe but what locks must be obtained to make its non-thread-safe methods thread-safe. For example, let's look at the documentation for Collections.synchronizedMap:

It is imperative that the user manually synchronize on the returned map when iterating over any of its collection views:

  Map m = Collections.synchronizedMap(new HashMap());
      ...
  Set s = m.keySet();  // Needn't be in synchronized block
      ...
  synchronized (m) {  // Synchronizing on m, not s!
      Iterator i = s.iterator(); // Must be in synchronized block
      while (i.hasNext())
          foo(i.next());
  }
 
Failure to follow this advice may result in non-deterministic behavior.

Thread-safety documentation usually belongs in the class level documentation but if a specific method has special concerns the documentation can also live there. Documenting the thread-safety of static factories (like in the Collections.synchronizedMap example above) is also a good idea.

When a class uses a publicly accessible lock it can initially feel like it is enabling clients of the class more control. However, this flexibility can come at the cost. This is because it is incompatible with high-performance internal concurrent controls such as those used in ConcurrentHashMap. It also opens yourself up for a client holding the lock for a long time either accidentally or intentionally. In either case, it can lead to serious problems.

Alternatively, we can use a private lock object in our synchronized methods.

private final Object lock = new Object();

public void bar() {
  synchronized(lock) {
    ...
  }
}

There are a few things we can learn from this example. By marking the object as private we protect ourselves from someone external to our class holding the object for too long. We also mark it as final protecting ourselves from accidentally reassigning it and from subclasses changing it as well. Even when using locks from the java.util.concurrent.locks package we should always make them final.

As convenient and beneficial as this private lock pattern is, we can only use this method when writing unconditionally thread-safe objects. When writing a conditionally thread-safe object we are unable to do this because we have to provide the user of the class a handle to hold a lock when doing operations on the non-thread-safe parts of the object.

Thread-safety can be a tricky problem within an application. It becomes much more difficult when the classes we are using haven't documented what level of thread-safety they have implemented. There is no right or wrong level of thread-safety, different types of behavior can lend themselves to different levels of thread-safety. No matter the level we can document their status. Remember to make all your lock objects final and when possible also private. By following these guidelines we can mitigate some of the difficulty out of working in a concurrent environment.