Java Concurrency in Practice – Thread Safety

In my last post, Java Concurrency in Practice: Study Notes, I finished my writing with a statement that all code paths accessing a shared state must be thread safe. But, what is thread-safety? A piece of code is said to be thread-safe when it continues to behave correctly when accessed from multiple threads. To put it in a slightly more formal way:

“A class is thread-safe if it behaves correctly when accessed from multiple threads, regardless of the scheduling or interleaving of the execution of those threads by the runtime environment, and with no additional synchronization or other coordination on the part of the calling code” [JCiP].

At the heart of the definition, there is something we call: correctness. As we programmers often don’t get precise specifications we have to define correctness as something we recognize when we see it – the code works. Since a single-threaded environment is just an edge case of a multi-threaded environment. A program, class, or any piece of code “cannot be thread-safe if it is not even correct in a single-threaded environment” [JCiP].

Let’s continue with an another definition:

“stateless objects are always thread-safe” [JCiP].

But, what do we mean by the state of the object? An object’s state includes any data that can affect its externally visible behavior.

“An object’s state is its data, stored in state variables such as instance or static fields. An object’s state may include fields from other, dependent objects” [JCiP].

For example, a HashMaps state is defined not just by its fields but also by the state of the key value pairs it contains. Now, let’s suppose that we have an object with a state accessible from multiple threads. We often define a set of actions over the object with pre- and post-conditions, invariants, that must hold true before or after any action. These invariants rule out part of the objects state space as invalid. If an object is correctly implemented, no sequence of operations can get the object into an invalid state.

Change of state might be tricky. Often, what seems like a single action operation, it might not be atomic, which means that it does not execute as a single, indivisible operation.

“Operations A and B are atomic with respect to each other if, from the perspective of a thread executing A, when another thread executes B, either all of B has executed or none of it has” [JCiP].

In the CarFactory example below the state of the factory instance is represented by the member variable nextId. The code below looks totally innocent until we realize that incrementing variable nextId is not an atomic operation. It consists of three separate actions: fetch the current value, add one to it, and write the new value back. If multiple threads can access the CarFactory instance with some unlucky timing two cars can get the same id. Depending on the application this can have fatal consequences.

...
@NotThreadSafe
class CarFactory {

  private long nextId = 1;
 
  public Car createCar() {
    Car c = new Sedan();
    c.setId(nextId++);
    return c;
  }
  ...
}

The possibility of getting incorrect results by unlucky timing is so important in concurrent programming that it has a name: a race condition.

“A race condition occurs when the correctness of a computation depends on the relative timing or interleaving of multiple threads by the runtime” [JCiP].

To avoid race conditions, there are multiple ways to prevent other threads from using a variable while we’re in the middle of modifying it. Our aim is to ensure that other threads can observe or modify the state only before we start or after we finish, but not in the middle. One way to make our CarFactory thread safe is to use the AtomicLong library class. Because method getAndIncrement ensures atomicity now we don’t have to worry about unlucky timing. Annotations @NotThreadSafe and @ThreadSafe are not part of the standard jdk. They are used throughout this article and in the book to differentiate between safe and unsafe patterns.

...
import java.util.concurrent.atomic.AtomicLong;

@ThreadSafe
class CarFactory {

  private AtomicLong nextId = new AtomicLong(1);
 
  public Car createCar() {
    Car c = new Sedan();
    c.setId(nextId.getAndIncrement());
    return c;
  }
  ...
}

But what happens if the state of the object is shared among multiple variables? Is it enough to replace all member variables with their atomic version? Of course, not.

To preserve state consistency, update related state variables in a single atomic operation” [JCiP].

Locking helps us to preserve state consistency be ensuring that a critical section can only be executed by a single thread at a time. Java provides the synchronized block as a built-in locking mechanism.

synchronized (lock) {
   // Access or modify shared state guarded by lock
}

Every java object has an intrinsic lock that can be used for synchronization purposes. This internal lock is automatically acquired when entering a synchronized block and automatically released when leaving the synchronized block – even if it is by throwing an exception. A special case of a synchronized block is when we make a whole method synchronized. By doing so, we synchronize on the objects intrinsic lock or in case of a static method the Class< ? > object is used.

Intrinsic locks are reentrant. If a thread tries to acquire a lock that it already holds, the request succeeds. Reentrancy means that locks are acquired on a per-thread rather than per-invocation basis. Reentrancy is implemented by associating with each lock an acquisition count and remembering the owning thread.

From a performance point of view, locking has its own challenges. We will discuss them in detail in a later chapter. For now, just remember one simple rule:

“Avoid holding locks during lengthy computations or operations at risk of not completing quickly such as network or console I/O” [JCiP].

In the next post, I’m going to deal with sharing objects: memory visibility, immutability, what is safe and unsafe publication. Stay tuned!

Resource
[JCiP] Java Concurrency in Practice by Brian Goetz, ISBN-10: 0321349601

Java Concurrency in Practice: Study Notes

A Personal Standpoint

