Software Development

Stop Using Synchronized Blocks in Java

Thread safety in Java doesn’t have to be painful

Stop Using Synchronized Blocks in Java
Concurrency in Java - AI generated image

Have you ever tried to synchronise threads together?

You might assume chaos, but Java’s concurrency tools are surprisingly good at keeping things in order.

In this post, we’ll walk through practical examples to show how easy and safe concurrent programming can be when you use the right tools.

Why Threads Collide (and Why You Should Care)

Before we look at Java's tools, let's understand what a Thread is.

A Thread is an independent flow of execution that can run at the same time as other threads.

Here's a diagram showing the main thread executing (the main program) with two other threads running concurrently:

Blog Image

Each thread has a call stack, local variables, but they share the same heap memory.

They need access to the same heap memory so threads can communicate with one another via shared data structures.

We'll take a look at two data structures that Java provides out of the box to make working with concurrency easy.

Making Shared Data Safe with ConcurrentHashMap

ConcurrentHashMap is a Java HashMap extended with thread-safe methods.

Here's an example of how it works:

import java.util.concurrent.ConcurrentHashMap;

public class ConcurrentMapExample {
    public static void main(String[] args) {
        ConcurrentHashMap<String, Integer> userScores = new ConcurrentHashMap<>();

        userScores.putIfAbsent("Daniel", 10);
        userScores.putIfAbsent("John", 15);

        userScores.computeIfPresent("Daniel", (key, val) -> val + 5);

        System.out.println("Daniel's updated score: " + userScores.get("Daniel"));
    }
}

A key observation is that we use the method putIfAbsent instead of just put when adding Daniel and John to the Hashmap.

This is necessary in a concurrent environment because if two threads run put simultaneously, you don't know which order the put will happen in, and this can lead to inconsistent values.

For example, if in another thread you run userScores.put("Daniel", 1), the final value printed may be 6 instead of 15 which is inconsistent with the code you see here.

Thread-Safe Counters With Atomic Classes

Avoid using normal variables as counters in concurrent environments.

For instance, doing counter++ is not thread-safe if counter is a shared variable.

Different threads can access the counter at the same time, leading to threads overwriting values and creating inconsistent results.

You may think of using synchronized blocks, but they can be inefficient and make your code more nested and harder to read.

Instead, Java provides an Atomic class from java.util.concurrent to help you create thread-safe counters as follows:

import java.util.concurrent.atomic.AtomicInteger;

public class AtomicCounterExample {
    public static void main(String[] args) {
        AtomicInteger counter = new AtomicInteger(0);

        // Increment the counter atomically
        int newValue = counter.incrementAndGet();

        System.out.println("Current count: " + newValue);
    }
}

This is a great package, and you can define atomic representations of most primitive data structures such as booleans, long, and strings.

Real-World Example: Handling Simultaneous Updates

Here is a partial code snippet for a distributed storage system remove function:

import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.Map;

public class RemoveExample {
    // Thread-safe shared file index
    private static final Map<String, IndexData> index = new ConcurrentHashMap<>();

    public static void removeFile(String filename) throws Exception {
        // Atomically update the entry only if it exists and is in a valid state
        IndexData data = index.computeIfPresent(filename, (key, value) -> {
            if (value.getStatus() == Protocol.STORE_COMPLETE) {
                value.setStatus(Protocol.REMOVE_IN_PROGRESS);
                value.setLatch(new CountDownLatch(3)); // example: waiting for 3 acks
                return value;
            } else {
                throw new RuntimeException("File not removable");
            }
        });

        if (data == null) {
            throw new RuntimeException("File not found");
        }

        // Simulate sending REMOVE requests to dstores...
    

        // Wait for all data stores (3 data stores) to acknowledge before removal
        // using a CountdownLatch from "java.util.concurrent"
        boolean success = data.getLatch().await(5000, TimeUnit.MILLISECONDS);
        if (!success) {
            throw new Exception("Timeout while removing");
        }

        // Safely remove the file entry
        index.remove(filename);
    }
}

TL;DR:

  • The server maintains an index of all files stored, mapped to relevant data about this file.
  • We use the computeIfPresent method from the ConcurrentHashMap structure to make sure the file actually exists and is stored before removing it.
  • We send requests to each data store that holds copies of this filename and expect 3 acknowledgements back to indicate the file is successfully removed.
  • Once all acks have been received, remove the file from the index.

CONCLUSION

Java gives you the tools to write safe, concurrent code without reinventing the wheel. Structures like ConcurrentHashMap and AtomicInteger make it easy to manage shared state across threads.

Understanding how these tools work will help you write code that is efficient and reliable. Give them a try and take some of the pain out of multithreading.

REFERENCES

1