Skip to main content

Thread Synchronization

When two or more threads attempt to access the same shared resource simultaneously, it can lead to erratic behavior, data inconsistency, and corrupted results. This is known as a Race Condition or Thread Interference.

To solve this problem, Java provides a mechanism called Synchronization. Synchronization ensures that only one thread can access the shared resource at a given time.


Why Use Synchronization?

  • To prevent thread interference.
  • To prevent consistency problems (memory consistency errors).

The synchronized Keyword

In Java, synchronization is achieved using the synchronized keyword. You can synchronize:

  1. A Method
  2. A Block of Code

Every object in Java has an intrinsic lock (also known as a monitor lock) associated with it. When a thread invokes a synchronized method or block, it must acquire the lock for that object. Other threads attempting to invoke synchronized code on the same object will block (enter the BLOCKED state) until the first thread releases the lock.


1. Synchronized Method

If you declare a method as synchronized, Java places a lock on the object on which the method is being invoked.

Example: Without Synchronization (Data Inconsistency)

class Table {

// Method is NOT synchronized
void printTable(int n) {
for (int i = 1; i <= 5; i++) {
System.out.println(n * i);
try {
Thread.sleep(400); // Pausing execution to increase chance of interference
} catch (Exception e) {
System.out.println(e);
}
}
}
}

class Thread1 extends Thread {

Table t;

Thread1(Table t) {
this.t = t;
}

public void run() {
t.printTable(5);
}
}

class Thread2 extends Thread {

Table t;

Thread2(Table t) {
this.t = t;
}

public void run() {
t.printTable(100);
}
}

public class LabSync1 {

public static void main(String args[]) {
Table obj = new Table(); // Shared resource

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

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

Output:

5
100
10
200
15
300
... (Outputs are mixed up randomly)

Example: With Synchronization

By simply adding the synchronized keyword to the method, we ensure thread safety.

class Table {

// Method IS synchronized
synchronized void printTable(int n) {
for (int i = 1; i <= 5; i++) {
System.out.println(n * i);
try {
Thread.sleep(400);
} catch (Exception e) {
System.out.println(e);
}
}
}
}

// ... Thread1, Thread2, and Main remain the same

Output:

5
10
15
20
25
100
200
300
400
500

[!NOTE] Now, t1 fully completes its task before t2 is allowed to execute printTable() on the shared obj.


2. Synchronized Block

Sometimes, you do not want to synchronize an entire method, as it can reduce performance (threads have to wait longer). Instead, you can synchronize just the specific block of code that accesses the shared resource.

class Table {

void printTable(int n) {
System.out.println("Non-synchronized code executing safely...");

// Synchronized Block locking on the current object 'this'
synchronized (this) {
for (int i = 1; i <= 5; i++) {
System.out.println(n * i);
try {
Thread.sleep(400);
} catch (Exception e) {
System.out.println(e);
}
}
}

System.out.println("Non-synchronized code finished executing.");
}
}

Static Synchronization (Class Level Lock)

If you declare a static method as synchronized, the lock is placed on the Class object (Table.class) rather than the instance object.

This is known as a Class Level Lock. It prevents multiple threads from executing the static synchronized method across all instances of the class simultaneously.

class Table {

// Class Level Lock
static synchronized void printTable(int n) {
for (int i = 1; i <= 5; i++) {
System.out.println(n * i);
try {
Thread.sleep(400);
} catch (Exception e) {
System.out.println(e);
}
}
}
}

[!WARNING] Deadlocks can occur when two or more threads are waiting on each other to release locks. Overuse of synchronization can cause deadlocks and severe performance degradation. Always keep synchronized blocks as small as possible!