Not long ago a guy from the HR department of the company I work asked if I have interest interviewing candidates for open positions. And I said something like: “Sure, why not”. On the first couple of occasions, I was only an observant. I asked a question or two, but the interview itself was conducted by a more experienced colleague. Then I started doing interviews on my own over the phone and started taking up the leading role in face-to-face interviews. Of course, every interview is different and each interviewer has a favorite set of questions but more or less all interviews are conducted based on a script prescribing which specific topics should be visited. As I started taking more and more responsibility for interviews I noticed that there was a topic that made me very uncomfortable. Often I just asked a couple of basic questions or sometimes even skipped it “because of a time constraint”. You have probably guessed by now, it was: concurrency. Of course, I had a basic concept of what concurrency is and what are the main pitfalls. However, I only had limited knowledge and experience with writing concurrent Java programs. I thought to myself, it would a good time to study the topic of concurrency in Java until, from being a weakness, it becomes one of my strongest points.

Study. Ok, but from What?

One can learn a lot about concurrency in Java only by reading the documentation of concurrency related Java classes like java.lang.Thread. However, to get a broader picture and a detailed explanation I was looking for a good book to learn from. After a little bit of googling, I’ve noticed that most of the sources point to a single book: Java Concurrency in Practice by Brian Goetz [JCiP].

Java Concurrency in Practice book cover
Java Concurrency in Practice by Brian Goetz

After reading the book I can say that it wasn’t a coincidence. It’s a wonderful book: it’s deep but also written in a way that is easy to understand, and most common problems and their solutions are illustrated with code snippets. To put it simply: it’s a must read for every Java developer.

Study Notes

The best way of acquiring new knowledge is not just to read books, but also to take notes. By doing so our brain is “forced” to process the new information once more. This is one of the reasons why I did other books on this as well – check posts in the Bookshelf Category. Another reason is that I hope others can benefit from my notes too. If you don’t have the time to read the full book, or you are not sure if it worths the time: just check my notes. In this case, I decided to go chapter-by-chapter because there is so much to process. I’ll present my notes from each chapter on a weekly basis. It won’t take more than 5-10 minutes to read them and at the end, I’ll share some extra content too. Without any further due, here comes the first chapter.

Chapter 1: Introduction

Writing concurrent programs is hard, to maintain them is arguably even harder: So, why bother with concurrency in the first place? In a nutshell: it is the easiest way to tap the computing power of multiprocessor systems, and often it is easier to write a complicated asynchronous program by writing multiple pieces of code that run concurrently but each doing only a single well-defined task.

The main motivating factors behind the development of operating systems that allowed multiple programs to execute simultaneously were:

  • Resource utilization – programs sometimes have to wait for external events that are out of they control. For example, an I/O operation to finish. While waiting for other programs might do some useful work.
  • Fairness – multiple users and programs may have equal claims on the computer’s resources. In a multi-user or multi-process environment, it’s more desirable for each program to get a chance to do some work than to wait for one program to run to finish and then start another.
  • Convenience – “It is often easier or more desirable to write several programs that each perform a single task and have them coordinate with each other as necessary than to write a single program that performs all the tasks” [JCiP].

Individual programs run in isolated processes: resources such as memory, file handles, and security credentials are allocated by the operating system separately. If they needed to, processes could communicate through means of inter-process communication: sockets, signal-handlers, shared memory, and files. “The same concerns (resource utilization, fairness, and convenience) that motivated the development of processes also motivated the development of threads. Threads allow multiple streams of program control flow to coexist within a process. They share process-wide resources such as memory and file handles, but each thread has its own program counter, stack, and local variables.” [JCiP]

In most modern operating system the basic unit of scheduling is the thread – not the process. All threads within a process have access to the heap that allows fine grained inter-process communication. However, uncoordinated access from multiple threads can leave shared data in an inconsistent state, resulting in undefined program behavior.

Java’s built-in support for threads is a double-edged sword. On one hand, it simplifies the development of concurrent applications. On the other hand, developers need to be aware of thread-safety issues. “Thread safety can be unexpectedly subtle because, in the absence of sufficient synchronization, the ordering of operations in multiple threads is unpredictable and sometimes surprising” [JCiP]. Fortunately, Java provides a number of synchronization mechanisms to coordinate shared access. But, in the absence of such synchronization, “the compiler, hardware, and runtime are allowed to take substantial liberties with the timing and ordering of actions, such as caching variables in registers or processor-local caches where they are temporarily (or even permanently) invisible to other threads” [JCiP].

When writing concurrent applications one must never compromise on safety, we must ensure that “nothing bad ever happens”. Although, that is desirable we also want to make sure that “something good eventually happens”, meaning that the program should not get into a state where it is permanently unable to make progress. On top of that, we often want “good things to happen quickly”. It really would be a waste of effort to rewrite an application to use multiple threads and end up with a program with a worse performance than the single-threaded version.

Threads are really everywhere. “When the JVM starts, it creates threads for JVM housekeeping tasks (garbage collection, finalization) and the main thread for running the main method” [JCiP]. Sometimes concurrency is introduced by using frameworks. The developer writes the business logic which seems to be a simple sequential order of steps (see. convenience as a reason for concurrency) but when it is plugged into the framework it might execute in parallel with other tasks, thus requiring that all code paths accessing shared state be thread-safe.