Java Synchronized: What Is Thread Synchronization In Java

This article explains thread synchronization in Java along with related concepts like Java lock, race condition, mutex, Java volatile & deadlock.

In a multithreading environment where multiple threads are involved, there are bound to be clashes when more than one thread tries to get the same resource at the same time. These clashes result in “race conditions” and thus the program produces unexpected results.

For example, consider a single file being updated by two threads. If thread T1 is in the process of updating a variable in this file, and while this update is still in progress, thread T2 also updates the same variable, the variable will give incorrect results.

Thread Synchronization in Java

When multiple threads are involved, we should manage these threads in such a way that a resource can be accessed by only one thread at a time. In the above example, the file accessed by both threads should be managed so that T2 cannot access it until T1 is done accessing it. This is achieved in Java using “Thread Synchronization”.

What You Will Learn:

  • Thread Synchronization In Java
  • Race Condition In Java
  • Locks/Monitors In Java
  • Mutexes In Java
  • Synchronized Keyword
  • Types of Synchronization
  • Multi-threading Without Synchronization
  • Synchronized Code Block
  • Multi-threading With Synchronization
  • Synchronized Method
  • Volatile In Java
  • Deadlock In Java
  • Frequently Asked Questions

Thread Synchronization In Java

Since Java is a multi-threaded language, thread synchronization is very important as multiple threads execute in parallel in an application.

We use the keywords “synchronized” and “volatile” to achieve synchronization in Java. Synchronization is needed when the shared object or resource is mutable. If the resource is immutable, threads will only read it concurrently or individually, not needing synchronization.

In synchronized code, JVM ensures only one thread executes it at a time. Concurrent access to shared resources often introduces errors like “memory inconsistency” and “thread interference”. Synchronization makes access to shared resources mutually exclusive, avoiding these errors.

We use monitors to implement synchronization. A monitor can be accessed by only one thread at a time. When a thread gets the lock, we say it has entered the monitor.

When a monitor is accessed by a thread, it is locked and other threads trying to enter it are suspended until the thread inside finishes and releases the lock.

Race Condition In Java

In a multithreaded environment, when multiple threads try to access a shared resource to write simultaneously, they race to finish accessing it first. This gives rise to a ‘race condition’.

There is no problem if multiple threads access a shared resource concurrently for reading only. The problem arises when they access the same resource concurrently for writing.

Race conditions happen due to improper synchronization. Synchronizing threads properly ensures only one thread accesses the resource at a time, eliminating race conditions.

The best way to detect race conditions is thorough code review. As developers, we must review code to check for potential race conditions.

Locks/Monitors In Java

We mentioned earlier we use monitors or locks to implement synchronization. The monitor/lock is an internal entity associated with every object. So threads needing to access an object must first acquire its lock, work on it, and release the lock.

public class Lock {

  private boolean isLocked = false;

  public synchronized void lock() throws InterruptedException {
    while(isLocked) {
      wait();
    }

    isLocked = true;
  }

  public synchronized void unlock(){
    isLocked = false;
    notify();
  }

}

The lock() method locks the instance. Threads calling it will block until unlock() sets isLocked to false and notifies waiting threads.

Remember:

  • Each Java object has a lock/monitor accessible by threads
  • Only one thread can acquire the monitor at a time
  • synchronized allows synchronizing threads by making blocks/methods synchronized
  • Shared resources threads need to access are kept in synchronized blocks/methods

Mutexes In Java

In multithreaded environments, race conditions happen when multiple threads access shared resources simultaneously, causing unexpected output.

The code trying to access the shared resource is the critical section. Synchronizing access to critical sections avoids race conditions by ensuring only one thread accesses them at a time.

The simplest synchronizer is a “mutex”. A mutex ensures only one thread executes the critical section at a time.

A mutex is similar to monitors/locks we discussed earlier. To access the critical section, a thread must acquire the mutex. After accessing the critical section, it releases the mutex. Waiting threads are blocked until the mutex is released allowing another thread to enter.

There are several ways to implement a mutex in Java:

  • Using Synchronized Keyword
  • Using Semaphore
  • Using ReentrantLock

We will discuss only the synchronized keyword approach here. Semaphore and ReentrantLock are discussed later when covering the Java concurrency package.

Synchronized Keyword

Java provides the synchronized keyword to mark critical sections. The critical section can be a block of code or an entire method. Thus, only one thread can access critical sections marked synchronized at a time.

We can write the concurrent parts of an application using synchronized. It also eliminates race conditions by making blocks/methods synchronized.

Marking blocks/methods as synchronized protects shared resources inside from simultaneous access and corruption.

Types of Synchronization

There are 2 types of synchronization:

Process Synchronization: Multiple processes/threads executing reach a state where they commit to a specific sequence of actions.

Thread Synchronization: Multiple threads trying to access a shared space are synchronized so only one accesses it at a time.

We are concerned with thread synchronization in multithreading. We have seen the synchronized keyword approach here.

In Java, synchronized can be used with:

  • A block of code
  • A method

These are mutually exclusive types of thread synchronization using mutual exclusion to prevent interference between threads accessing shared data.

The other type of thread synchronization is “inter-thread communication” based on cooperation between threads. This is out of this article’s scope.

Before synchronizing blocks/methods, let‘s see thread behavior without synchronization.

Multi-threading Without Synchronization

This Java program has unsynchronized multiple threads:

// Print counter  
void printCounter() {

  for(int i = 5; i > 0; i--) {
    System.out.println("Counter => "+ i); 
  }

}

// Thread class
class ThreadCounter extends Thread {

  // printCounter reference  
  PrintCount pd;  

  // run method  
  public void run() {
    pd.printCounter();
  } 

}

public class Main {

  public static void main(String args[]) {

    // Shared PrintCount instance
    PrintCount pd = new PrintCount();

    // Create threads
    ThreadCounter t1 = new ThreadCounter(pd);  
    ThreadCounter t2 = new ThreadCounter(pd);

    // Start threads
    t1.start();
    t2.start();

  }

}

Output:

Counter => 5
Counter => 4 
Counter => 3
Counter => 2
Counter => 1
Counter => 5
Counter => 4
Counter => 3 
Counter => 2
Counter => 1

The output is inconsistent. Both threads start and display the counter one after the other.

The first thread should‘ve displayed the counter then the second thread should‘ve begun. Let‘s synchronize now and begin with code block synchronization.

Synchronized Code Block

A synchronized block synchronizes a block of code, usually a few lines. It is used when we don‘t want an entire method synchronized.

For example, consider a 75 line method, only 10 lines of which need to be executed by one thread at a time. Synchronizing the entire method would burden the system unnecessarily. So we use synchronized blocks.

The scope of a synchronized block is smaller than a synchronized method. A synchronized method locks the object to be used by multiple threads.

The syntax of a synchronized block:

synchronized (lock_object){
  // Synchronized code 
}

Here lock_object is the object reference threads need to obtain the lock on before executing synchronized code inside the block.

The synchronized keyword ensures only one thread can acquire the lock at a time, making others wait until the thread holding the lock finishes and releases it.

A NullPointerException is thrown if null is used as the lock_object.

If a thread sleeps while still holding the lock, the lock is not released. Other threads cannot access the shared object during this sleep time.

Now let‘s modify the earlier unsynchronized program to use a synchronized block and compare outputs.

Multi-threading With Synchronization

Here is the program using a synchronized block. In the run() method, we synchronize the code printing each thread‘s counter:

// Print counter
void printCounter() {

  for(int i = 5; i > 0; i--) {
    System.out.println("Counter => "+ i );
  }

}

// Thread class  
class ThreadCounter extends Thread {

  // PrintCount reference
  PrintCount pd; 

  // run() now synchronized 
  public void run() {

    synchronized(pd) {
      pd.printCounter(); 
    }

  }

}  

public class Main {

  public static void main(String args[]) {

    // Shared PrintCount 
    PrintCount pd = new PrintCount();

    // Threads
    ThreadCounter t1 = new ThreadCounter(pd);
    ThreadCounter t2 = new ThreadCounter(pd);  

    // Start threads
    t1.start();
    t2.start();

  }

}

Output:

Counter => 5
Counter => 4
Counter => 3 
Counter => 2
Counter => 1
Counter => 5
Counter => 4
Counter => 3
Counter => 2
Counter => 1

The output using synchronized block is consistent. As expected, the first thread finishes displaying the counter then the second thread displays it.

Synchronized Method

Earlier we saw declaring small blocks synchronized. To synchronize an entire method, we declare the method itself synchronized.

When a method is synchronized, only one thread can make the method call at a time.

The syntax for a synchronized method:

<access_modifier> synchronized method_name(parameters) {
  // Method body
} 

Like synchronized blocks, synchronized methods also need a lock object used by threads calling the method.

For static synchronized methods the lock object is the .class object. For non-static methods, it is the this object.

Synchronized is re-entrant, meaning a synchronized method can call another synchronized method with the same lock. So a thread holding the lock can call another synchronized method without needing to acquire a separate lock.

Below is an example demonstrating Synchronized Method:

class NumberClass {

  // Synchronized method 
  synchronized void printSquares(int n) throws InterruptedException {

    for(int i = 1; i <= n; i++) {
      System.out.println(i*i);  
      Thread.sleep(500);  
    }

  }

}

