Thread Synchronization in Java

Multi-thread applications are useful to execute more than one operations simultaneously. However, if more than one threads are accessing a shared resource, there is a high risk of data corruption or unexpected result. This article explains the reason for the unexpected results and the possible solution to avoid it.

Even though threads are considered to be executing simultaneously, actually they are executed one after another using time-sharing mechanism of the underlying operating system. (For more details visit to this link). Consider a sample code as shown below.
public class ConcurrencyProblem {
    static int[] array = {0};

    public static void main(String[] args) throws InterruptedException {
        Thread a = new Thread() {
            public void run() {
                for(int i = 1; i <= 1000; i++) {
                    increase();
                }
            }
        };

        Thread b = new Thread() {
            public void run() {
                for(int i = 1; i <= 1000; i++) {
                    decrease();
                }
            }
        };

        a.start();
        b.start();
        a.join();
        b.join();
        System.out.println(array[0]);
    }

    public static void increase() {
        array[0]++;
    }

    public static void decrease() {
        array[0]--;
    }
}
In this code thread A is increasing the value of first element of the array by one. At the same time thread B is decreasing the value of first element of the array by one. Both threads are executing these operations 1000 times. So ideally the final result should be 0 since thread A increases the value 1000 times and thread B decreases the value 1000 times. However, when you run this application, sometimes you may get an outputs other than 0. If you run this application again and again, you will get different outputs on each execution.

Now have a deep look on what happens in background.
Even though, array[0]++ or array[0]-- seems to be a simple command in Java, it has few steps to be accomplished in background.

Step 1:
Copy the value from the RAM to a register.

Step 2:
Increase/Decrease the value by one in the register.

Step 3:
Update the RAM by the increased value from the register.

To get a clear idea, let's execute just a single iteration of the above code.

Assume that thread A is executing now and the current value of the array[0] is equal to 0.
Step 1:
Thread A copies the value to a register P.


 Step 2:
Thread A increases the value by one.


Step 3:
Thread A updates the value in the RAM.


Step 4:
Assume that, the thread A's time is over or thread A is terminated, and thread B starts to execute.

Step 5:
Thread B copies the value to a register Q.


Step 6:
Thread B decreases the value by one.


Step 7:
Thread B updates the value in the RAM.


Now we have a happy ending; the final result is 0.

Wait... there is a special case. Let's execute the code once again.

Step 1:
Thread A copies the value to a register P.


Step 2:
Thread A increases the value by one.


Step 3:
Assume that, the thread A's time is over and thread B starts to execute. (Normally, the operating system can interrupt a thread at any time). Now thread A's current state is stored and it is moved to the thread queue.


Step 4:
Thread B starts to execute and copies the value to a register Q.


Step 5:
Thread B decreases the value by one.


Step 6:
Thread B updates the value in the RAM.


Step 7:
Now thread B is terminated or moved to thread queue, and thread A gets the chance.
Thread A has to continue from its last state, so it updates the RAM by the value of register P.


In this case, final result is 1 not 0. There is a possibility to get -1 as well, if thread B updates the RAM at last.

For the provided sample code, the final result depends on the number of interrupted executions. As you can see, the problem is due to the timeout of a thread before it completes its task. This kind of problems can happen if more than one threads are accessing a shared resource concurrently. In other words, if you have a multi-thread application where more than one threads modifying a shared resource, then your application may produce unexpected results. Here the shared resource can be any kind of resources like a variable, file or database. This problem can be avoided by declaring the operation as an atomic operation. Atomic operation is an operation, once it has been started, it cannot be interrupted by any other threads.

Synchronization is one of the technique to define atomic operations. The Java keyword synchronized can be applied either to a method or a synchronized block to declare an atomic operation.

The modified code using synchronized method is given below.
public class ConcurrencyProblem {
    static int[] array = {0};

    public static void main(String[] args) throws InterruptedException {
        Thread a = new Thread() {
            public void run() {
                for(int i = 1; i <= 1000; i++) {
                    increase();
                }
            }
        };

        Thread b = new Thread() {
            public void run() {
                for(int i = 1; i <= 1000; i++) {
                    decrease();
                }
            }
        };

        a.start();
        b.start();
        a.join();
        b.join();
        System.out.println(array[0]);
    }

    public synchronized static void increase() {
        array[0]++;
    }

    public synchronized static void decrease() {
        array[0]--;
    }
}
This time always the result will be 0. If a thread is currently executing a synchronized static method, any other threads cannot execute any of the static synchronized methods of that class. In this case, once thread A entered into the increase method, thread B cannot execute the decrease method. In other words, thread B has to wait until thread A completes an atomic increment.

If a thread is executing a synchronized instance method, other threads cannot execute any synchronized instance methods of that object until the current thread leaves from the synchronized method. However, still other threads have the freedom to execute any non-synchronized methods.

Even though, synchronization is an easy technique to avoid unexpected results there are two drawbacks in using improper synchronization.

Problem 1: Synchronization reduces the performance.
Practically, the performance of synchronized applications is lesser than non-synchronized applications. Therefore, use the synchronization only if there are any shared resources accessed in a multi-thread environment. For single thread applications, synchronization is just an extra speed break. That is the reason, for recommending ArrayList over Vector and StringBuilder over StringBuffer for single thread applications (All the methods in Vector and StringBuffer are synchronized methods).

Problem 2: Synchronization causes to deadlock.
Even in multi-thread environment, improper synchronization can cause to thread deadlock. Thread deadlock is a critical situation, where two threads are waiting for a shared resource but both of them cannot continue to execute. More details about thread deadlock will is covered in Thread Deadlock article.

When a method is synchronized, the complete scope of the method is synchronized. That means, until the thread leaves that method other threads have to wait. Suppose, there is a method with 50 lines of code where only a single line is used to access the shared resource. In this case, synchronizing the whole method would reduce the performance. As a solution, using synchronized block is recommended over synchronized methods.

A synchronized block needs to have a reference for an object to lock (Cannot use synchronized block with primitive data types. This is the reason for using an array with one element instead of a primitive datatype int in the sample code). If you need to lock the current parent object, use synchronized block with a reference to this.

Format:
synchronized(<object-reference>) {

}

Modified code using synchronized block:
public class ConcurrencyProblem {
    static int[] array = {0};

    public static void main(String[] args) throws InterruptedException {
        Thread a = new Thread() {
            public void run() {
                for (int i = 1; i <= 1000; i++) {
                    increase();
                }
            }
        };

        Thread b = new Thread() {
            public void run() {
                for (int i = 1; i <= 1000; i++) {
                    decrease();
                }
            }
        };

        a.start();
        b.start();
        a.join();
        b.join();
        System.out.println(array[0]);
    }

    public static void increase() {
        // Do something else
        synchronized (array) {
            array[0]++;
        }
        // Do something else
    }

    public static void decrease() {
        // Do something else
        synchronized (array) {
            array[0]--;
        }
        // Do something else
    }
}

To lock all the static members of a class, synchronized block can be used with the class name as shown below.
synchronized(ConcurrencyProblem.class) {
   // do something here
}

Tips:
  • Use synchronization to avoid unexpected results in a multi-thread environment, with a shared resource.
  • Try to use synchronized block over synchronized methods.
  • Use synchronization only if you know what you are doing. Do not lead the program to thread deadlock.
  • Do not use synchronization in a single thread application.
  • Do not use any thread-safe classes in a single thread applications. (Eg: Vector and StringBuffer)


Find the source codes at Git Hub.
Previous
Next Post »

Contact Form

Name

Email *

Message *