Contents

Synchronization

Synchronization in Java

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

				
			

Importance of Thread Synchronization in Java

Introduction to Multithreading

Multithreading is a technique where multiple parts of a program run simultaneously, optimizing resource utilization. Threads, which are essentially lightweight processes, enable concurrent execution within a single process. For instance, imagine you are editing a document in MS Word while also playing music and browsing the internet. These activities are different processes happening at the same time. Within each application, like a music player, there are multiple threads that handle tasks like loading songs, managing the playlist, and adjusting volume. In this way, threads represent smaller tasks within a larger process, and multithreading allows these tasks to run concurrently.

Multithreading becomes particularly relevant when considering thread synchronization, which is crucial to avoid inconsistencies when multiple threads attempt to access shared resources simultaneously.

Thread Priorities

In Java, every thread has a priority that indicates how it should be treated compared to other threads. Threads with higher priorities may be given more CPU time or preempt threads with lower priorities. However, when two threads of the same priority compete for the same resource, managing their execution becomes more complex and can lead to errors.

Consider a scenario where multiple computers are connected to a single printer. If two computers attempt to print documents at the same time, the printer could mix the two print jobs, producing invalid output. Similarly, when multiple threads with the same priority try to access a shared resource in a program, the results can become inconsistent.

Java addresses this issue through thread synchronization.

Thread Synchronization

Thread synchronization ensures that only one thread can access a shared resource at a time, preventing interference between threads and avoiding data inconsistency. Synchronization in Java is implemented using locks or monitors. When a thread acquires a lock on a resource, no other thread can access it until the lock is released. This ensures safe and orderly execution of threads.

There are two main types of synchronization:

