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:
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.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.
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 callsnotify()
ornotifyAll()
on the same object.notify()
: This method wakes up a single waiting thread that calledwait()
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.
synchronized(obj) {
while (!condition) {
obj.wait(); // releases lock and waits
}
// condition met, continue
}
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.
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.).
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