Mastering Synchronization in Java Threads: A Comprehensive Guide Part 3

Table of Contents

In the realm of concurrent programming, synchronization plays a crucial role in ensuring thread safety and preventing race conditions. In Java, where multithreading is a fundamental feature, understanding synchronization mechanisms is essential for writing robust and efficient concurrent applications. In this comprehensive guide, we will delve into the intricacies of synchronization in Java threads, exploring its concepts, techniques, and best practices.

Synchronization in Java

Synchronization in Java is a critical concept for managing concurrent access to shared resources, preventing data inconsistency issues that can arise when multiple threads operate on the same data simultaneously.

“Synchronized”

The ‘synchronized’ keyword is the modifier applicable only to methods and blocks, but not to classes and variables. If multiple threads are trying to operate simultaneously, there may be a chance of a data inconsistency problem. To overcome this problem, we should use the “synchronized” keyword. If a method or block is declared as synchronized, then at any given time, only one thread is allowed to execute that method or block on the given object, resolving the data inconsistency problem.

The main advantage of the “synchronized” keyword is that we can resolve data inconsistency problems, but the main disadvantage is that it increases the waiting time of threads and creates performance problems. Hence, if there is no specific requirement, it is not recommended to use the “synchronized” keyword.

If a thread wants to execute a synchronized method on the given object, it first has to obtain the lock of that object. Once the thread has obtained the lock, it is allowed to execute any synchronized method on that object. Once the method execution is complete, the thread automatically releases the lock. Acquiring and releasing the lock are internally taken care of by the JVM, and the programmer is not responsible for this activity.

While a thread is executing a synchronized method on a given object, the remaining threads are not allowed to execute any synchronized methods simultaneously on the same object, but they are allowed to execute non-synchronized methods simultaneously.

Java
class X {
    synchronized m1() {
    }
    
    synchronized m2() {
    }
    
    m3() {
    }
}

If thread t1 comes to execute m1() on object x, then t1 only acquires the lock of object x and starts executing m1(). Now, if t2 comes to execute m1() or t3 comes to execute m2(), then in both situations, a waiting state will occur. However, if t4 comes to execute m3(), it will get a chance immediately.

The lock concept is implemented based on the object, not on the method. So, every Java object has two areas:

  1. Non-synchronized area: This area can be accessed by any number of threads simultaneously, for example, wherever the object’s state won’t be changed, like a read() operation.
  2. Synchronized area: This area can be accessed by only one thread at a time, for example, wherever we are performing update operations (Add/Remove/Delete/Replace), i.e., where the state of the object is changing.

Real-time Example:

Java
class ReservationSystem {
    CheckAvailability() {           // Non-synchronized 
        // Just read operation
    }
    
    synchronized bookTicket() {       //Synchronized 
        // Update
    }
}

Synchronized Demo

Java
class Display {
  public synchronized void wish(String name) { 
    for(int i=0; i<10; i++) {
      System.out.print("Good Morning:");
      try {
         Thread.sleep(2000);
      } catch(InterruptedException e) {
      }
      System.out.println(name);
    }
  }
}

class MyThread extends Thread {
 Display d;
 String name;
 MyThread(Display d, String name) {
   this.d = d;
   this.name =  name;
 }
 public void run() {
   d.wish(name);
 }
}

class SynchronizedDemo {
 public static void main(String[] args) {
   Display d = new Display();
   MyThread t1 = new MyThread(d, "Krushna");
   MyThread t2 = new MyThread(d, "Arjuna");
   t1.start();
   t2.start();
 }
}

Here, (d object).wish(“Krushna”) by t1 | .wish(“Arjuna”) by t2

In the SynchronizedDemo class, two threads t1 and t2 are created, both sharing the same Display object d. The wish() method of the Display class is synchronized, ensuring that only one thread can execute it at a time. This prevents data inconsistency issues.

Suppose,

If we do not declare the wish() method as synchronized, then both threads will be executed simultaneously, resulting in irregular output like below:

Java
Good Morning:Good Morning:Arjuna
Good Morning:Krushna
Good Morning:Arjuna
Good Morning:Krushna
Good Morning:Good Morning:Arjuna
Good Morning:Krushna
Good Morning:Arjuna
Good Morning:Krushna
Good Morning:Good Morning:Arjuna
Good Morning:Krushna
Good Morning:Arjuna
Good Morning:Krushna

But, if we declare wish() as synchronized, then at a time only one thread is allowed to execute wish() on the given Display object, ensuring regular output.

Java
Good Morning:Krushna
Good Morning:Krushna
Good Morning:Krushna
Good Morning:Krushna
Good Morning:Krushna
Good Morning:Krushna
Good Morning:Krushna
Good Morning:Krushna
Good Morning:Krushna
Good Morning:Krushna
Good Morning:Arjuna
Good Morning:Arjuna
Good Morning:Arjuna
Good Morning:Arjuna
Good Morning:Arjuna
Good Morning:Arjuna
Good Morning:Arjuna
Good Morning:Arjuna
Good Morning:Arjuna
Good Morning:Arjuna

As expected, each thread prints its name (“Krushna” for t1 and “Arjuna” for t2) ten times alternately, resulting in regular output due to the synchronization of the wish() method.

Case Study

Java
Display d1 = new Display();
Display d2 = new Display();

MyThread t1 = new MyThread(d1, "Krushna");
MyThread t2 = new MyThread(d2, "Arjuna");

t1.start();
t2.start();

In this case study, eventhough the wish() method is synchronized, we will get irregular output because threads are operating on different Java objects.

