Java Best Practice: Concurrency

mozammal Hossain
7 min readMar 12, 2024

--

Concurrency is hard. Wherever possible, strive to rely on existing libraries rather than designing your own thread-safe solutions. Even if your implementation is correct and efficient, future code changes can introduce hard-to-detect bugs. If you find yourself writing comments detailing how your code interacts with the Java Memory Model it may be worth stepping back and double-checking that an acceptable solution doesn’t exist elsewhere. Several such resources are detailed in this article.

As a rule of thumb, follow these steps when looking to solve a multi-threaded problem:

1. When possible, use an existing framework. Frameworks can not only help avoid bugs but also add features like instrumentation.

2. For lower-level needs, look in java.util.concurrent and Google Guava Collection to see if there is an existing API you can use. (This includes the Atomic* classes when they are exactly the right abstraction, but you should avoid building your own lock-free concurrency abstractions using atomics as building blocks).

3. If not, use synchronized blocks and/or java.util.concurrent’s locks

4.1f that’s not good enough and you need to do non-blocking synchronization, use volatile’s, or java.util.concurrent.atomic, or the Atomic classes in Guava.

5. Lastly, if you believe you must write your own concurrency solution, avoid creating data races since it is extremely hard to get them right.

Chapter 11 of Effective Java and Java Concurrency in Practice should be required reading for anyone spending time working with multi-threaded code.

Immutable data structures are inherently thread-safe

Because immutable data structures can never be modified, they are always safe to expose across threads. It’s often easier than you think to refactor immutability into your design, which gives you thread safety for free along with numerous other design benefits.

The simplest way to get thread safety is by using a strict form of immutability: your class must be final (or make subclassing impossible by having only private constructors, or be known to have only subclasses which also satisfy these constraints) and all its fields must be final and either primitive or objects which are themselves immutable (in this strict sense).

• Any object with a field of a mutable type cannot be considered inherently thread-safe and other measures must be taken (using one of the techniques described in the rest of this tutorial).

  • If any field is non-final because it is lazily initialized based on other states, thread safety requires an appropriate initialization mechanism.

Synchronized doesn’t mean Concurrent

Whenever multiple threads are interacting with shared (mutable) references those reads and writes must be synchronized in some way. However, synchronized blocks make your code thread-safe at the expense of concurrent execution. Any time execution enters a synchronized block or method any other thread trying to enter a synchronized block on the same object has to wait; even if in practice the operations are unrelated (e.g. they interact with different fields). This can dramatically reduce the benefit of trying to write multi-threaded code in the first place. In particular, needing the synchronized*( ) decorators in the Collections class often suggests a deeper design issue. Prefer to use proper concurrent collections and more granular locks when possible.

Locking with synchronized is a heavyweight form of ensuring ordering between threads, and there are a number of common APIs and patterns that you can use that are more lightweight, depending on your use case:

Compute a value once and make it available to all threads

Using a memoized Supplier is a painless way to efficiently provide an expensive value to multiple threads. A memoized Supplier ensures the desired value will be computed exactly once, after which all threads will have efficient read access. AutoValue also provides similar behavior via the ©Memoized annotation. There are other techniques for lazy initialization, but few are as easy to use as Suppliers.memoize( ).

Update Set and Map data structures across threads

Concurrent HashMap and its sibling Sets.newConcurrentHashSet provide fast, thread-safe data structures that allow for concurrent updates. While writes still require locking portions of the map, concurrent reads are very fast. Avoid using HashMap for cross-thread communication; even if wrapped with Collections.synchronizedMap( ) your program will be dramatically slowed down by the map-wide locks. If you need to compose multiple operations into a single atomic operation, which is not possible with ConcurrentHashMap or ConcurrentHashSet, use a lock guarding a thread-unsafe collection. Note that, in addition to using thread-safe containers, it is normally necessary to ensure that the values they contain are thread-safe, most easily by making them immutable. (It is possible to use a thread-safe container with non-thread-safe values if the container is being used to transfer “ownership” between threads and access to the values’ state is single-threaded, but this should be considered an advanced technique.)

Allow a group of threads to process a stream of data concurrently

Using a ConcurrentLinkedQueue or BlockingQueue implementation allows your processing threads to avoid interacting with each other at all. Instead, they read input from a queue and (potentially) write output to another. This way no other part of your implementation needs to be worried about thread-safety at all. Note that, in addition to using thread-safe containers, it is normally necessary to ensure that the values they contain are thread-safe, as described in the previous section.

