Skip to content

Synchronization

6. What is thread synchronization and why is it necessary? Explain how it is achieved in Java using synchronized methods and synchronized blocks.

Thread synchronization is the mechanism that ensures only one thread can access a shared resource or piece of code at a time.

It's necessary to prevent thread interference and memory consistency errors (or race conditions), which occur when multiple threads try to access and modify the same shared data simultaneously. Without synchronization, threads can overwrite each other's work, leading to corrupted data and unpredictable program behavior.

Synchronization in Java is achieved using the synchronized keyword, which can be applied in two ways:

  1. Synchronized Methods: When method is declared as synchronized, the entire method is locked. A thread must acquire the intrinsic lock of the object before it can execute the method. While one thread is inside a synchronized method, all other threads that try to enter any synchronized method on the same object will be blocked.

  2. Synchronized Blocks: Instead of locking an entire method, just a small part of it can be placed in a synchronized block which is marked with the synchronized keyword and specifies which object to lock on. This is more efficient as it reduces the scope of the lock.


Demonstrating a "race condition" where two threads increment a shared counter. Without synchronization, the final count is often incorrect. With synchronization, the result is always correct.

java
class Counter {
    private int count = 0;

    // A synchronized method to prevent race conditions
    public synchronized void increment() {
        count++;
    }
    
    // An alternative way using a synchronized block
    public void increment() {
        synchronized(this) {
            count++;
        }
    }
    
    public int getCount() {
        return count;
    }
}

public class SynchronizationDemo {
    public static void main(String[] args) throws InterruptedException {
        
        Counter counter = new Counter();
        // threads that will increment the counter
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                counter.increment();
            }
        });

        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                counter.increment();
            }
        });

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

        // Wait for both threads to finish
        t1.join();
        t2.join();

        // The final count should be 2000. 
        // Without synchronization, it's usually less.
        System.out.println("Final count: " 
	        + counter.getCount());
    }
}

Inter-thread Communication

7. Explain how threads can communicate using the wait(), notify(), and notifyAll() methods.

The wait(), notify(), and notifyAll() methods are fundamental to inter-thread communication in Java. They allow threads to coordinate their activities, especially when one thread (a consumer) needs to wait for another thread (a producer) to complete a task.

These methods are defined in the Object class and must be called from within a synchronized block or method on the object being used as a lock.

  • wait(): This method causes the current thread to release the lock and enter a "waiting" state. It will remain in this state until another thread calls notify() or notifyAll() on the same object.

  • notify(): This method wakes up a single waiting thread that called wait() on the same object. The awakened thread then attempts to reacquire the object's lock. If multiple threads are waiting, only one is chosen arbitrarily.

  • notifyAll(): This method wakes up all threads that are waiting on the same object's lock. Each of these threads will then compete to acquire the lock.

java
synchronized(obj) {
    while (!condition) {
        obj.wait();  // releases lock and waits
    }
    // condition met, continue
}
java
synchronized(obj) {
    // change condition
    obj.notify(); // or obj.notifyAll();
}

8. Write a program to implement the producer-consumer problem.

This classic problem involves two types of threads, a "producer" and a "consumer," who share a common, fixed-size buffer. The producer adds items to the buffer, and the consumer removes them.

The program uses wait() and notify() to ensure the producer doesn't add to a full buffer and the consumer doesn't try to remove from an empty one.

java
import java.util.LinkedList;

// This class represents the shared buffer between Producer and Consumer
class SharedBuffer {
    private LinkedList<Integer> buffer = new LinkedList<>();
    private int capacity = 2; // Buffer can hold max 2 items

    // Producer method
    public synchronized void produce(int item) throws InterruptedException {
        // Wait if the buffer is full
        while (buffer.size() == capacity) {
            System.out.println("Buffer is full. Producer is waiting...");
            wait();
        }
        
        buffer.add(item);
        System.out.println("Producer produced: " + item);
        
        // Notify the consumer that an item is available
        notify();
        Thread.sleep(100); // Simulate time taken
    }

    // Consumer method
    public synchronized int consume() throws InterruptedException {
        // Wait if the buffer is empty
        while (buffer.isEmpty()) {
            System.out.println("Buffer is empty. Consumer is waiting...");
            wait();
        }
        
        int item = buffer.removeFirst();
        System.out.println("Consumer consumed: " + item);
        
        // Notify the producer that space is available
        notify();
        Thread.sleep(100); // Simulate time taken
        return item;
    }
}

public class ProducerConsumerDemo {
    public static void main(String[] args) {
    
        SharedBuffer buffer = new SharedBuffer();

        // Producer thread
        Thread producerThread = new Thread(() -> {
            try {
                for (int i = 1; i <= 5; i++) {
                    buffer.produce(i);
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });

        // Consumer thread
        Thread consumerThread = new Thread(() -> {
            try {
                for (int i = 0; i < 5; i++) {
                    buffer.consume();
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });

        producerThread.start();
        consumerThread.start();
    }
}

Part E: Practical Application

9. Write a Java program using two threads to print numbers from 1 to 10 in sequence.

This program uses a shared object with wait() and notify() to ensure two threads print numbers in a strictly alternating sequence (1, 2, 3, 4, etc.).

java
class NumberPrinter {
    private int number = 1;
    private int max = 10;
    private boolean isOddTurn = true;

    public synchronized void printOdd() throws InterruptedException {
        while (number <= max) {
            
            while (!isOddTurn) {  // Wait if it's not odd thread's turn
                wait();
            }
            if (number > max) 
	            break;
            System.out.println(Thread.currentThread().getName() 
	            + ": " + number++);
            isOddTurn = false;
            notify(); // Notify the even thread
        }
    }

    // Method for the thread printing even numbers
    public synchronized void printEven() throws InterruptedException {
        while (number <= max) {
            while (isOddTurn) { // Wait if it's not even thread's turn
                wait();
            }
            if (number > max) 
	            break;
            System.out.println(Thread.currentThread().getName() 
	            + ": " + number++);
            isOddTurn = true;
            notify(); // Notify the odd thread
        }
    }
}

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

        // Thread 1 for printing odd numbers
        Thread t1 = new Thread(() -> {
            try {
                printer.printOdd();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "Thread-1 (Odd)");  // this is thread name

        // Thread 2 for printing even numbers
        Thread t2 = new Thread(() -> {
            try {
                printer.printEven();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "Thread-2 (Even)");

        t1.start();
        t2.start();
    }
}
Thread-1 (Odd): 1
Thread-2 (Even): 2
Thread-1 (Odd): 3
Thread-2 (Even): 4
Thread-1 (Odd): 5
Thread-2 (Even): 6
Thread-1 (Odd): 7
Thread-2 (Even): 8
Thread-1 (Odd): 9
Thread-2 (Even): 10

Made with ❤️ for students, by a fellow learner.