DEV Community

Cover image for 🛡️ What is a Race Condition in Java, and how it can be prevented using synchronized and AtomicInteger
Daniel Rendox
Daniel Rendox

Posted on

🛡️ What is a Race Condition in Java, and how it can be prevented using synchronized and AtomicInteger

Table of Contents

 1. Problem 1 — Main thread doesn't wait for other threads to finish
 2. Solution — future.get()
 3. Problem 2 — Multiple threads have access to the same variable at a time
 4. Solution — AtomicInteger
 5. Problem 3 — AtomicInteger may be not enough
 6. Solution — synchronized keyword
       Synched non-static method
       Another synched non-static method
       Simple unsynched method
       Static synched method
       Sounds overwhelming. How to understand it?

 7. AtomicInteger vs synchronized
 8. Conclusion
 9. Useful resources

Hello, everyone! First of all, thank you all for the amazing feedback and support you gave me on my previous post. Thanks @thepracticaldev for supporting aspiring writers. I'm so happy to be back with another article about how to synchronize threads!

Well, why should we synchronize them if the essence of multithreading is to perform operations asynchronously? Because with the benefits of multithreading come challenges. In this article, we’ll learn about problems that occur and ways to solve them.

Let’s consider this code:

public class DonutStorage {
    private int donutsNumber;
    public DonutStorage(int donutsNumber) {this.donutsNumber = donutsNumber;}
    public int getDonutsNumber() {return donutsNumber;}
    public void setDonutsNumber(int donutsNumber) {this.donutsNumber = donutsNumber;}
}

public class Consumer {
    private final DonutStorage donutStorage;
    public Consumer(DonutStorage donutStorage) {this.donutStorage = donutStorage;}

    /**
     * Subtracts the given number from the DonutStorage's donutsNumber.
     * @param numberOfItemsToConsume Number that will be subtracted from the donutsNumber
     */
    public void consume(int numberOfItemsToConsume) {
        donutStorage.setDonutsNumber(donutStorage.getDonutsNumber() - numberOfItemsToConsume);
    }
}

public class Main {
    public static void main(String[] args) {
        int consumersNumber = 10;
        DonutStorage donutStorage = new DonutStorage(20);
        ExecutorService executor = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());
        for (int i = 0; i < consumersNumber; i++) {
            executor.submit(() -> new Consumer(donutStorage).consume(1));
        }
        executor.shutdown();

        System.out.println("Number of remaining donuts: " + donutStorage.getDonutsNumber());
    }
}
Enter fullscreen mode Exit fullscreen mode

Here we have a simple program for a donut shop that counts the number of donuts. This shop probably has a server, which gets data from clients. As there are many clients who consume donuts, and the server must serve them simultaneously, it will have different threads each dedicated to one user.

To realize this in code, we create an ExecutorService with the number of threads equal to the available number of cores. Then we want it to serve the consumerNumber of clients, so there is a for loop that submits tasks to create a new Consumer that will consume items from the DonutStorage, and print the number of remaining donuts on the console.

Let’s assume that there are 10 consumers, each buys 1 donut, and we have 20 donuts in total. To prevent errors, if a user wants more donuts than there are in stock, the remaining donuts will be sold (the donutsNumber will be set to 0).

Problem 1

So we expect the number of remaining donuts to be 10, but I have the following printed (you may have a different result):

Number of remaining donuts: 19
Enter fullscreen mode Exit fullscreen mode

Question

Why is that so? This problem is not connected with race condition. So go ahead and make a guess! C’mon, you should know the answer from the previous article!

Answer

Because we didn’t call join so the main thread doesn’t wait for the others to finish.

Don’t get confused by executor.shutdown(). It doesn’t mean that the code below it is executed after the executor is shut down. Look, the main thread does 5 things:

  • creates some variables;
  • creates an executor;
  • submits tasks to the executor;
  • shuts down the executor;
  • prints the result;

These operations are done sequentially, but, when it tells the executor to shut down, it doesn’t actually wait until it happens. According to the documentation of shutdown(), when this method is called, ExecutorService

Initiates an orderly shutdown in which previously submitted tasks are executed, but no new tasks will be accepted.

However, the main thread isn’t blocked and continues its work.

In other words, in my situation, only 1 of the 10 users had managed to get a donut, when the main thread printed the number of remaining ones. As threads can be scheduled in other ways, rerunning the program, we may get a different result.

Solution

