Synchronization in Multithreading Java

In this article, we will learn what is synchronization and it's usage in multithreading programming.
When two or more threads need access to a shared resource, they need some way to ensure that the resource will be used by only one thread at a time. The process by which this is achieved is called synchronization.
There are two types of problems arise when multiple threads try to read and write shared data concurrently -
  1. Thread interference errors
  2. Memory consistency errors
The tool needed to prevent these errors is synchronization.
Let’s understand these problems one by one.

1. Thread Interference

Consider a simple class called Counter
class Counter {
    private int c = 0;

    public void increment() {
        c++;
    }

    public void decrement() {
        c--;
    }

    public int value() {
        return c;
    }
}
Counter is designed so that each invocation of increment will add 1 to c, and each invocation of decrement will subtract 1 from c. However, if a Counter object is referenced from multiple threads, interference between threads may prevent this from happening as expected.
Interference happens when two operations, running in different threads, but acting on the same data, interleave. This means that the two operations consist of multiple steps, and the sequences of steps overlap.
Let's analysis above program suppose Thread A invokes increment at about the same time Thread B invokes decrement. If the initial value of c is 0, their interleaved actions might follow this sequence:
  • Thread A: Retrieve c.
  • Thread B: Retrieve c.
  • Thread A: Increment retrieved value; result is 1.
  • Thread B: Decrement retrieved value; result is -1.
  • Thread A: Store result in c; c is now 1.
  • Thread B: Store result in c; c is now -1. Thread A's result is lost, overwritten by Thread B. This particular interleaving is only one possibility. Under different circumstances, it might be Thread B's result that gets lost, or there could be no error at all. Because they are unpredictable, thread interference bugs can be difficult to detect and fix.
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.

Memory Consistency Errors

Memory consistency errors occur when different threads have inconsistent views of what should be 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.
The tool needed to prevent these Thread interference errors and Memory consistency errors is synchronization.

Synchronization

Let's discuss how to use synchronization to achieve how can only one thread access the shared resource.
We can synchronize our code in either of two ways. Both involve the use of the synchronized keyword.
  1. Using Synchronized Methods
  2. Using Synchronized Statement Or Block

Using Synchronized Methods

To make a method synchronized, simply add the synchronized keyword to its declaration:
public class SynchronizedCounter {
    private int c = 0;

    public synchronized void increment() {
        c++;
    }

    public synchronized void decrement() {
        c--;
    }

    public synchronized int value() {
        return c;
    }
}
If count is an instance of SynchronizedCounter, then making these methods synchronized has two effects:
  • First, it is not possible for two invocations of synchronized methods on the same object to interleave. When one thread is executing a synchronized method for an object, all other threads that invoke synchronized methods for the same object block (suspend execution) until the first thread is done with the object.
  • Second, when a synchronized method exits, it automatically establishes a happens-before relationship with any subsequent invocation of a synchronized method for the same object. This guarantees that changes to the state of the object are visible to all threads.
Let's understand in depth with one more simple example that does not use it—but should.
The following program has three simple classes CallMe, Caller, and SynchronizedMethodExample. This program is not synchronized yet and notice it's output.
package com.javaguides.javamultithreading.synchronization;

      // This program is not synchronized.
     class Callme {
          void  call(final String msg) {
               System.out.print("[" + msg);
               try {
                   Thread.sleep(1000);
               } catch (final InterruptedException e) {
                    System.out.println("Interrupted");
               }
              System.out.println("]");
         }   
    }

class Caller implements Runnable {
     protected String msg;
     protected Callme target;
     protected Thread t;

     public Caller(final Callme targ, final String s) {
         target = targ;
         msg = s;
         t = new Thread(this);
         t.start();
     }

     @Override
     public void run() {
         target.call(msg);
     }
}

