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 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 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 ofunlock()
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:
Feature | Lock Framework | Synchronized |
---|---|---|
Method Flexibility | Lock can be implemented across different methods. | Synchronized cannot be shared across methods. |
Try to Acquire Lock | Supports tryLock() with timeout to attempt acquiring the lock. | Not supported. |
Fair Lock Management | Yes, with fair lock option for long-waiting threads. | Not supported. |
Waiting Threads List | You can see the list of waiting threads. | Not possible. |
Handling Exceptions | Needs 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 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 returnsfalse
. - 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. |
Related Chapters
- Overview
- Basic Concepts
- Input/Output in Java
- Flow Control in Java
- Operators in Java
- Arrays
- OOPS in Java
- Inheritance
- Abstraction
- Encapsulation
- Polymorphism
- Constructors
- Methods
- Memory Allocation
- Wrapper Classes
- Keywords
- Access Modifiers
- Classes in java
- Packages in Java
- Exceptions in Java
- Multithreading in Java
- Synchronization in Java
- File Handling