1. Mutual Exclusion : Mutual exclusion is a technique to prevent multiple threads from interfering with each other while sharing a resource. It can be implemented through:

  • Synchronized Methods
  • Synchronized Blocks
  • Static Synchronization
  • Synchronized Methods : Using the synchronized keyword, we can ensure that a method is executed by only one thread at a time, making it thread-safe. Example:
				
					class Printer {
    public void printJob(int jobNumber) {
        for (int i = 1; i <= 5; i++) {
            System.out.println("Job " + jobNumber + " is printing...");
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

class Job1 extends Thread {
    Printer printer;
    Job1(Printer p) { printer = p; }
    public void run() { printer.printJob(1); }
}

class Job2 extends Thread {
    Printer printer;
    Job2(Printer p) { printer = p; }
    public void run() { printer.printJob(2); }
}

public class Main {
    public static void main(String[] args) {
        Printer p = new Printer();
        Job1 job1 = new Job1(p);
        Job2 job2 = new Job2(p);
        job1.start();
        job2.start();
    }
}

				
			

Output:

				
					Job 1 is printing...
Job 2 is printing...
Job 1 is printing...
Job 2 is printing...
...

				
			

In this output, the jobs are printing simultaneously, leading to mixed and overlapping output.

Example 2: With Synchronized Method

				
					class Printer {
    synchronized public void printJob(int jobNumber) {
        for (int i = 1; i <= 5; i++) {
            System.out.println("Job " + jobNumber + " is printing...");
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

class Job1 extends Thread {
    Printer printer;
    Job1(Printer p) { printer = p; }
    public void run() { printer.printJob(1); }
}

class Job2 extends Thread {
    Printer printer;
    Job2(Printer p) { printer = p; }
    public void run() { printer.printJob(2); }
}

public class Main {
    public static void main(String[] args) {
        Printer p = new Printer();
        Job1 job1 = new Job1(p);
        Job2 job2 = new Job2(p);
        job1.start();
        job2.start();
    }
}

				
			

Output:

				
					Job 1 is printing...
Job 1 is printing...
Job 1 is printing...
...
--------------------------
Job 2 is printing...
Job 2 is printing...
...

				
			

With synchronization, one job completes before the other starts, ensuring consistent output.

  •  Synchronized Block: A synchronized block allows you to synchronize only a portion of the code rather than the entire method. This can be useful for optimizing performance when only specific code needs to be synchronized. Example:
				
					class Printer {
    public void printJob(int jobNumber) {
        synchronized(this) {
            for (int i = 1; i <= 5; i++) {
                System.out.println("Job " + jobNumber + " is printing...");
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
        System.out.println("Job " + jobNumber + " completed.");
    }
}

class Job1 extends Thread {
    Printer printer;
    Job1(Printer p) { printer = p; }
    public void run() { printer.printJob(1); }
}

class Job2 extends Thread {
    Printer printer;
    Job2(Printer p) { printer = p; }
    public void run() { printer.printJob(2); }
}

public class Main {
    public static void main(String[] args) {
        Printer p = new Printer();
        Job1 job1 = new Job1(p);
        Job2 job2 = new Job2(p);
        job1.start();
        job2.start();
    }
}

				
			

Output:

				
					Job 1 is printing...
Job 1 is printing...
...
Job 1 completed.
Job 2 is printing...
...

				
			
  • Static Synchronization in Java : Static synchronization in Java is used to synchronize static methods. When a static method is synchronized, the class-level lock is obtained. This ensures that only one thread can access any of the static synchronized methods of a class at a time, even if multiple threads are accessing different objects of the class. Example:
				
					class Table {
    // Synchronized static method
    synchronized static void printTable(int n) {
        for (int i = 1; i <= 5; i++) {
            System.out.println(n * i);
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                System.out.println(e);
            }
        }
    }
}

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

class MyThread2 extends Thread {
    public void run() {
        Table.printTable(10);
    }
}

public class StaticSynchronizationExample {
    public static void main(String[] args) {
        MyThread1 t1 = new MyThread1();
        MyThread2 t2 = new MyThread2();
        
        t1.start();
        t2.start();
    }
}

				
			

Output:

				
					5
10
15
20
25
50
100
150
200
250

				
			

2. Inter-Thread Communication in Java: Inter-thread communication in Java allows threads to communicate with each other using methods like wait(), notify(), and notifyAll(). These methods are part of the Object class and must be called within synchronized blocks or methods.

This mechanism is useful when one thread needs to wait for a specific condition to be fulfilled by another thread.

				
					class SharedResource {
    private int data;
    private boolean isProduced = false;

    synchronized void produce(int value) {
        while (isProduced) {
            try {
                wait(); // Wait until data is consumed
            } catch (InterruptedException e) {
                System.out.println(e);
            }
        }
        data = value;
        System.out.println("Produced: " + data);
        isProduced = true;
        notify(); // Notify the consumer
    }

    synchronized void consume() {
        while (!isProduced) {
            try {
                wait(); // Wait until data is produced
            } catch (InterruptedException e) {
                System.out.println(e);
            }
        }
        System.out.println("Consumed: " + data);
        isProduced = false;
        notify(); // Notify the producer
    }
}

class Producer extends Thread {
    SharedResource resource;

    Producer(SharedResource resource) {
        this.resource = resource;
    }

    public void run() {
        for (int i = 1; i <= 5; i++) {
            resource.produce(i);
        }
    }
}

class Consumer extends Thread {
    SharedResource resource;

    Consumer(SharedResource resource) {
        this.resource = resource;
    }

    public void run() {
        for (int i = 1; i <= 5; i++) {
            resource.consume();
        }
    }
}

public class InterThreadCommunicationExample {
    public static void main(String[] args) {
        SharedResource resource = new SharedResource();
        Producer producer = new Producer(resource);
        Consumer consumer = new Consumer(resource);

        producer.start();
        consumer.start();
    }
}

				
			

Output:

				
					Produced: 1
Consumed: 1
Produced: 2
Consumed: 2
Produced: 3
Consumed: 3
Produced: 4
Consumed: 4
Produced: 5
Consumed: 5

				
			

Method and Block Synchronization

Need for Synchronization

In a multithreaded environment, multiple threads often share access to the same fields and objects. While this form of communication can be highly efficient, it introduces potential issues such as thread interference and memory consistency errors. Synchronization constructs are crucial to prevent these errors.

Example of Synchronization Need

Consider the following example:

				
					// Java program demonstrating the need for synchronization
class Counter {
    private int count = 0;

    public void increment() {
        count++;
    }

    public int getCount() {
        return count;
    }
}

public class Test {
    public static void main(String[] args) {
        Counter counter = new Counter();
        counter.increment();
        System.out.println(counter.getCount());
    }
}

				
			

Output:

				
					1

				
			

In this example, three key operations occur:

1. Fetch the current value of count.
2. Increment the fetched value.
3. Store the new value back in the count variable.

If multiple threads access this shared object, here’s what could happen:

  • Thread 1 fetches the value of count (initially 0), increments it to 1.
  • Thread 2 fetches the value of count, which is still 0 (because Thread 1 hasn’t saved it yet) and also increments it.

After both threads complete, the value of count is incorrectly set to 1 instead of 2. This shows why synchronization is necessary.

Synchronization in Java

In Java, synchronization ensures that only one thread at a time can access a shared resource, preventing the corruption of the object’s state. If multiple threads are only reading shared resources without modification, synchronization is not necessary.

Java provides two primary forms of synchronization:

1. Method-level synchronization
2. Block-level synchronization

1. Method Synchronization

Synchronized methods offer a straightforward way to prevent thread interference and memory consistency errors. If multiple threads call a synchronized method on the same object, only one thread can execute it at any given time.

Here’s an example of unsynchronized access to a shared resource:

				
					// Example: Multiple threads accessing the same object without synchronization
class Printer {
    public void printNumbers() {
        for (int i = 0; i < 3; i++) {
            System.out.println(i);
            try {
                Thread.sleep(500);  // Simulate time-consuming task
            } catch (InterruptedException e) {
                System.out.println(e);
            }
        }
    }
}

class Worker extends Thread {
    Printer printer;

    Worker(Printer printer) {
        this.printer = printer;
    }

    public void run() {
        printer.printNumbers();
    }
}

public class Main {
    public static void main(String[] args) {
        Printer printer = new Printer();
        Worker thread1 = new Worker(printer);
        Worker thread2 = new Worker(printer);
        
        thread1.start();
        thread2.start();
    }
}

				
			

Output:

				
					0
0
1
1
2
2

				
			

Multiple threads are accessing the shared Printer object simultaneously, leading to interleaved output.

Now, let’s use synchronization:

				
					// Example: Synchronized method ensuring only one thread can access the method at a time
class Printer {
    synchronized public void printNumbers() {
        for (int i = 0; i < 3; i++) {
            System.out.println(i);
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                System.out.println(e);
            }
        }
    }
}

class Worker extends Thread {
    Printer printer;

    Worker(Printer printer) {
        this.printer = printer;
    }

    public void run() {
        printer.printNumbers();
    }
}

public class Main {
    public static void main(String[] args) {
        Printer printer = new Printer();
        Worker thread1 = new Worker(printer);
        Worker thread2 = new Worker(printer);
        
        thread1.start();
        thread2.start();
    }
}

				
			

Output:

				
					0
1
2
0
1
2

				
			

Now, both threads access the printNumbers method in a synchronized way, ensuring that only one thread at a time can run it.

2. Block Synchronization

Sometimes, we need to synchronize only a portion of the code within a method instead of the entire method. This is called block-level synchronization. For example, if a method has 100 lines of code, but only 10 lines modify a shared resource, it’s efficient to synchronize just those 10 lines.

Example:

				
					// Block synchronization example
import java.util.List;
import java.util.ArrayList;

class Person {
    String name = "";
    public int changeCount = 0;

    public void updateName(String newName, List<String> nameList) {
        synchronized (this) {
            name = newName;
            changeCount++;  // Keep track of how many times name is updated
        }

        // The rest of the code does not need synchronization
        nameList.add(newName);
    }
}

public class Test {
    public static void main(String[] args) {
        Person person = new Person();
        List<String> nameList = new ArrayList<>();
        
        person.updateName("Alice", nameList);
        System.out.println(person.name);
    }
}

				
			

Output:

				
					Alice

				
			

Lock framework vs Thread synchronization

Thread synchronization can also be achieved using the Lock framework, introduced in java.util.concurrent.locks package. The Lock framework provides more control over locks compared to synchronized blocks. While synchronized blocks lock the entire method or a block of code, the Lock framework allows more flexibility and advanced locking mechanisms.

This new framework was introduced to address the limitations of traditional synchronization, such as when you have multiple methods that require synchronization. With traditional synchronized blocks, only one thread can access one synchronized method at a time. This can lead to performance issues, especially when many methods require synchronization.

The Lock framework overcomes this limitation by allowing different locks to be assigned to different sets of methods, thus increasing concurrency and improving overall performance.

Example Usage:

				
					Lock lock = new ReentrantLock();
lock.lock();

// Critical section
lock.unlock();

				
			

The lock() method acquires the lock, and unlock() releases it. It is essential to ensure that every call to lock() is followed by a corresponding call to unlock(). Forgetting to call unlock() will result in a deadlock.

Key Considerations:

  • Acquiring a lock without releasing it will result in deadlock.
  • The number of lock() calls should always match the number of unlock() calls.
  • Unlocking without having first acquired the lock will throw an exception.

Example Scenario

In the following example, a shared resource class (Resource) contains two methods, each with its own lock. The DisplayTask and ReadTask classes represent two different jobs that will be executed by multiple threads. Using different locks for these two methods allows the tasks to be executed concurrently without interference.

				
					import java.util.Date;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

// Main class demonstrating the Lock framework
public class LockExample {
    public static void main(String[] args) {
        SharedResource resource = new SharedResource();
        Thread[] threads = new Thread[10];

        // Creating threads for DisplayTask
        for (int i = 0; i < 5; i++) {
            threads[i] = new Thread(new DisplayTask(resource), "Thread " + i);
        }

        // Creating threads for ReadTask
        for (int i = 5; i < 10; i++) {
            threads[i] = new Thread(new ReadTask(resource), "Thread " + i);
        }

        // Starting all threads
        for (int i = 0; i < 10; i++) {
            threads[i].start();
        }
    }
}

// Task for displaying a record
class DisplayTask implements Runnable {
    private SharedResource resource;

    DisplayTask(SharedResource resource) {
        this.resource = resource;
    }

    @Override
    public void run() {
        System.out.println("Executing display task");
        resource.displayRecord();
    }
}

// Task for reading a record
class ReadTask implements Runnable {
    private SharedResource resource;

    ReadTask(SharedResource resource) {
        this.resource = resource;
    }

    @Override
    public void run() {
        System.out.println("Executing read task");
        resource.readRecord();
    }
}

// Shared resource class with two methods having different locks
class SharedResource {
    private final Lock displayLock = new ReentrantLock();
    private final Lock readLock = new ReentrantLock();

    // Synchronized displayRecord method using displayLock
    public void displayRecord() {
        displayLock.lock();
        try {
            long duration = (long) (Math.random() * 10000);
            System.out.println(Thread.currentThread().getName() + ": Displaying record for " + (duration / 1000) + " seconds :: " + new Date());
            Thread.sleep(duration);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            System.out.println(Thread.currentThread().getName() + ": Display completed");
            displayLock.unlock();
        }
    }

    // Synchronized readRecord method using readLock
    public void readRecord() {
        readLock.lock();
        try {
            long duration = (long) (Math.random() * 10000);
            System.out.println(Thread.currentThread().getName() + ": Reading record for " + (duration / 1000) + " seconds :: " + new Date());
            Thread.sleep(duration);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            System.out.println(Thread.currentThread().getName() + ": Read completed");
            readLock.unlock();
        }
    }
}

				
			

Output:

				
					Executing display task
Executing display task
Executing display task
Executing display task
Executing display task
Executing read task
Executing read task
Executing read task
Executing read task
Executing read task
Thread 0: Displaying record for 3 seconds :: Mon Oct 16 11:59:42 IST 2024
Thread 5: Reading record for 5 seconds :: Mon Oct 16 11:59:42 IST 2024
Thread 0: Display completed
Thread 1: Displaying record for 4 seconds :: Mon Oct 16 11:59:45 IST 2024
Thread 5: Read completed
Thread 6: Reading record for 5 seconds :: Mon Oct 16 11:59:47 IST 2024
Thread 1: Display completed

				
			
Differences Between Lock and Synchronized:
FeatureLock FrameworkSynchronized
Method FlexibilityLock can be implemented across different methods.Synchronized cannot be shared across methods.
Try to Acquire LockSupports tryLock() with timeout to attempt acquiring the lock.Not supported.
Fair Lock ManagementYes, with fair lock option for long-waiting threads.Not supported.
Waiting Threads ListYou can see the list of waiting threads.Not possible.
Handling ExceptionsNeeds careful handling to avoid leaving a lock held during exceptions.Synchronized automatically releases the lock.

Deadlock in Java Multithreading

The synchronized keyword in Java is used to ensure that a class or method is thread-safe, meaning that only one thread at a time can hold the lock for the synchronized method or block. This forces other threads to wait until the lock is released. It becomes essential in multi-threaded environments where multiple threads are running concurrently. However, this can also introduce a problem known as deadlock.

What is Deadlock?

Deadlock occurs when two or more threads are blocked forever, each waiting on the other to release the lock. This happens when multiple synchronized blocks or methods are trying to acquire locks on each other’s objects, leading to an indefinite waiting state.

Example of Deadlock

In the following example, two threads attempt to call synchronized methods on two shared objects. This causes a deadlock because each thread holds a lock that the other needs in order to proceed.

				
					class Utility {
    // Method to sleep the current thread for a specified time
    static void sleep(long millis) {
        try {
            Thread.sleep(millis);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

// Shared class used by both threads
class SharedResource {
    // Synchronized method that locks the current object and tries to call another method
    synchronized void method1(SharedResource resource) {
        System.out.println(Thread.currentThread().getName() + " enters method1 of " + this);
        Utility.sleep(1000);
        resource.method2();
        System.out.println(Thread.currentThread().getName() + " exits method1 of " + this);
    }

    synchronized void method2() {
        System.out.println(Thread.currentThread().getName() + " enters method2 of " + this);
        Utility.sleep(1000);
        System.out.println(Thread.currentThread().getName() + " exits method2 of " + this);
    }
}

// Thread1 tries to call method1 on shared resources
class ThreadOne extends Thread {
    private SharedResource resource1;
    private SharedResource resource2;

    public ThreadOne(SharedResource resource1, SharedResource resource2) {
        this.resource1 = resource1;
        this.resource2 = resource2;
    }

    @Override
    public void run() {
        resource1.method1(resource2);
    }
}

// Thread2 tries to call method1 on shared resources
class ThreadTwo extends Thread {
    private SharedResource resource1;
    private SharedResource resource2;

    public ThreadTwo(SharedResource resource1, SharedResource resource2) {
        this.resource1 = resource1;
        this.resource2 = resource2;
    }

    @Override
    public void run() {
        resource2.method1(resource1);
    }
}

public class DeadlockExample {
    public static void main(String[] args) {
        SharedResource resource1 = new SharedResource();
        SharedResource resource2 = new SharedResource();

        ThreadOne threadOne = new ThreadOne(resource1, resource2);
        threadOne.setName("Thread1");
        threadOne.start();

        ThreadTwo threadTwo = new ThreadTwo(resource1, resource2);
        threadTwo.setName("Thread2");
        threadTwo.start();

        Utility.sleep(2000);  // Allow threads to attempt execution
    }
}

				
			

Output:

				
					Thread1 enters method1 of SharedResource@1540e19d
Thread2 enters method1 of SharedResource@677327b6

				
			

Explanation:

1. Thread1 acquires a lock on resource1 and enters the method1() method.
2. At the same time, Thread2 acquires a lock on resource2 and enters its method1() method.
3. Both threads try to acquire locks on the other’s resource (i.e., Thread1 tries to lock resource2, and Thread2 tries to lock resource1), causing a deadlock where both are stuck indefinitely waiting for the other to release the lock.

Detecting Deadlock

Deadlock detection can be done by collecting a thread dump of your program. On Windows, you can use the following command:

				
					jcmd <PID> Thread.print

				
			

Where <PID> is the Process ID of your running program, which can be obtained using the jps command. The thread dump will reveal if a deadlock condition has occurred by indicating threads that are waiting for each other.

Avoiding Deadlock

While deadlock is difficult to eliminate entirely, you can reduce its likelihood by adopting the following practices:

1. Avoid Nested Locks: Deadlock often occurs when multiple locks are required. Try to avoid locking more than one resource at a time.
2. Avoid Unnecessary Locks: Only lock resources when absolutely necessary to minimize contention.
3. Use Thread Join with Timeout: Deadlocks can occur when threads wait indefinitely for each other. Using Thread.join() with a timeout ensures that threads won’t wait forever and can recover if necessary.

Reentrant Lock in Java

In Java, traditional thread synchronization is typically achieved through the use of the synchronized keyword. While this provides basic synchronization, it can be somewhat rigid. For instance, a thread can acquire a lock only once, and there’s no built-in mechanism to manage waiting threads. This can lead to situations where certain threads are starved of resources, potentially for long periods of time.

To offer more flexibility, Java provides ReentrantLocks, which are part of the java.util.concurrent.locks package. They allow more advanced control over thread synchronization, overcoming some of the limitations of the synchronized keyword.

What are Reentrant Locks?

The ReentrantLock class implements the Lock interface and provides synchronization capabilities for methods accessing shared resources. In code, you can wrap the critical section (the part of the code that manipulates shared resources) with lock() and unlock() calls. This ensures that only the thread holding the lock can access the shared resource, while others are blocked.

As the name suggests, ReentrantLock allows the same thread to acquire the lock multiple times. Each time a thread locks the resource, a “hold count” is incremented. This count is decremented each time the thread calls unlock(), and the resource is only truly unlocked when the count reaches zero.

A notable feature of ReentrantLock is its fairness parameter. When fairness is enabled (by passing true to the constructor), the lock is granted to the thread that has been waiting the longest, thereby preventing thread starvation.

Example Usage

				
					public void someMethod() {
    reentrantLock.lock();
    try {
        // Perform operations on shared resource
    } catch (Exception e) {
        e.printStackTrace();
    } finally {
        reentrantLock.unlock();
    }
}

				
			

The unlock() call is placed inside the finally block to ensure that the lock is released even if an exception occurs during the execution of the try block.

Key Methods of ReentrantLock
  • lock(): Acquires the lock and increments the hold count.
  • unlock(): Decrements the hold count. When the count reaches zero, the lock is released.
  • tryLock(): Attempts to acquire the lock without blocking. If the lock is available, the method returns true; otherwise, it returns false.
  • tryLock(long timeout, TimeUnit unit): Waits for the specified time to acquire the lock before giving up.
  • lockInterruptibly(): Acquires the lock unless the thread is interrupted.
  • getHoldCount(): Returns the number of times the current thread has acquired the lock.
  • isHeldByCurrentThread(): Checks if the lock is held by the current thread.
  • isLocked(): Checks if the lock is held by any thread.
  • hasQueuedThreads(): Checks if any threads are waiting to acquire the lock.
  • newCondition(): Returns a Condition object for more complex thread interactions.

ReentrantLock Example

Below is an example that demonstrates how to use ReentrantLock in a multi-threaded scenario:

				
					import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.locks.ReentrantLock;

class Worker implements Runnable {
    private String taskName;
    private ReentrantLock lock;

    public Worker(ReentrantLock lock, String taskName) {
        this.lock = lock;
        this.taskName = taskName;
    }

    @Override
    public void run() {
        boolean taskCompleted = false;

        while (!taskCompleted) {
            if (lock.tryLock()) {
                try {
                    // Outer lock acquired
                    Date now = new Date();
                    SimpleDateFormat formatter = new SimpleDateFormat("HH:mm:ss");
                    System.out.println(taskName + " - Acquired outer lock at " + formatter.format(now));
                    Thread.sleep(1000);

                    // Inner lock
                    lock.lock();
                    try {
                        now = new Date();
                        System.out.println(taskName + " - Acquired inner lock at " + formatter.format(now));
                        System.out.println("Hold count: " + lock.getHoldCount());
                        Thread.sleep(1000);
                    } finally {
                        // Release inner lock
                        System.out.println(taskName + " - Releasing inner lock");
                        lock.unlock();
                    }
                    taskCompleted = true;
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    // Release outer lock
                    System.out.println(taskName + " - Releasing outer lock");
                    lock.unlock();
                }
            } else {
                System.out.println(taskName + " - Waiting for lock");
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

public class ReentrantLockExample {
    private static final int THREAD_COUNT = 3;

    public static void main(String[] args) {
        ReentrantLock lock = new ReentrantLock();
        ExecutorService executorService = Executors.newFixedThreadPool(THREAD_COUNT);

        executorService.execute(new Worker(lock, "Task1"));
        executorService.execute(new Worker(lock, "Task2"));
        executorService.execute(new Worker(lock, "Task3"));

        executorService.shutdown();
    }
}

				
			

Output:

				
					Task1 - Acquired outer lock at 10:15:30
Task2 - Waiting for lock
Task3 - Waiting for lock
Task1 - Acquired inner lock at 10:15:31
Hold count: 2
Task1 - Releasing inner lock
Task1 - Releasing outer lock
Task2 - Acquired outer lock at 10:15:32
Task2 - Acquired inner lock at 10:15:33
Hold count: 2
Task2 - Releasing inner lock
Task2 - Releasing outer lock
Task3 - Acquired outer lock at 10:15:34
Task3 - Acquired inner lock at 10:15:35
Hold count: 2
Task3 - Releasing inner lock
Task3 - Releasing outer lock

				
			

Difference Between Lock and Monitor in Java Concurrency

Java Concurrency

Java concurrency involves handling multiple threads to maximize CPU usage by ensuring efficient processing of tasks and reducing idle CPU time. The need for synchronization in multithreading has given rise to constructs like locks (or mutex) and monitors, which help control access to shared resources. Originally, locks were used for thread synchronization, but later, monitors provided a more robust and error-free mechanism.

Before diving into the differences between locks and monitors, let’s first look at their individual characteristics.

Overview of Lock (Mutex)

Locks were originally used as part of thread management to control access to shared resources. Threads would check flags to determine whether a resource was available (unlocked) or in use (locked). Now, Java provides a more explicit way of using locks through the Lock interface in the concurrency API. This method gives developers better control over locking mechanisms than the traditional implicit locking provided by monitors.

Here is an example that demonstrates basic lock usage:

				
					import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

class SharedResource {
    private Lock lock = new ReentrantLock();

    public void performTask(String threadName) {
        lock.lock();  // Acquiring lock
        try {
            System.out.println(threadName + " has acquired the lock.");
            Thread.sleep(1000); // Simulating some work
            System.out.println(threadName + " is performing the task.");
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();  // Releasing lock
            System.out.println(threadName + " has released the lock.");
        }
    }
}

public class LockExample {
    public static void main(String[] args) {
        SharedResource resource = new SharedResource();
        Thread t1 = new Thread(() -> resource.performTask("Thread 1"));
        Thread t2 = new Thread(() -> resource.performTask("Thread 2"));

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

				
			
Overview of Monitor

Monitors provide a more structured approach to synchronization in Java. They ensure that only one thread at a time can access a critical section of code. Monitors are implemented in Java through the synchronized keyword (applied to methods or code blocks), ensuring mutual exclusion between threads. Additionally, they allow threads to cooperate when working on shared tasks.

Let’s look at a simple example where two threads are synchronized to use a shared resource:

				
					class SharedPrinter {

    // Synchronized method to ensure one thread at a time
    synchronized public void printMessage(String message) {
        for (char c : message.toCharArray()) {
            System.out.print(c);
            try {
                Thread.sleep(100);  // Simulate some delay
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println();
    }
}

class PrinterThread extends Thread {
    private SharedPrinter printer;
    private String message;

    public PrinterThread(SharedPrinter printer, String message) {
        this.printer = printer;
        this.message = message;
    }

    public void run() {
        printer.printMessage(message);
    }
}

public class MonitorExample {
    public static void main(String[] args) {
        SharedPrinter printer = new SharedPrinter();
        Thread t1 = new PrinterThread(printer, "Hello");
        Thread t2 = new PrinterThread(printer, " World!");

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

				
			

Output:

				
					HWeolrlod!

				
			
Key Differences Between Lock and Monitor in Java
Lock (Mutex)Monitor
Locks have been around since the early stages of multithreading.Monitors were introduced later in the evolution of concurrency mechanisms.
Typically implemented as a flag or data field to manage coordination.Synchronization is built-in using Java’s synchronized keyword or similar constructs.
Critical section and locking mechanisms are managed by the thread itself.Mutual exclusion and cooperation are managed by the shared resource (or monitor).
Threads handle the synchronization, making it prone to errors in certain cases, such as when a thread’s time slice expires before releasing the lock.Monitors manage synchronization more efficiently, especially in small thread pools, but may face challenges when inter-thread communication is necessary.
Lock-based mechanisms are relatively less structured and leave synchronization entirely to threads.Monitors provide a structured and robust synchronization approach at the object level.
Queuing of threads is either managed by the operating system or absent.Threads are queued and managed directly by the shared object being accessed.
Less common and used only when explicit control is required.Monitors are widely used as they inherently use inter-thread locks.