public class Main {

  public static void main(String args[]) {

    NumberClass number = new NumberClass();

    Thread t1 = new Thread() {
      public void run() {
        number.printSquares(10);
      }
    };

    Thread t2 = new Thread() {
      public void run() {
        number.printSquares(10);
      }  
    };

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

  }

}

Only one thread prints the squares at a time due to the synchronized method.

When synchronizing static methods the lock object is the class, not the object. This static synchronization example demonstrates it:

class Table {

   synchronized static void printTable(int n) {

     // prints table 

   }

}

class Thread1 extends Thread {
  public void run() {
    Table.printTable(5); 
  }
}

class Thread2 extends Thread {   
  public void run() {
    Table.printTable(9);
  }  
}

public class Main {

  public static void main(String args[]) {

    Thread1 t1 = new Thread1(); 
    Thread2 t2 = new Thread2();

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

  }

}

To summarize, synchronized in Java:

  • Guarantees mutually exclusive shared resource access using locking
  • Prevents race conditions
  • Prevents concurrent programming errors
  • Requires threads get an exclusive lock to enter synchronized blocks/methods
  • After executing, the thread flushes write operations eliminating memory inconsistencies

Volatile In Java

volatile is used to make classes thread-safe and modify variables from different threads. Both primitive and object types can be declared volatile.

When a variable is volatile, its value is never cached and always read from main memory. volatile guarantees ordering and visibility. Although variables can be declared volatile, classes and methods cannot.

class ABC {

  static volatile int myvar = 10;

}

Here variable myvar is static and volatile. A static variable is shared among all class objects. The volatile variable always resides in main memory, never cached. Hence there is only one copy of myvar in main memory and all read/write actions occur on it. Without volatile, each thread would have a different copy leading to inconsistencies.

Some differences between volatile and synchronized:

Volatile Synchronized
For variables only For code blocks and methods
Cannot block waiting threads Can block waiting threads
Improves thread performance Degrades thread performance
Resides in main memory Doesn‘t reside in main memory
Synchronizes one variable Synchronizes all variables

Deadlock In Java

By synchronizing threads using synchronized we ensure they execute simultaneously avoiding inconsistencies. But sometimes they can no longer function simultaneously and instead wait endlessly for resources, known as deadlock.

Deadlock in Java

Deadlock occurs when:

  • Thread 1 waits for resource locked by thread 2
  • Thread 2 waits for resource locked by thread 1

Neither thread finishes execution until the held resource is released.

Below is an example of deadlocks in Java:

public class Main {

  public static void main(String[] args) {

    String resource1 = "User Repository"; 
    String resource2 = "Auth Token";

    // Thread 1 locks resource1 then waits 
    Thread thread1 = new Thread() {
      public void run() {
        // Lock resource 1
        synchronized (resource1) {

          // Wait for resource 2
          synchronized (resource2) {

          }

        }
      }  
    };

    // Thread 2 locks resource2 then waits for resource1  
    Thread thread2 = new Thread() {
      public void run() {

        // Lock resource 2
        synchronized (resource2) {

          // Wait for resource 1
          synchronized (resource1) {

          }

        }

      }
    };  

    thread1.start();
    thread2.start();

  }

} 

Both threads lock one resource each while waiting for the other resource, creating deadlock.

We cannot prevent deadlocks fully but can avoid them:

  • Avoid nested locks
  • Use Thread.join() with timeouts
  • Only lock necessary code

Since deadlocks break code flow, we must avoid them in programs.

Frequently Asked Questions

Q: What is synchronization and why is it important?

A: Synchronization controls shared resource access by threads. Without it threads can update/change resources simultaneously inconsistently. In multithreading we must synchronize resource access mutually exclusively for consistency.

Q: What is synchronization and non-synchronization in Java?

A: Synchronization means constructs are thread-safe, allowing only one thread access at a time. Non-synchronized constructs allow concurrent access by threads. StringBuilder is a popular non-synchronized Java class.

Q: Why is synchronization required?

A: For concurrent execution processes need shared resources. To prevent clashes synchronizing them ensures all threads access smoothly.

Q: How do you get a synchronized ArrayList?

A: Use Collections.synchronizedList() with ArrayList argument to convert to synchronized list.

Q: Is HashMap synchronized?

A: No, HashMap is not synchronized but Hashtable is.

Conclusion

We discussed thread synchronization in detail including the volatile keyword and deadlocks in Java. Synchronization has process and thread synchronization – we are more concerned with thread synchronization and saw the synchronized keyword approach here.

Deadlock is an endless waiting scenario for resources by threads. We saw its example in Java and methods to avoid it.

Read More Topics