But how can we join the threads? With a simple Thread we would write myThread.join(). But how to do it with ExecutorService?

It’s not appropriate to use ExecutorService in this way. We should harness its Future power instead.
Even though, executor.submit(()->{}) submits a Runnable, the method still returns a Future that never contains any value inside but can tell when the task gets finished.

To get the desired functionality, we should keep these futures from each submission in a list and then loop through them to make sure all the tasks are finished:

public class Main {
    public static void main(String[] args) {
        int consumersNumber = 10;
        DonutStorage donutStorage = new DonutStorage(20);
        ExecutorService executor = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());
        List<Future<?>> futures = new ArrayList<>(consumersNumber);
        for (int i = 0; i < consumersNumber; i++) {
            futures.add(executor.submit(() -> new Consumer(donutStorage).consume(1)));
        }
        executor.shutdown();

        // make the main thread wait for others to finish
        for (Future<?> future: futures) {
            try {
                future.get();
            } catch (InterruptedException | ExecutionException e) {
                System.out.println("Exception while getting from future" + e.getMessage());
                e.printStackTrace();
            }
        }

        System.out.println("Number of remaining donuts: " + donutStorage.getDonutsNumber());
    }
}
Enter fullscreen mode Exit fullscreen mode

This way the main thread gets blocked (waits) until the other threads finish their work.

BTW, it’s wrong to assume that getting only the final future is enough because the tasks aren’t done sequentially.

You can compare the old and the updated versions here.

Problem 2 — Multiple threads have access to the same variable at a time

Change the main method and run the code. Output:

Number of remaining donuts: 10
Enter fullscreen mode Exit fullscreen mode

The problem is solved, right? Unfortunately, no. Try and run it again! Output:

Number of remaining donuts: 12
Enter fullscreen mode Exit fullscreen mode

Explanation

Let's examine what may have happened.

You see, multiple threads have access to the private int donutsNumber in the DonutStorage at the same time. To change the value of this variable they should:

  1. Get the variable
  2. Set the variable

Because of the thread interleaving we may get something like this:

The 5th thread reads the value of 15
The 6th thread reads the value of 15 (as the 5th user hasn’t yet changed the value)
The 5th thread changes the value to 14
The 6th thread changes the value to 14

Blank in this table means that the thread for whatever reason sits idle, which is completely normal.

So everything goes OK until the 5th and the 6th threads read the same value simultaneously and one item is automatically not counted. And it may not be the only case. This behavior is called a race condition.

Multithreaded programming: threory — each doggy eats from its bowl, actual — the doggies messed up with the bowls, each eats not from its bowl, the food is on the floor

Solution — AtomicInteger

To solve a synchronization problem we first should check if the solution is already written for us. In this case, there is one - AtomicInteger.

An atomic operation is an operation that is done at once. As we’ve seen, subtracting a number from a variable is not an atomic operation. Similarly, when you write i++, it seems to be atomic, but it’s not because the value is read and then written (so two operations not one).

There are classes like AtomicInteger, AtomicLong, AtomicBoolean, etc. They internally make these operations atomic and in this way solve the problem.

Here is the improved code:

public class DonutStorage {
    private final AtomicInteger donutsNumber;
    public DonutStorage(int donutsNumber) {
        this.donutsNumber = new AtomicInteger(donutsNumber);
    }
    public AtomicInteger getDonutsNumber() {return donutsNumber;}
}

public class Consumer {
    private final DonutStorage donutStorage;
    public Consumer(DonutStorage donutStorage) {
        this.donutStorage = donutStorage;
    }
    /**
     * Subtracts the given number from the DonutStorage's donutsNumber.
     * @param numberOfItemsToConsume Number that will be subtracted from the donutsNumber
     */
    public void consume(int numberOfItemsToConsume) {
        donutStorage.getDonutsNumber().addAndGet(-numberOfItemsToConsume);
    }
}
Enter fullscreen mode Exit fullscreen mode

Firstly, we change the donutsNumber’s type to AtomicInteger, modify the respective getter, and delete the setter because we no longer need it. Then, the Consumer’s consume method should be modified to work with AtomicInteger. Let’s use its addAndGetmethod, which atomically adds the given number to the current value or subtracts from it if the given param is negative.

Mind you, it wouldn’t work if we’d modified getters and setters for donutsNumber to return int instead of making the consume method work with AtomicInteger. That’s because different threads would’ve ended up using int and bypassing the atomic nature of AtomicInteger. So be careful with this and make sure that you are using it correctly. However, this problem will be explained in more detail in the following section.

