DEV Community

Hamza Belmellouki
Hamza Belmellouki

Posted on

Concurrency: Thread Safety In Java

What is Thread safety? To save you the time from looking up to Wikipedia here is the definition:

Thread safety is a computer programming concept applicable to multi-threaded code. Thread-safe code only manipulates shared data structures in a manner that ensures that all threads behave properly and fulfill their design specifications without unintended interaction.

What does this definition really mean? How can we correctly write thread-safe code? What are some of the constructs that Java offers to write such code?

Accessing shared data

Access to mutable shared data requires synchronization which can be implemented using one of these approaches:

  • Java’s built-in synchronized keyword

  • volatile fields to guarantee that any thread that reads a field will see the most recently written value

  • Explicit locks

  • Atomic variables

Producing Thread-safe code is all about managing access to shared mutable states. When mutable states are published or shared between threads, they need to be synchronized to avoid bugs like race conditions and memory consistency errors.

Race condition

Java Concurrency In Practice defines race condition as:

A race condition occurs when the correctness of a computation depends on the relative timing or interleaving of multiple threads by the runtime; in other words, when getting the right answer relies on lucky timing.

To understand what the definition means, let us look at an example:

public class CheckThenActRace { // Risky
    private Connection connection = null;

    public Connection getConnection() {
        if (connection == null)
            connection = new Connection();
        return connection;
    }
}
Enter fullscreen mode Exit fullscreen mode

CheckThenActRace has race conditions that can compromise its correctness. Say that threads A and B execute getConnection simultaneously. A sees that connection is null, and instantiates a new Connection. B also checks if connection is null. Whether connection is null at this point depends unpredictably on CPU scheduler timing. If connection is null when B examines it, the two callers to getConnection may receive two different results, even though getConnection is always supposed to return the same instance.

To resolve such an issue, the operation check-then-act needs to be atomic; meaning it should happen all at once. We can achieve atomicity by making getConnection synchronized:

public synchronized Connection getConnection() {
    if (connection == null)
        connection = new Connection();
    return connection;
}
Enter fullscreen mode Exit fullscreen mode

Now when Thread A calls getConnection; A acquires the intrinsic lock of the object and locks it until getConnection is completely executed, then A releases the lock for Thread B to acquire it.

Memory consistency errors

When reads and writes occur in different threads, issues like memory consistency errors could emerge. In Java, there is no guarantee that the reading thread will see a value written by another thread on a timely basis, or even at all.

To enable visibility of memory writes across threads, you need to rely on synchronization. For example:

class MemoryErrorDemo { // don't do this
    private static boolean flag;
    private static int number;

    private static class ReaderThread extends Thread {
        public void run() {
            while (!flag) {
                // do nothing
            }
            System.out.println(number);
        }
    }

    public static void main(String[] args) {
        new ReaderThread().start();
        number = 5;
        flag = true;
    }
}
Enter fullscreen mode Exit fullscreen mode

MemoryErrorDemo could loop forever because the value of flag might never become visible to the reader thread. Even more weirdly, MemoryErrorDemo could print zero instead of 5 because the write to flag might be made visible to the reader thread before the write to number, a phenomenon known as reordering.

We could use the built-in intrinsic lock or the volatile keyword, a weaker form of synchronization, to ensure that writes to a variable are propagated predictably to other threads. To make the program works as expected, we have to add the volatile keyword to the second and third line:

private volatile static boolean flag;
private volatile static int number;
Enter fullscreen mode Exit fullscreen mode

Atomicity

Atomicity is when a set of actions all execute as a single operation, in an indivisible manner. Thinking that a set of actions in a multi-threaded program will be executed in sequential order might lead to behavior incorrectness. That’s due to thread interference, which means threads may overlap if they execute several steps on the same data. For example:

class AtomicityDemo {

    private static long counter = 0;

    private static class CounterThread extends Thread {
        public void run() {
            for (int i = 0; i < 1000; i++) {
                increment();
            }
        }
    }