public class SynchronizedMethodExample {
     public static void main(final String args[]) {
          final Callme target = new Callme();
          final Caller ob1 = new Caller(target, "Hello");
          final Caller ob2 = new Caller(target, "Synchronized");
          final Caller ob3 = new Caller(target, "World");
          // wait for threads to end
           try {
               ob1.t.join();
               ob2.t.join();
               ob3.t.join();
           } catch (final InterruptedException e) {
               System.out.println("Interrupted");
           }
     }
}
Output:
[Hello[Synchronized[World]
]
]
As you can see, by calling sleep( ), the call( ) method allows execution to switch to another thread. This results in the mixed-up output of the three message strings. In this program, nothing exists to stop all three threads from calling the same method, on the same object, at the same time. This is known as a race condition, because the three threads are racing each other to complete the method.
This example used sleep( ) to make the effects repeatable and obvious. In most situations, a race condition is more subtle and less predictable, because you can’t be sure when the context switch will occur. This can cause a program to run right one time and wrong the next.
To fix this problem in this program, we must serialize access to call( ). That is, we must restrict its access to only one thread at a time. To do this, you simply need to proceed call( )’s definition with the keyword synchronized.
Let's rewrite the synchronized program:
package com.javaguides.javamultithreading.synchronization;

      // This program is synchronized.
     class Callme {
     synchronized void  call(final String msg) {
          System.out.print("[" + msg);
          try {
               Thread.sleep(1000);
          } catch (final InterruptedException e) {
               System.out.println("Interrupted");
          }
          System.out.println("]");
     }
}

class Caller implements Runnable {
     protected String msg;
     protected Callme target;
     protected Thread t;

     public Caller(final Callme targ, final String s) {
         target = targ;
         msg = s;
         t = new Thread(this);
         t.start();
     }

     @Override
     public void run() {
         target.call(msg);
     }
}

public class SynchronizedMethodExample {
     public static void main(final String args[]) {
          final Callme target = new Callme();
          final Caller ob1 = new Caller(target, "Hello");
          final Caller ob2 = new Caller(target, "Synchronized");
          final Caller ob3 = new Caller(target, "World");
          // wait for threads to end
           try {
               ob1.t.join();
               ob2.t.join();
               ob3.t.join();
           } catch (final InterruptedException e) {
               System.out.println("Interrupted");
           }
     }
}
Output:
[Hello]
[Synchronized]
[World]

Using Synchronized Statement or Block

While creating synchronized methods within classes that you create is an easy and effective means of achieving synchronization, it will not work in all cases. 
To understand, consider that you want to synchronize access to objects of a class that was not designed for multithreaded access. That is, the class does not use synchronized methods. Further, this class was not created by you, but by a third party, and you do not have access to the source code. Thus, you can’t add synchronized to the appropriate methods within the class. How can access to an object of this class be synchronized? Fortunately, the solution to this problem is quite easy: You simply put calls to the methods defined by this class inside a synchronized block.
This is the general form of the synchronized statement:
synchronized(objRef) {
// statements to be synchronized
}
Here, objRef is a reference to the object being synchronized. A synchronized block ensures that a call to a synchronized method that is a member of objRef’s class occurs only after the current thread has successfully entered objRef’s monitor.
Here is an alternative version of the preceding example (using synchronized method ), using a synchronized block within the run( ) method:
package com.javaguides.javamultithreading.synchronization;

      // This program is not synchronized.
class Callme {
      void  call(final String msg) {
          System.out.print("[" + msg);
          try {
               Thread.sleep(1000);
          } catch (final InterruptedException e) {
               System.out.println("Interrupted");
          }
          System.out.println("]");
     }
}

class Caller implements Runnable {
     protected String msg;
     protected Callme target;
     protected Thread t;

     public Caller(final Callme targ, final String s) {
         target = targ;
         msg = s;
         t = new Thread(this);
         t.start();
     }

     @Override
      synchronized public void run() {
         target.call(msg);
     }
}

public class SynchronizedMethodExample {
     public static void main(final String args[]) {
          final Callme target = new Callme();
          final Caller ob1 = new Caller(target, "Hello");
          final Caller ob2 = new Caller(target, "Synchronized");
          final Caller ob3 = new Caller(target, "World");
          // wait for threads to end
           try {
               ob1.t.join();
               ob2.t.join();
               ob3.t.join();
           } catch (final InterruptedException e) {
               System.out.println("Interrupted");
           }
     }
}
Output:
[Hello]
[World]
[Synchronized]
Here, the call( ) method is not modified by synchronized. Instead, the synchronized statement is used inside Caller’s run() method. This causes the same correct output as the preceding example(using synchronized methods) because each thread waits for the prior one to finish before proceeding.

Free Spring Boot Tutorial | Full In-depth Course | Learn Spring Boot in 10 Hours


Watch this course on YouTube at Spring Boot Tutorial | Fee 10 Hours Full Course

Comments