Provide instances of a non-thread-safe type to multiple threads

The ThreadLocal class provides an efficient mechanism to make instances of a non-thread-safe class available to several threads, by constructing one instance per thread. While needing Thread Local in the first place is often a code-smell, it’s useful for cases beyond your control, such as the notorious SimpleDateFomat.

Update a value from multiple threads atomically

When applicable prefer the Atomic* classes such as Atomiclnteger to volatile fields or synchronized blocks. Like the volatile keyword the atomic wrapper classes guarantee that reads and writes will be atomic and visible across threads, but they also provide additional features like atomic compareAndSet ( ) and increment and decrement operations which can be tricky or impossible with volatile fields. Of course, when you’re trying to keep the state of multiple fields in sync you have to synchronize; atomicity is not sufficient. If you find yourself using a number of Atomic* instances as a group, using primitives and a synchronized block may well be both cleaner and faster. volatile and Atomic* provide faster reads than synchronized blocks though they may (depending on the architecture) still have some overhead on reads.
If the update operations you are performing are commutative (e.g. maintaining a minimum or maximum value), LongAccumulat or DoubleAccumulator perform better than Atomic* under contention.

Maintain granular control of your concurrency invariants

The JDK provides several synchronization aids including Semaphore, CountDownLatch, CyclicBarrier, and Phaser that enable you to create custom concurrency invariants. Document any usages of these classes and the invariants they enforce, and minimize their visibility so the invariants are easy for others to follow in the future.

Decouple concurrent processing from thread management

Prefer the executor framework; it’s more robust and powerful than writing manual thread management code. In essence, it separates how tasks are run from which tasks should run. Generally, you’re only really concerned with the latter. The Executors class provides many common Executor and ExecutorService implementations, and Guava’s MoreExecutors class provides even more options. Between the two the vast majority of multi-threading behavior you might want can be obtained with a single method call. The ForkJoinPool class is a special implementation of ExecutorService designed specifically for tasks that in turn spawn subtasks.

Documentation

Thread safety should be part of your class’ public API. Your users will (or should) assume your classes are not thread-safe unless they explicitly say so. You should go further and detail exactly what concurrency guarantees your class provides. At a minimum, mention what level of thread safety your class supports:

• Immutable Instances of this class appear constant. No external synchronization is necessary. Examples include String, Long, BigInteger, etc.

• Unconditionally thread-safe- Instances of this class are mutable, but the class has sufficient internal synchronization that its instances can be used concurrently without the need for any external synchronization. Examples include AtomicLong and Concurrent HashMap.

• Conditionally thread-safe- Like unconditionally thread-safe, except that some methods require external synchronization for safe concurrent use. Examples include the collections returned by the Collections. synchronized wrappers, whose iterators require external synchronization.

• Not thread-safe — Instances of this class are mutable. To use them concurrently, clients must surround each method invocation (or invocation sequence) with external synchronization of the client’s choosing. Examples include general-purpose collection implementations, such as ArrayList and HashMap.

Thread-safety annotations

To document thread safety in an unambiguous, machine-readable way, you can use the existing annotations @Immutable, @ThreadSafe, and @javax. annotation.concurrent.NotThreadSafe instead of textual comments. For conditionally thread-safe classes, add a comment explaining the precise conditions instead. Use @GuardedBy to indicate when a field must be accessed within a lock.

Testing

Testing multi-threaded code is even harder than writing it, there’s no way around it. Following the advice in this article(notably using existing APIs rather than implementing it yourself in the first place) can help.
One thing to remember is that you’re writing code with undefined behavior (namely its execution order), so avoid writing tests that depend on a particular order, or inspect state at arbitrary times. Instead, look at the invariants and guarantees your class makes, and limit your tests to that behavior. This might mean your tests have less coverage than you’d like, but it’s better than defining brittle and/or change-detector tests that depend on implementation details. If identifying testable invariants is difficult, it’s possible the guarantees of the class itself are under-specified.

Sign up to discover human stories that deepen your understanding of the world.

Free

Distraction-free reading. No ads.

Organize your knowledge with lists and highlights.

Tell your story. Find your audience.

Membership

Read member-only stories

Support writers you read most

Earn money for your writing

Listen to audio narrations

Read offline with the Medium app

--

--

No responses yet

Write a response