Java Concurrency issues and Thread Synchronization

Welcome to the fifth part of my tutorial series on Java Concurrency. In earlier tutorials, We learned how to write concurrent code in Java. In this blog post, we’ll look at some common pitfalls related to concurrent/multithreaded programs, and learn how to avoid them.

Concurrency issues

Multithreading is a very powerful tool which enables us to better utilize the system’s resources, but we need to take special care while reading and writing data shared by multiple threads.

Two types of problems arise when multiple threads try to read and write shared data concurrently -

  1. Thread interference errors
  2. Memory consistency errors

Let’s understand these problems one by one.

Thread Interference Errors (Race Conditions)

Consider the following Counter class which contains an increment() method that increments the count by one, each time it is invoked -

class Counter {
    int count = 0;

    public void increment() {
        count = count + 1;
    }

    public int getCount() {
        return count;
    }
}

Now, Let’s assume that several threads try to increment the count by calling the increment() method simultaneously -

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class RaceConditionExample {

    public static void main(String[] args) throws InterruptedException {
        ExecutorService executorService = Executors.newFixedThreadPool(10);

        Counter counter = new Counter();

        for(int i = 0; i < 1000; i++) {
            executorService.submit(() -> counter.increment());
        }

        executorService.shutdown();
        executorService.awaitTermination(60, TimeUnit.SECONDS);
    
        System.out.println("Final count is : " + counter.getCount());
    }
}

What do you think the result of the above program will be? Will the final count be 1000 because we’re calling increment 1000 times?

Well, the answer is no! Just run the above program and see the output for yourself. Instead of producing the final count of 1000, it gives inconsistent result each time it is run. I ran the above program three times on my computer, and the output was 992, 996 and 993.

Let’s dig deeper into the program and understand why the program’s output is inconsistent -

When a thread executes the increment() method, following three steps are performed :

  1. Retrieve the current value of count
  2. Increment the retrieved value by 1
  3. Store the incremented value back in count

Now let’s assume that two threads - ThreadA and ThreadB, execute these operations in the following order -

  1. ThreadA : Retrieve count, initial value = 0
  2. ThreadB : Retrieve count, initial value = 0
  3. ThreadA : Increment retrieved value, result = 1
  4. ThreadB : Increment retrieved value, result = 1
  5. ThreadA : Store the incremented value, count is now 1
  6. ThreadB : Store the incremented value, count is now 1

Both the threads try to increment the count by one, but the final result is 1 instead of 2 because the operations executed by the threads interleave with each other. In the above case, the update done by ThreadA is lost.

The above order of execution is just one possibility. There can be many such orders in which these operations can execute making the program’s output inconsistent.

When multiple threads try to read and write a shared variable concurrently, and these read and write operations overlap in execution, then the final outcome depends on the order in which the reads and writes take place, which is unpredictable. This phenomenon is called Race condition.

The section of the code where a shared variable is accessed is called Critical Section.

Thread interference errors can be avoided by synchronizing access to shared variables. We’ll learn about synchronization in the next section.

Let’s first look at the second kind of error that occurs in multithreaded programs - Memory Consistency Errors.

Memory Consistency Errors

Memory inconsistency errors occur when different threads have inconsistent views of the same data. This happens when one thread updates some shared data, but this update is not propagated to other threads, and they end up using the old data.

Why does this happen? Well, there can be many reasons for this. The compiler does several optimizations to your program to improve performance. It might also reorder instructions in order to optimize performance. Processors also try to optimize things, for instance, a processor might read the current value of a variable from a temporary register (which contains the last read value of the variable), instead of main memory (which has the latest value of the variable).

Consider the following example which demonstrates Memory Consistency Error in action -

public class MemoryConsistencyErrorExample {
    private static boolean sayHello = false;

    public static void main(String[] args) throws InterruptedException {

        Thread thread = new Thread(() -> {
           while(!sayHello) {
           }

           System.out.println("Hello World!");

           while(sayHello) {
           }

           System.out.println("Good Bye!");
        });

        thread.start();

        Thread.sleep(1000);
        System.out.println("Say Hello..");
        sayHello = true;

        Thread.sleep(1000);
        System.out.println("Say Bye..");
        sayHello = false;
    }
}

In ideal scenario, the above program should -

  1. Wait for one second and then print Hello World! after sayHello becomes true.
  2. Wait for one more second and then print Good Bye! after sayHello becomes false.
# Ideal Output
Say Hello..
Hello World!
Say Bye..
Good Bye!

But do we get the desired output after running the above program? Well, If you run the program, you will see the following output -

# Actual Output
Say Hello..
Say Bye..

Also, the program doesn’t even terminate.

Wait. What? How is that possible?