You can compare the old and the updated versions here.

Problem 3 — AtomicInteger may be not enough

Now consumers are allowed to take as many items as they want, but the number of donuts is limited. Let’s improve the program to satisfy this requirement.

So we’ll add an if-statement to the consume method that checks if the given number is bigger than the number of donuts in stock. When so, the number of donuts in stock will be set to 0. But it’ll be also good to know how many donuts a consumer actually consumes so let’s make the consume method return this number. We’ll also change the lambda in the Main to print the results in the form of “Thread’s name consumed number of items”. To test how it works, let’s also make the consumers try to consume more than is available, say 3 items each. The changes may look like this:

futures.add(executor.submit(() -> {
    Consumer consumer = new Consumer(donutStorage);
    System.out.println(Thread.currentThread().getName() + " consumed " +
            consumer.consume(3)); // changed the number from 1 to 3
}));

/**
 * Subtracts the given number from the DonutStorage's donutsNumber. If the given number is bigger
 * than the number of donuts in stock, sets the donutsNumber to 0.
 * @param numberOfItemsToConsume Number that will be subtracted from the donutsNumber
 * @return the number of consumed items
 */
public int consume(int numberOfItemsToConsume) {
    AtomicInteger donutsNumber = donutStorage.getDonutsNumber();
    // if there aren't enough donuts in stock, consume as many as there are
    if (numberOfItemsToConsume > donutsNumber.get()) {
        int result = donutsNumber.get();
        donutsNumber.set(0);
        return result;
    }

    donutStorage.getDonutsNumber().addAndGet(-numberOfItemsToConsume);
    return numberOfItemsToConsume;
}
Enter fullscreen mode Exit fullscreen mode

You can compare the old and the updated versions here.

Running the code, we can get the desired result!

pool-1-thread-1 consumed 3
pool-1-thread-3 consumed 3
pool-1-thread-6 consumed 3
pool-1-thread-8 consumed 0
pool-1-thread-7 consumed 2
pool-1-thread-1 consumed 0
pool-1-thread-2 consumed 3
pool-1-thread-5 consumed 3
pool-1-thread-4 consumed 3
pool-1-thread-7 consumed 0
Number of remaining donuts: 0
Enter fullscreen mode Exit fullscreen mode

You see how threads interleave. The one which gets only 2 items doesn’t go after the others that get 3 items. It’s completely OK. What’s not OK is that if we add a simple action that takes some time into the consume method, for example, printing some random text it will break the program. Check this out:

public int consume(int numberOfItemsToConsume) {
    AtomicInteger donutsNumber = donutStorage.getDonutsNumber();
    // if there aren't enough donuts in stock, consume as many as there are
    if (numberOfItemsToConsume > donutsNumber.get()) {
        int result = donutsNumber.get();
        donutsNumber.set(0);
        return result;
    }

    // printing some random text breaks the program
    System.out.println("The UFO flew in and left this inscription here.");

    donutStorage.getDonutsNumber().addAndGet(-numberOfItemsToConsume);
    return numberOfItemsToConsume;
}
Enter fullscreen mode Exit fullscreen mode

Output:

The UFO flew in and left this inscription here.
The UFO flew in and left this inscription here.
The UFO flew in and left this inscription here.
The UFO flew in and left this inscription here.
The UFO flew in and left this inscription here.
The UFO flew in and left this inscription here.
The UFO flew in and left this inscription here.
The UFO flew in and left this inscription here.
pool-1-thread-4 consumed 3
pool-1-thread-4 consumed -4
pool-1-thread-4 consumed 0
pool-1-thread-6 consumed 3
pool-1-thread-5 consumed 3
pool-1-thread-7 consumed 3
pool-1-thread-3 consumed 3
pool-1-thread-2 consumed 3
pool-1-thread-1 consumed 3
pool-1-thread-8 consumed 3
Number of remaining donuts: 0
Enter fullscreen mode Exit fullscreen mode

Despite the resulting number still being 0, we have issues with the resulting value of consume method. Why is that so?

That’s because printing some text introduces a delay resulting in the interleaving of the threads in a way that causes another race condition. Add it doesn’t mean that we shouldn’t just write some random text and the program will work fine. On another person’s computer, it may return an unexpected output without the random text, because their computer may be slower/faster/less or more busy, etc.