Conclusion: If multiple threads are operating on the same Java object, then synchronization is required. If multiple threads are operating on multiple Java objects, then synchronization is not required.

Every class in Java has a unique lock, which is known as the class-level lock. If a thread wants to execute a static synchronized method, it requires the class-level lock. Once the thread acquires the class-level lock, it is allowed to execute any static synchronized method of that class. Once the method execution completes, the thread automatically releases the lock.

While a thread is executing a static synchronized method, the remaining threads are not allowed to execute any static synchronized method of that class simultaneously. However, the remaining threads are allowed to execute the following methods simultaneously:

  • static m3() (normal static method)
  • synchronized m4() (normal synchronized instance method)
  • m5() (normal instance methods)
Java
class X {
    static synchronized m1() {
    }
    
    static synchronized m2() {
    }
    
    static m3() {
    }
    
    synchronized m4() {
    }
    
    m5() {
    }
}
Java
(object X).m1()

If thread t1 comes to execute m1(), then it acquires the class-level lock: t1 --> CL(X)

If t2 comes to execute m1(), it will go into a waiting state.

If t3 comes to execute m2(), it will go into a waiting state.

If t4 comes to execute m3(), it will execute it.

If t5 comes to execute m4(), it will execute it.

If t6 comes to execute m5(), it will execute it.

Synchronized Block

If only a few lines of code require synchronization, it is not recommended to declare the entire method as synchronized. Instead, we can enclose those few lines of code using a synchronized block.

The main advantage of a synchronized block over a synchronized method is that it reduces the waiting time of threads and improves the performance of the system.

We can declare synchronized blocks as follows:

To get the lock of the current object:

Java
synchronized(this) {
    // If a thread gets the lock of the current object, then only it is allowed to execute this area
}

To get the lock of a particular object ‘b’:

Java
synchronized(b) {
    // If a thread gets the lock of the particular object 'b', then only it is allowed to execute this area
}

To get the class-level lock:

Java
synchronized(Display.class) {
    // If a thread gets the class-level lock of the "Display" class, then only it is allowed to execute this area
}

Using synchronized blocks allows for finer-grained control over synchronization, focusing only on the critical sections of code that require it, thus improving the overall performance of the system.

Example

Java
class Display {
    public void wish(String name) {
        ;;;;;;;;;;;;;;;;;;;;;;;;;;;; // 1 lakh lines of code

        synchronized (this) {
            for (int i = 0; i < 10; i++) {
                System.out.println("Good Morning:");
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                }
                System.out.println(name);
            }
        }
        ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; // 1 lakh lines of code 
    }
}

class MyThread extends Thread {
    Display d;
    String name;

    MyThread(Display d, String name) {
        this.d = d;
        this.name = name;
    }

    public void run() {
        d.wish(name);
    }
}

class SynchronizedDemo {
    public static void main(String[] args) {
        Display d = new Display();
        MyThread t1 = new MyThread(d, "Krushna");
        MyThread t2 = new MyThread(d, "Arjuna");
        t1.start();
        t2.start();
    }
}

Let’s see code section wise.

In the Display class, the wish() method is defined with extensive code execution before and after the synchronized block. Inside the synchronized block, a loop prints “Good Morning” along with the provided name, ensuring thread-safe execution of this critical section of code.

In the MyThread class, threads are created to execute the wish() method of the Display object with provided names.

In the SynchronizedDemo class, Display object d is created, and two threads (t1 and t2) are instantiated to execute the wish() method on d with different names concurrently.

Lock Concept

Lock concept is applicable for object type and class type, but not for primitives. Hence, we can’t pass a primitive type as an argument to a synchronized block; otherwise, we will get a compilation error: “unexpected type found: int required: reference.”

Can a thread acquire multiple locks simultaneously?

Yes, of course, from different objects. For example:

Java
class X {
    public synchronized void m1() {
        // Here, the thread has the lock of the 'X' object

        Y y = new Y();

        synchronized (y) {
            // Here, the thread has locks of x and y

            Z z = new Z();
            synchronized (z) {
                // Here, the thread has locks of x, y, and z
            }
        }
    }
}

X x = new X();
x.m1();

In this example, while executing method m1() of object x of class X, the thread acquires locks from objects of classes X, Y, and Z simultaneously.

Synchronized Statement

A synchronized statement refers to the set of statements enclosed within a synchronized method or synchronized block. These statements are termed synchronized statements because they are executed under the protection of synchronization, ensuring thread-safe access to critical sections of code.

In a synchronized method, the entire method body is considered a synchronized statement, as all statements within it are executed atomically, allowing only one thread to execute the method at any given time.

Similarly, in a synchronized block, the statements enclosed within the block are synchronized statements. These statements are executed under the lock associated with the specified object or class, ensuring mutual exclusion among threads attempting to access the synchronized block concurrently.

Overall, synchronized statements play a crucial role in achieving thread safety by ensuring that critical sections of code are executed in a mutually exclusive manner, thereby preventing data races and maintaining program correctness.

Part 4: Mastering Inter-Thread Communication in Java

Conclusion

Synchronization is a fundamental aspect of Java concurrency, enabling safe and efficient coordination among multiple threads. By mastering synchronization techniques and understanding the underlying principles, developers can build robust and scalable concurrent applications. With this comprehensive guide, you’re equipped to navigate the complexities of synchronization in Java threads and harness the power of concurrent programming effectively.

Skill Up: Software & AI Updates!

Receive our latest insights and updates directly to your inbox

Related Posts

error: Content is protected !!