EZ

Eduzan

Learning Hub

Eduzan
Eduzan / Java

Synchronization

In multithreaded applications, there are instances where multiple threads attempt to access shared resources simultaneously, which can lead to inconsistencies and unexpected results.

Why Use Synchronization in Java?

Java provides synchronization to ensure that only one thread can access a shared resource at any given time, thus avoiding conflicts.

Java Synchronized Blocks

Java offers a way to synchronize the tasks performed by multiple threads through synchronized blocks. A synchronized block is synchronized on a specific object, which acts as a lock. Only one thread can execute the code within the synchronized block while holding the lock, and other threads must wait until the lock is released.

General Form of Synchronized Block:

synchronized(lock_object) {
   // Code that needs synchronized access
}

Example:

import java.util.*;
public class Main {
    public static void main(String[] args) {
        System.out.println("Hello, World!");
    }
}

Output:

Hello, World!

This mechanism is implemented using monitors or locks in Java. A thread must acquire a lock on the object before entering the synchronized block, and it releases the lock once the block is exited.

Types of Synchronization:

1. Process Synchronization: Coordinates the execution of multiple processes to ensure shared resources are managed safely.
2. Thread Synchronization: Manages thread execution in multithreaded programs, with two main approaches:

  • Mutual Exclusion (Synchronized methods, synchronized blocks, static synchronization)
  • Cooperation (Inter-thread communication)

Example:

// Java program demonstrating synchronization

class Task {
    public void performTask(String message) {
        System.out.println("Executing\t" + message);
        try {
            Thread.sleep(800);
        } catch (InterruptedException e) {
            System.out.println("Thread interrupted.");
        }
        System.out.println("\n" + message + " Completed");
    }
}

class TaskRunner extends Thread {
    private String message;
    Task task;

    TaskRunner(String msg, Task taskInstance) {
        message = msg;
        task = taskInstance;
    }

    public void run() {
        synchronized (task) {
            task.performTask(message);
        }
    }
}

public class SyncExample {
    public static void main(String[] args) {
        Task task = new Task();
        TaskRunner runner1 = new TaskRunner("Task 1", task);
        TaskRunner runner2 = new TaskRunner("Task 2", task);

        runner1.start();
        runner2.start();

        try {
            runner1.join();
            runner2.join();
        } catch (Exception e) {
            System.out.println("Thread interrupted");
        }
    }
}

Output:

Executing     Task 1

Task 1 Completed
Executing     Task 2

Task 2 Completed

Alternative Implementation Using Synchronized Method

We can also define the entire method as synchronized to achieve the same behavior without explicitly synchronizing blocks inside the thread’s run() method:

class Task {
    public synchronized void performTask(String message) {
        System.out.println("Executing\t" + message);
        try {
            Thread.sleep(800);
        } catch (InterruptedException e) {
            System.out.println("Thread interrupted.");
        }
        System.out.println("\n" + message + " Completed");
    }
}

In this case, we don’t need to add the synchronized block in the run() method since the performTask() method itself is synchronized.

Example with Partial Synchronization of a Method

Sometimes, we may want to synchronize only part of the method instead of the entire method. Here’s how it can be done:

class Task {
    public void performTask(String message) {
        synchronized (this) {
            System.out.println("Executing\t" + message);
            try {
                Thread.sleep(800);
            } catch (InterruptedException e) {
                System.out.println("Thread interrupted.");
            }
            System.out.println("\n" + message + " Completed");
        }
    }
}

This is useful when only certain parts of the method need exclusive access to shared resources, while other parts can run concurrently.

Example of Synchronized Method Using Anonymous Class

class NumberPrinter {
    synchronized void printNumbers(int base) {
        for (int i = 1; i <= 3; i++) {
            System.out.println(base + i);
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                System.out.println(e);
            }
        }
    }
}

public class AnonymousSyncExample {
    public static void main(String[] args) {
        final NumberPrinter printer = new NumberPrinter();

        Thread thread1 = new Thread() {
            public void run() {
                printer.printNumbers(10);
            }
        };

        Thread thread2 = new Thread() {
            public void run() {
                printer.printNumbers(20);
            }
        };

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

Output:

11
12
13
21
22
23
End of lesson.