The question is why the program breaks despite AtomicInteger. Another question is why the race condition doesn’t happen in the previous section’s code. And you are about to get the answers.

Explanation

That’s because AtomicInteger only provides atomic access to the value it holds and uses a non-blocking algorithm. And that means that multiple threads may access the value at the same time. Whereas, our program contains consume method that gets executed by multiple threads simultaneously.

Here is a possible scenario:

The 6th thread reads the value of 5 and gets false in the if-condition
The 7th thread reads the value of 5 and gets false in the if-condition either
The 6th thread atomically reads 5 and writes 2
The 7th thread atomically reads 2 and writes -1

You see that the 7th user may make the value negative because it has already passed the check and moved from the if-statement when it updates the value.

Solution — synchronized keyword

Luckily, the solution to this is quite simple — just use the synchronized keyword for the critical section. In our case, it’s the whole consume method. This will tell the JVM that only one thread at a time can execute the critical section.

Here is some info you need to know regarding this:

In Java, this keyword can be used with (both static and non-static) methods and code blocks:

public synchronized void m1(){}
public static synchronized void m2(){}
synchronized (this) {}
synchronized (MyClass.class) {}
Enter fullscreen mode Exit fullscreen mode

Synched non-static method

This means that when there is an Object, for instance, donutStorage that has the following method:

public synchronized void setDonutsNumber(int donutsNumber) {
  this.donutsNumber = donutsNumber;
}
Enter fullscreen mode Exit fullscreen mode

only 1 thread will be allowed to execute it (set the variable) at a time.

Another synched non-static method

And when there is another synchronized method in this class:

public synchronized int getDonutsNumber() {
  return donutsNumber;
}
Enter fullscreen mode Exit fullscreen mode

another thread will not be allowed to execute it, while the first one is executing setDonutsNumber(), even though this method does not depend on setDonutsNumber()

However, if another synchronized method is called within a synchronized method or code block it will not cause any issues and the thread executing it will freely run that method.

// the thread that is executing m1 will freely get access to m2
// and execute it, even though m2 is synchronized
public synchronized void m1() {
    m2();
}
public synchronized void m2() {}
Enter fullscreen mode Exit fullscreen mode

Simple unsynched method

However, if there is an unsynchronized method in this class, for example:

public void optimizeDatabase() {}
Enter fullscreen mode Exit fullscreen mode

multiple other threads will be allowed to execute it

Static synched method

In addition, if there is a static synchronized method in this class, for example:

public static synchronized double calculateAverageDonutWeight(Donut[] donuts) {}
Enter fullscreen mode Exit fullscreen mode

another thread will be allowed to execute it, even though the first one is executing setDonutsNumber because one of them is static and another one is non-static.

Similarly to non-static, if there is another static synchronized method*,* other threads will be restricted from executing it, while this one is being executed.

Sounds overwhelming. How to understand it?

Sounds overwhelming, but in fact, this is pretty easy to understand.

A synchronized method tells the thread executing it to acquire a lock, which doesn’t allow any other threads to pass if they don’t have the lock.

By default, there are only two locks associated with a class: class-level lock (for static methods), and object-level lock (for non-static methods).

Synchronized blocks require an explicit indication of what lock they use.

synchronized (this) {}                      // object-level lock
synchronized (MyClass.class) {}     // class-level lock
Enter fullscreen mode Exit fullscreen mode

There is a good visual explanation in this video. It is also a good idea to play around with code to understand the topic and put the necessary info into your long-term memory.

Updated code

Mind you, in this case, when we use synchronized, AtomicInteger is no longer needed. That’s because it’s only consume method that causes the race condition and no one else. Keeping AtomicInteger will only cause performance degradation. However, if you eventually decide to get multithreaded access to the DonutStorage in other parts of your code, you will need to use synchronized either (or find another solution).

Here’s the updated code:

// DonutStorage is the same as it was in the beginning
public class DonutStorage {
    private int donutsNumber;
    public DonutStorage(int donutsNumber) {this.donutsNumber = donutsNumber;}
    public int getDonutsNumber() {return donutsNumber;}
    public void setDonutsNumber(int donutsNumber) {this.donutsNumber = donutsNumber;}
}

public int consume(int numberOfItemsToConsume) {
    synchronized (donutStorage) {
        int donutsNumber = donutStorage.getDonutsNumber();
        // if there aren't enough donuts in stock, consume as many as there are
        if (numberOfItemsToConsume > donutsNumber) {
            donutStorage.setDonutsNumber(0);
            return donutsNumber;
        }
        donutStorage.setDonutsNumber(donutsNumber - numberOfItemsToConsume);
        return numberOfItemsToConsume;
    }
}
Enter fullscreen mode Exit fullscreen mode

