diff --git a/coarse-grained-lock/README.md b/coarse-grained-lock/README.md new file mode 100644 index 000000000000..fde006aaa0f1 --- /dev/null +++ b/coarse-grained-lock/README.md @@ -0,0 +1,105 @@ +--- +Title: "Coarse-Grained Lock Pattern in Java: Simplifying Thread-Safe Operations" +Short Title: Coarse-Grained Lock +Description: "Coarse-Grained Lock pattern ensures thread safety by holding a global lock for related objects instead of multiple locks." +Category: Concurrency +Tags: + + Concurrency + Synchronization + Thread Safety + Locking Mechanisms +--- + + +## Intent of Coarse-Grained Lock Design Pattern + +The Coarse-Grained Lock pattern simplifies synchronization in multithreaded systems by applying a single lock to a group of operations or a shared resource. This design reduces the complexity of managing multiple fine-grained locks, albeit at the cost of some parallelism. +## Detailed Explanation of Coarse-Grained Lock Pattern with Real-World Examples + +Real-world example + +> Imagine a bank system where multiple threads handle customer transactions such as deposits, withdrawals, and balance checks. To ensure account integrity, a coarse-grained lock can be applied to the entire account object. While this reduces concurrency, it simplifies synchronization logic and prevents data inconsistencies. + +In plain words + +> A Coarse-Grained Lock is a single lock used to control access to a shared resource or related group of operations. It’s a simple way to ensure thread safety when fine-grained locking would be too complex to manage. + + +## Programmatic Example of Coarse-Grained Locks Pattern in Java + +The Coarse-Grained Lock pattern is used to synchronize access to multiple related objects. In this example, we demonstrate how to use a single lock to coordinate updates to a customer and their associated addresses. + +**Locking the Address and Customer Objects** + +```java +package com.iluwatar.coarse.grained; + +/** + * Demonstrates the Coarse-Grained Lock pattern. + */ +public class App { + + /** + * Program entry point. + * + * @param args command line arguments + */ + public static void main(String[] args) { + // Create a lock instance to synchronize access + Lock lock = new Lock(); + + // Create a customer and their addresses + Customer customer = new Customer(55, "John"); + Address address1 = new Address(customer.getCustomerId(), 1, "Chicago"); + Address address2 = new Address(customer.getCustomerId(), 2, "Houston"); + + // Use the lock to synchronize modifications to customer and addresses + lock.synchronizedMethod(() -> { + customer.setName("Smith"); + address1.setCity("Dallas"); + address2.setCity("Phoenix"); + }); + } +} + +``` +Both Addresses and customer objects can only be changed by a single thread, otherwise if another thread attempts to access a variable, then no update will occur. This example demonstrates how the Coarse-Grained Lock ensures thread-safe modifications across related objects, simplifying concurrency management while maintaining accuracy. + +## When to Use the Coarse-Grained Lock Pattern in Java + +The Coarse-Grained Lock pattern is applicable: + +* When ensuring thread safety in a multithreaded environment with shared resources. +* For applications where ease of implementation outweighs the need for high concurrency. +* When fine-grained locking introduces complexity or increases the risk of deadlocks. + +## Real-World Applications of Coarse-Grained Pattern in Java + +* Ensuring the integrity of account transactions in Banking Systems. +* Preventing concurrent threads from corrupting shared log files in Inventory Management Systems. +* Synchronizing stock operations to prevent over-selling. + +## Benefits and Trade-Offs of Coarse-Grained Lock Pattern +Benefits: + +* Easier to implement and debug compared to fine-grained locking. +* Reduces the chances of deadlocks due to fewer locks being used. +* Ensures data consistency in critical sections. + +Trade-Offs: + +* Limits the ability of threads to perform parallel operations on shared resources. +* Can lead to contention when multiple threads compete for the same lock. +* Operations that don’t require synchronization are still blocked. + +## Related Patterns + +- Readers–writer lock: allows for concurrent access for read-only operations, while requiring an exclusive lock for write operations. +- Lock Manager pattern: Can be used to define graunality in coarse-grained locks, as well as, detection and handling of deadlocks. +- Fine-Grained Lock: Increases concurrency by locking smaller portions of code or objects. +## References and Credits + +* [Java Concurrency in Practice](https://amzn.to/4cYY4kU) +* [Patterns of Enterprise Application Architecture](https://amzn.to/3Uh7rW1) +* [Oracle java concurrency](https://docs.oracle.com/javase/tutorial/essential/concurrency/) diff --git a/coarse-grained-lock/coarse-grained-lock.iml b/coarse-grained-lock/coarse-grained-lock.iml new file mode 100644 index 000000000000..9e3449c9d8f0 --- /dev/null +++ b/coarse-grained-lock/coarse-grained-lock.iml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/coarse-grained-lock/etc/coarse-grained-lock.urm.png b/coarse-grained-lock/etc/coarse-grained-lock.urm.png new file mode 100644 index 000000000000..3faee2e9cdb9 Binary files /dev/null and b/coarse-grained-lock/etc/coarse-grained-lock.urm.png differ diff --git a/coarse-grained-lock/etc/coarse-grained-lock.urm.puml b/coarse-grained-lock/etc/coarse-grained-lock.urm.puml new file mode 100644 index 000000000000..d79f4427590e --- /dev/null +++ b/coarse-grained-lock/etc/coarse-grained-lock.urm.puml @@ -0,0 +1,42 @@ +@startuml +package com.iluwatar.coarse.grained { + + class App { + + App() + + main(args: String[]) {static} + } + + class Lock { + + synchronizedMethod(task: Runnable): void + } + + class Customer { + - customerId: int + - name: String + + Customer(customerId: int, name: String) + + setCustomerId(customerId: int): void + + getCustomerId(): int + + getName(): String + + setName(name: String): void + + } + + class Address { + - addressId: int + - customerId: int + - city: String + + Address(customerId: int, addressId: int, city: String) + + getAddressId(): int + + getCustomerId(): int + + getCity(): String + + setAddressId(addressId: int): void + + setCustomerId(customerId: int): void + + setCity(city: String): void + } + + Lock --> Customer : "Protects" + Lock --> Address : "Protects" + +} + +@enduml diff --git a/coarse-grained-lock/pom.xml b/coarse-grained-lock/pom.xml new file mode 100644 index 000000000000..6647dc56e663 --- /dev/null +++ b/coarse-grained-lock/pom.xml @@ -0,0 +1,33 @@ + + + 4.0.0 + + com.iluwatar + java-design-patterns + 1.26.0-SNAPSHOT + + + coarse-grained-lock + + + 17 + 17 + UTF-8 + + + + org.testng + testng + 7.10.2 + test + + + org.junit.jupiter + junit-jupiter-api + test + + + + \ No newline at end of file diff --git a/coarse-grained-lock/src/main/java/com/iluwatar/coarse/grained/Address.java b/coarse-grained-lock/src/main/java/com/iluwatar/coarse/grained/Address.java new file mode 100644 index 000000000000..7ae20c269009 --- /dev/null +++ b/coarse-grained-lock/src/main/java/com/iluwatar/coarse/grained/Address.java @@ -0,0 +1,85 @@ +package com.iluwatar.coarse.grained; + +/** + * Represents an address with attributes customerId, addressId, and city. + * This class associates a customer's address with a unique ID and provides + * getter and setter methods to manage its attributes. + */ +public class Address { + + /** Unique identifier for the address. */ + private int addressId; + + /** Identifier of the customer associated with the address. */ + private int customerId; + + /** The city where the address is located. */ + private String city; + + /** + * Constructs an {@code Address} object with the specified customer ID, address ID, and city. + * + * @param customerId the ID of the customer + * @param addressId the unique ID of the address + * @param city the city of the address + */ + public Address(int customerId, int addressId, String city) { + this.customerId = customerId; + this.addressId = addressId; + this.city = city; + } + + /** + * Returns the unique address ID. + * + * @return the address ID + */ + public int getAddressId() { + return addressId; + } + + /** + * Updates the address ID with the specified value. + * + * @param addressId the new address ID + */ + public void setAddressId(int addressId) { + this.addressId = addressId; + } + + /** + * Returns the ID of the customer associated with the address. + * + * @return the customer ID + */ + public int getCustomerId() { + return customerId; + } + + /** + * Updates the customer ID with the specified value. + * + * @param customerId the new customer ID + */ + public void setCustomerId(int customerId) { + this.customerId = customerId; + } + + /** + * Returns the city where the address is located. + * + * @return the city name + */ + public String getCity() { + return city; + } + + /** + * Updates the city name with the specified value. + * + * @param city the new city name + */ + public void setCity(String city) { + this.city = city; + } +} diff --git a/coarse-grained-lock/src/main/java/com/iluwatar/coarse/grained/App.java b/coarse-grained-lock/src/main/java/com/iluwatar/coarse/grained/App.java new file mode 100644 index 000000000000..7c84e75dc088 --- /dev/null +++ b/coarse-grained-lock/src/main/java/com/iluwatar/coarse/grained/App.java @@ -0,0 +1,30 @@ +package com.iluwatar.coarse.grained; +/** + * The coarse grained lock is a pattern designed to handle locking multiple objects at the same time; + * for instance, a customer can have multiple addresses and wants to change an information regarding them. + * It makes more buissenes logic to lock both the customer and the addresses in order to avoid any confuison. + * This reduces concurrent programming but ensures a more simple code that can work accurately. + */ + +public class App { + + /** + * Program entry point. + * + * @param args command line args + */ + + public static void main(String[] args) { + Lock lock = new Lock(); + Customer customer = new Customer(55, "john"); + Address address1 = new Address(customer.getCustomerId(), 1, "chicago"); + Address address2 = new Address(customer.getCustomerId(), 2, "houston"); + + lock.synchronizedMethod(() -> { + customer.setName("smith"); + address1.setCity("dallas"); + address2.setCity("phoenix"); + }); + } + +} \ No newline at end of file diff --git a/coarse-grained-lock/src/main/java/com/iluwatar/coarse/grained/Customer.java b/coarse-grained-lock/src/main/java/com/iluwatar/coarse/grained/Customer.java new file mode 100644 index 000000000000..1d465bbeb2cd --- /dev/null +++ b/coarse-grained-lock/src/main/java/com/iluwatar/coarse/grained/Customer.java @@ -0,0 +1,61 @@ +package com.iluwatar.coarse.grained; + +/** + * Represents a customer with a unique ID and a name. + * Provides methods to access and modify customer attributes. + */ +public class Customer { + + /** The name of the customer. */ + private String name; + + /** The unique identifier for the customer. */ + private int customerId; + + /** + * Constructs a {@code Customer} object with the specified ID and name. + * + * @param customerId the unique ID of the customer + * @param name the name of the customer + */ + public Customer(int customerId, String name) { + this.customerId = customerId; + this.name = name; + } + + /** + * Returns the name of the customer. + * + * @return the customer's name + */ + public String getName() { + return name; + } + + /** + * Updates the name of the customer. + * + * @param name the new name of the customer + */ + public void setName(String name) { + this.name = name; + } + + /** + * Returns the unique ID of the customer. + * + * @return the customer ID + */ + public int getCustomerId() { + return customerId; + } + + /** + * Updates the unique ID of the customer. + * + * @param customerId the new customer ID + */ + public void setCustomerId(int customerId) { + this.customerId = customerId; + } +} diff --git a/coarse-grained-lock/src/main/java/com/iluwatar/coarse/grained/Lock.java b/coarse-grained-lock/src/main/java/com/iluwatar/coarse/grained/Lock.java new file mode 100644 index 000000000000..217b88327ff2 --- /dev/null +++ b/coarse-grained-lock/src/main/java/com/iluwatar/coarse/grained/Lock.java @@ -0,0 +1,23 @@ +package com.iluwatar.coarse.grained; + +/** + * Implements a coarse-grained lock for synchronizing tasks. + * This class provides a mechanism to ensure thread safety by wrapping tasks + * inside a synchronized block. + */ +public class Lock { + + /** The internal lock object used for synchronization. */ + private final Object newLock = new Object(); + + /** + * Executes a given task within a synchronized block, ensuring thread safety. + * + * @param task the {@code Runnable} task to be executed in a synchronized context + */ + public void synchronizedMethod(Runnable task) { + synchronized (newLock) { + task.run(); + } + } +} diff --git a/coarse-grained-lock/src/test/java/com/iluwatar/coarse/grained/AppTest.java b/coarse-grained-lock/src/test/java/com/iluwatar/coarse/grained/AppTest.java new file mode 100644 index 000000000000..3af2e828ea45 --- /dev/null +++ b/coarse-grained-lock/src/test/java/com/iluwatar/coarse/grained/AppTest.java @@ -0,0 +1,73 @@ +package com.iluwatar.coarse.grained; + +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.*; + +/** + * Unit tests for the {@link App} class. + */ +class AppTest { + + @Test + void testLockingMechanism() { + // Arrange + Lock lock = new Lock(); + Customer customer = new Customer(55, "john"); + Address address1 = new Address(customer.getCustomerId(), 1, "chicago"); + Address address2 = new Address(customer.getCustomerId(), 2, "houston"); + + // Act + lock.synchronizedMethod(() -> { + customer.setName("smith"); + address1.setCity("dallas"); + address2.setCity("phoenix"); + }); + + // Assert + assertEquals("smith", customer.getName(), "Customer name should be updated to 'smith'."); + assertEquals("dallas", address1.getCity(), "Address 1 city should be updated to 'dallas'."); + assertEquals("phoenix", address2.getCity(), "Address 2 city should be updated to 'phoenix'."); + } + + @Test + void testConcurrentModification() throws InterruptedException { + // Arrange + Lock lock = new Lock(); + Customer customer = new Customer(55, "john"); + Address address1 = new Address(customer.getCustomerId(), 1, "chicago"); + Address address2 = new Address(customer.getCustomerId(), 2, "houston"); + + // Simulate two threads attempting to modify data + Thread thread1 = new Thread(() -> lock.synchronizedMethod(() -> { + customer.setName("alice"); + address1.setCity("seattle"); + })); + + Thread thread2 = new Thread(() -> lock.synchronizedMethod(() -> { + customer.setName("bob"); + address2.setCity("miami"); + })); + + // Act + thread1.start(); + thread2.start(); + thread1.join(); + thread2.join(); + + /* + Assert + Only one thread's changes should be applied because of locking. + The output can be either customer names depending on the thread that will finish last. + both addresses will update as each thread access only one resource and not both at the same time. + */ + assertTrue( + customer.getName().equals("alice") || customer.getName().equals("bob"), + "Customer name should reflect changes from one thread only." + ); + assertEquals("seattle", address1.getCity(), + "Address cities should reflect changes from one thread only."); + + assertEquals("miami", address2.getCity(), + "Address cities should reflect changes from one thread only."); + } +}