title | shortTitle | description | category | language | tag | |||||
---|---|---|---|---|---|---|---|---|---|---|
Monitor Pattern in Java: Implementing Robust Locking Mechanisms with Monitors |
Monitor |
Learn how the Monitor design pattern in Java ensures thread safety and synchronization. Explore examples, applicability, and benefits of using monitors in concurrent programming. |
Concurrency |
en |
|
- Synchronized Block
The Monitor design pattern in Java is essential for synchronizing concurrent operations, ensuring thread safety and preventing race conditions.
Real-world example
Imagine a shared office printer that several employees need to use. The printer can only handle one print job at a time to avoid mixing up pages from different documents. This scenario is analogous to the Monitor design pattern in programming.
In this example, the printer represents the shared resource, and the employees are analogous to threads. A system is set up where each employee must request access to the printer before starting their print job. This system ensures that only one employee (or "thread") can use the printer at a time, preventing any overlap or interference between jobs. Once a print job is complete, the next employee in the queue can access the printer. This mechanism mirrors the Monitor pattern's way of controlling access to a shared resource, ensuring orderly and safe use by multiple "threads" (employees).
In plain words
Monitor pattern is used to enforce single-threaded access to data. Only one thread at a time is allowed to execute code within the monitor object.
Wikipedia says
In concurrent programming (also known as parallel programming), a monitor is a synchronization construct that allows threads to have both mutual exclusion and the ability to wait (block) for a certain condition to become false. Monitors also have a mechanism for signaling other threads that their condition has been met.
The Monitor design pattern is a synchronization technique used in concurrent programming to ensure that only one thread can execute a particular section of code at a time. It is a method of wrapping and hiding the synchronization primitives (like semaphores or locks) within the methods of an object. This pattern is useful in situations where race conditions could occur.
The Java Monitor design pattern can be seen in the Bank
class example. By using synchronized methods, the Bank
class ensures that only one thread can perform transactions at any given time, illustrating effective use of the Monitor pattern in real-world applications.
Here is a simplified version of the Bank
class with additional comments:
public class Bank {
@Getter
private final int[] accounts;
public Bank(int accountNum, int baseAmount) {
accounts = new int[accountNum];
Arrays.fill(accounts, baseAmount);
}
public synchronized void transfer(int accountA, int accountB, int amount) {
// Only one thread can execute this method at a time due to the 'synchronized' keyword.
if (accounts[accountA] >= amount && accountA != accountB) {
accounts[accountB] += amount;
accounts[accountA] -= amount;
}
}
public synchronized int getBalance() {
// Only one thread can execute this method at a time due to the 'synchronized' keyword.
int balance = 0;
for (int account : accounts) {
balance += account;
}
return balance;
}
public synchronized int getBalance(int accountNumber) {
// Only one thread can execute this method at a time due to the 'synchronized' keyword.
return accounts[accountNumber];
}
}
In the Main
class, multiple threads are created to perform transactions on the bank accounts. The Bank
class, acting as a monitor, ensures that these transactions are performed in a thread-safe manner.
public class Main {
private static final int NUMBER_OF_THREADS = 5;
private static final int BASE_AMOUNT = 1000;
private static final int ACCOUNT_NUM = 4;
public static void runner(Bank bank, CountDownLatch latch) {
try {
SecureRandom random = new SecureRandom();
Thread.sleep(random.nextInt(1000));
for (int i = 0; i < 1000000; i++) {
bank.transfer(random.nextInt(4), random.nextInt(4), random.nextInt(0, BASE_AMOUNT));
}
latch.countDown();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
public static void main(String[] args) throws InterruptedException {
var bank = new Bank(ACCOUNT_NUM, BASE_AMOUNT);
var latch = new CountDownLatch(NUMBER_OF_THREADS);
var executorService = Executors.newFixedThreadPool(NUMBER_OF_THREADS);
for (int i = 0; i < NUMBER_OF_THREADS; i++) {
executorService.execute(() -> runner(bank, latch));
}
latch.await();
}
}
In this example, the Bank
class is the monitor, and the transfer
method is the critical section that needs to be executed in a mutually exclusive manner. The synchronized
keyword in Java is used to implement the Monitor pattern, ensuring that only one thread can execute the transfer
method at a time.
The Monitor design pattern should be used in situations where you have shared resources that need to be accessed and manipulated by multiple threads or processes concurrently. This pattern is particularly useful in scenarios where synchronization is necessary to prevent race conditions, data corruption, and inconsistent states. Here are some situations where you should consider using the Monitor pattern:
-
Shared Data: When your application involves shared data structures, variables, or resources that need to be accessed and updated by multiple threads. Monitors ensure that only one thread can access the shared resource at a time, preventing conflicts and ensuring data consistency.
-
Critical Sections: When you have critical sections of code that need to be executed by only one thread at a time. Critical sections are portions of code where shared resources are manipulated, and concurrent access could lead to problems. Monitors help ensure that only one thread can execute the critical section at any given time.
-
Thread Safety: When you need to ensure thread safety without relying solely on low-level synchronization mechanisms like locks and semaphores. Monitors provide a higher-level abstraction that encapsulates synchronization and resource management.
-
Waiting and Signaling: When you have scenarios where threads need to wait for certain conditions to be met before proceeding. Monitors often include mechanisms for threads to wait for specific conditions and for other threads to notify them when the conditions are satisfied.
-
Deadlock Prevention: When you want to prevent deadlocks by providing a structured way to acquire and release locks on shared resources. Monitors help avoid common deadlock scenarios by ensuring that resource access is well-managed.
-
Concurrent Data Structures: When you're implementing concurrent data structures, such as queues, stacks, or hash tables, where multiple threads need to manipulate the structure while maintaining its integrity.
-
Resource Sharing: When multiple threads need to share limited resources, like connections to a database or access to a network socket. Monitors can help manage the allocation and release of these resources in a controlled manner.
-
Improved Maintainability: When you want to encapsulate synchronization logic and shared resource management within a single object, improving code organization and making it easier to reason about concurrency-related code.
However, it's important to note that the Monitor pattern might not be the best fit for all concurrency scenarios. In some cases, other synchronization mechanisms like locks, semaphores, or concurrent data structures might be more suitable. Additionally, modern programming languages and frameworks often provide higher-level concurrency constructs that abstract away the complexities of low-level synchronization.
Before applying the Monitor pattern, it's recommended to thoroughly analyze your application's concurrency requirements and choose the synchronization approach that best suits your needs, taking into consideration factors like performance, complexity, and available language features.
Common implementations of the Monitor design pattern in Java include synchronized methods and blocks, as well as concurrent data structures like Vector
and Hashtable
.
Benefits:
- Ensures mutual exclusion, preventing race conditions.
- Simplifies the complexity of thread management by providing a clear structure for resource access.
Trade-offs:
- Can lead to decreased performance due to locking overhead.
- Potential for deadlocks if not carefully designed.
Semaphore: Used to control access to a common resource by multiple threads; Monitor uses a binary semaphore concept at its core. Mutex: Another mechanism for ensuring mutual exclusion; Monitor is a higher-level construct often implemented using mutexes.