    private static void increment() {
        ++counter;
    }

    public static void main(String[] args) throws InterruptedException {
        CounterThread counterThread = new CounterThread();
        counterThread.start();
        for (int i = 0; i < 1000; i++) {
            increment();
        }
        counterThread.join();
        System.out.println("Final number should be: 2000, but it's :" + counter);
    }
}
Enter fullscreen mode Exit fullscreen mode

AtomicityDemo simulates two threads writing to a shared variable. You may get the correct result if you’re lucky but if you run the program multiple times you’ll get different incorrect values. That’s because the operation ++counter isn’t a one-time operation, instead, it is composed of three steps so thread interference happens between these action steps.

To solve this problem we’ll convert the multi-step increment operation into an atomic operation. Fortunately, the Java core API provides Thread-safe objects to perform the operation. In this case, we’ll leverage AtomicLong wich is in java.util.concurrent.atomic package. Java Concurrency In Practice states:

Where practical, use existing thread-safe objects, like AtomicLong, to manage your class’s state. It is simpler to reason about the possible states and state transitions for existing thread-safe objects than it is for arbitrary state variables, and this makes it easier to maintain and verify thread safety.

The resulting code would look like:

import java.util.concurrent.atomic.AtomicLong;

class AtomicityDemo {

    private static AtomicLong counter = new AtomicLong(0);

    private static class CounterThread extends Thread {
        public void run() {
            for (int i = 0; i < 1000; i++) {
                increment();
            }
        }
    }

    private static void increment() {
        counter.incrementAndGet();
    }

    public static void main(String[] args) throws InterruptedException {
        CounterThread counterThread = new CounterThread();
        counterThread.start();
        for (int i = 0; i < 1000; i++) {
            increment();
        }
        counterThread.join();
        System.out.println("Final number should be: 2000, but it's :" + counter.get());
    }
}
Enter fullscreen mode Exit fullscreen mode

Now the increment method atomically increments the current value. The two threads will not interleave when trying to increment counter variable because it happens all in one operation.

Locking

Locking is an interesting topic in concurrency. In Java, you could use either the object’s intrinsic lock or an explicit lock (implementation of the Lock interface). Both of these two locking strategies help us achieve thread-safety by synchronizing access to shared mutable data.

Object’s Intrinsic Lock

Every object has an intrinsic lock associated with it. Any thread that wants access to a shared object has to acquire the intrinsic lock first so no other thread can access that object, and when the thread finishes with the lock it releases it so other threads may acquire it.

When you declare that a method as synchronized, the thread calling that method acquires the intrinsic lock for that method’s object and releases it when the method exits. In CheckThenActRace (see above) we had used the object’s intrinsic lock to synchronize access to shared mutable objects and also we can leverage it to establish happens-before relationships that are essential to visibility.

Explicit Lock

Added in Java 5, the package java.util.concurrent.locks has multiple classes that implement the Lock interface. One of these classes is ReentrantLock which is a reentrant mutual exclusion Lock with the same basic behavior and semantics as the intrinsic lock accessed using synchronized methods and statements. Typical usage of this class:

Lock lock = new ReentrantLock();

lock.lock();
try {
    //do critical section code. May throw exception
} finally {
    lock.unlock();
}
Enter fullscreen mode Exit fullscreen mode

This trick makes sure that the Lock is unlocked in case an exception is thrown from the code in the critical section. if an exception occurred in the critical section and the unlock() wasn’t called from inside a finally block, the lock would remain locked forever, causing all threads trying to acquire the lock to hang indefinitely.

Conclusion

In this article, we saw what is thread-safety is all about, how to write thread-safe code. We also talked about common pitfalls that may occur when developing concurrent programs and how to deal with them.

If you want to know more about the subject, I highly recommend reading “Java Concurrency In Practice” by Brian Goetz.

In the next article, there will be more Core Java. Stay tuned!

Oldest comments (0)