Yes! That is what Memory Consistency Error is. The first thread is unaware of the changes done by the main thread to the sayHello variable.

You can use volatile keyword to avoid memory consistency errors. We’ll learn more about volatile Keyword shortly.

Synchronization

Thread interference and memory consistency errors can be avoided by ensuring the following two things-

  1. Only one thread can read and write a shared variable at a time. When one thread is accessing a shared variable, other threads should wait until the first thread is done. This guarantees that the access to a shared variable is Atomic, and multiple threads do not interfere.

  2. Whenever any thread modifies a shared variable, it automatically establishes a happens-before relationship with subsequent reads and writes of the shared variable by other threads. This guarantees that changes done by one thread are visible to others.

Luckily, Java has a synchronized keyword using which you can synchronize access to any shared resource, thereby avoiding both kinds of errors.

Synchronized Methods

Following is the Synchronized version of the Counter class. We use Java’s synchronized keyword on increment() method to prevent multiple threads from accessing it concurrently -

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

class SynchronizedCounter {
    private int count = 0;

    // Synchronized Method 
    public synchronized void increment() {
        count = count + 1;
    }

    public int getCount() {
        return count;
    }
}

public class SynchronizedMethodExample {
    public static void main(String[] args) throws InterruptedException {
        ExecutorService executorService = Executors.newFixedThreadPool(10);

        SynchronizedCounter synchronizedCounter = new SynchronizedCounter();

        for(int i = 0; i < 1000; i++) {
            executorService.submit(() -> synchronizedCounter.increment());
        }

        executorService.shutdown();
        executorService.awaitTermination(60, TimeUnit.SECONDS);

        System.out.println("Final count is : " + synchronizedCounter.getCount());
    }
}

If you run the above program, it will produce the desired output of 1000. No race conditions occur and the final output is always consistent. The synchronized keyword makes sure that only one thread can enter the increment() method at one time.

Note that the concept of Synchronization is always bound to an object. In the above case, multiple invocations of increment() method on the same instance of SynchonizedCounter leads to a race condition. And we’re guarding against that using the synchronized keyword. But threads can safely call increment() method on different instances of SynchronizedCounter at the same time, and that will not result in a race condition.

In case of static methods, synchronization is associated with the Class object.

Synchronized Blocks

Java internally uses a so-called intrinsic lock or monitor lock to manage thread synchronization. Every object has an intrinsic lock associated with it.

When a thread calls a synchronized method on an object, it automatically acquires the intrinsic lock for that object and releases it when the method exits. The lock release occurs even if the method throws an exception.

In case of static methods, the thread acquires the intrinsic lock for the Class object associated with the class, which is different from the intrinsic lock for any instance of the class.

synchronized keyword can also be used as a block statement, but unlike synchronized method, synchronized statements must specify the object that provides the intrinsic lock -

public void increment() {
    // Synchronized Block - 

    // Acquire Lock
    synchronized (this) { 
        count = count + 1;
    }   
    // Release Lock
}

When a thread acquires the intrinsic lock on an object, other threads must wait until the lock is released. However, the thread that currently owns the lock can acquire it multiple times without any problem.

The idea of allowing a thread to acquire the same lock more than once is called Reentrant Synchronization.

Volatile Keyword

Volatile keyword is used to avoid memory consistency errors in multithreaded programs. It tells the compiler to avoid doing any optimizations to the variable. If you mark a variable as volatile, the compiler won’t optimize or reorder instructions around that variable.

Also, The variable’s value will always be read from the main memory instead of temporary registers.

Following is the same MemoryConsistencyError example that we saw in the previous section, except that, this time we have marked sayHello variable with volatile keyword.

public class VolatileKeywordExample {
    private static volatile boolean sayHello = false;

    public static void main(String[] args) throws InterruptedException {

        Thread thread = new Thread(() -> {
           while(!sayHello) {
           }

           System.out.println("Hello World!");

           while(sayHello) {
           }

           System.out.println("Good Bye!");
        });

        thread.start();

        Thread.sleep(1000);
        System.out.println("Say Hello..");
        sayHello = true;

        Thread.sleep(1000);
        System.out.println("Say Bye..");
        sayHello = false;
    }
}

Running the above program produces the desired output -

# Output
Say Hello..
Hello World!
Say Bye..
Good Bye!

Conclusion

In this tutorial, we learned about different concurrency issues that might arise in multi-threaded programs and how to avoid them using synchronized methods and blocks. Synchronization is a powerful tool but please note that unnecessary synchronization can lead to other problems like deadlock and starvation.

You can find all the code snippets used in this tutorial in my github repository. In the next blog post, we’ll learn how to use lock objects and atomic variables to avoid concurrency issues.

Thank you for reading. Please ask any doubts or questions in the comment section below.