You can compare the old and the updated versions here.

Also, note that making the consume method synchronized wouldn’t work. That’s because a thread entering it would have to acquire a lock for the Consumer object, while we want it to get a lock for the donutStorage.

There is no point in getting a lock for the Consumer object because each thread has its own Consumer object therefore each Consumer is used by only one thread. On the other hand, donutStorage is a single instance of the DonutStorage class throughout our code. Getting a lock for it before executing the consume method forces other threads to wait until the current one releases that lock.

Output:

pool-1-thread-3 consumed 0
pool-1-thread-7 consumed 3
pool-1-thread-5 consumed 3
pool-1-thread-8 consumed 3
pool-1-thread-1 consumed 3
pool-1-thread-2 consumed 3
pool-1-thread-4 consumed 2
pool-1-thread-6 consumed 3
pool-1-thread-8 consumed 0
pool-1-thread-3 consumed 0
Number of remaining donuts: 0
Enter fullscreen mode Exit fullscreen mode

You can also add that print-random-text command to make sure that it works properly.

AtomicInteger vs synchronized

Now you may be wondering why he told me about AtomicInteger if it’s easier just to use synchronized.

Well, atomic classes are faster as they don’t use blocking. Not using blocking is also better for preventing deadlocks, which will be brought up in the following article. Also, when you use an atomic class and want to create another thread-safe block of code, you will not need to synch it.

However, as you saw, atomic classes don’t fully prevent race conditions. So let’s break it down to understand when to use AtomicInteger over synchronized.

AtomicInteger variable is an equivalent of a simple variable with synchronized getters and setters:

This:

public class DonutStorage {
    private final AtomicInteger donutsNumber;
    public DonutStorage(int donutsNumber) {
        this.donutsNumber = new AtomicInteger(donutsNumber);
    }
    public AtomicInteger getDonutsNumber() {return donutsNumber;}
}
Enter fullscreen mode Exit fullscreen mode

Will give the same result as this:

public class DonutStorage {
    private int donutsNumber;
    public DonutStorage(int donutsNumber) {this.donutsNumber = donutsNumber;}
    public synchronized int getDonutsNumber() {return donutsNumber;}
    public synchronized void setDonutsNumber(int donutsNumber) {this.donutsNumber = donutsNumber;}
}
Enter fullscreen mode Exit fullscreen mode

The code in the “AtomicInteger may be not enough” section doesn’t work, whereas the code in “Solution — AtomicInteger" works fine. That’s because the latter’s consumer method contains only 1 operation (atomic subtraction from the value) whereas the former’s contains as many as 4 accesses to the value.

So, when you need to perform simple updates on the variable, AtomicInteger is best. But, if your synchronization scope is bigger than that use synchronized.

In addition, I wanted to emphasize that it’s recommended to first search for some ready and well-tested solutions for your problem.

There is also a good explanation of this on StackOverflow.

Conclusion

Sorry for loading you with that amount of information. Unfortunately, multithreading is not an easy topic. You could just learn to simply use synchronized and that’s it, but the real problems would come in practice. I tried to provide real examples, problems, solutions to them, and common gotchas.

To sum up, race conditions are common multithreading problems that sometimes may be not easy to spot. It’s important to synchronize threads if they have access to the shared resource. Of course, atomic classes and synchronized aren’t the only ways to synchronize threads. The solution to your specific problem depends on the complexity of your code and what actually you want to get. It’s always better to first search for a ready solution.

Thank you for reading this article! I’m excited to write a new one in a few days. It’s going to be about inter-thread communication, particularly explaining wait and notify methods, and probably SynchronousQueue. If you learned something from this tutorial, please provide some feedback. If you have any questions or corrections, I would love to hear from you in the comments.

Useful resources

Here are some useful resources that I learned from.

  1. The code from this article is available on GitHub.
  2. Synchronization | GeeksforGeeks (YouTube video) — a good visual representation of locks.
  3. Synchronization while using AtomicInteger | Stack Overflow — if you didn’t understand when to use AtomicInteger over synchronized.
  4. Synchronized Block for .class | Stack Overflow — if you didn’t understand what is synchronized (MyClass.class)

Top comments (0)