Mastering Java Thread Synchronization for Robust Applications
Written on
Chapter 1: Understanding Synchronization in Java
In concurrent programming, it is crucial to manage multiple threads and their access to shared resources effectively. Java provides synchronization mechanisms that allow you to coordinate thread interactions, safeguarding data integrity while preventing race conditions. This article explores the nuances of synchronization, equipping you to design resilient multi-threaded applications.
Synchronization Basics
The 'synchronized' keyword in Java applies to methods and blocks but not to classes or variables. When multiple threads try to execute tasks simultaneously, data inconsistency can occur. The 'synchronized' keyword addresses this issue by allowing only one thread to execute the synchronized method or block at a time for a specific object, thus preventing data inconsistencies.
While the primary benefit of using 'synchronized' is to tackle data inconsistency, it can also lead to increased thread waiting times, potentially causing performance bottlenecks. Therefore, it's advisable to use 'synchronized' only when necessary.
When a thread wants to run a synchronized method, it must first acquire the object's lock. Once the lock is obtained, the thread can execute any synchronized method on that object. After the method finishes executing, the lock is released automatically. The Java Virtual Machine (JVM) manages this lock acquisition and release internally, relieving the programmer from this responsibility.
While one thread runs a synchronized method on an object, other threads cannot execute any synchronized methods on that same object concurrently. However, they can still run non-synchronized methods at the same time.
Example:
class X {
synchronized void m1() {}
synchronized void m2() {}
void m3() {}
}
If thread t1 calls m1() on object x, it secures the lock for x and starts executing. If t2 tries to call m1() or if t3 calls m2(), both will have to wait. In contrast, t4 can call m3() without any delay.
Locking Mechanism
The locking concept in Java is centered around objects rather than methods. Each Java object comprises two segments:
- Non-synchronized area: Accessible by multiple threads simultaneously, often used for read operations where the object's state remains unchanged.
- Synchronized area: Can be accessed by only one thread at a time, typically used for operations that modify the object's state (like adding, removing, deleting, or replacing).
Real-world Example:
class ReservationSystem {
void checkAvailability() {
// Just a read operation}
synchronized void bookTicket() {
// Update operation}
}
Synchronized Method Demonstration
class Display {
public synchronized void wish(String name) {
for (int i = 0; i < 10; i++) {
System.out.print("Good Morning: ");
try {
Thread.sleep(2000);} catch (InterruptedException e) {
// Handle exception}
System.out.println(name);
}
}
}
class MyThread extends Thread {
Display d;
String name;
MyThread(Display d, String name) {
this.d = d;
this.name = name;
}
public void run() {
d.wish(name);}
}
class SynchronizedDemo {
public static void main(String[] args) {
Display d = new Display();
MyThread t1 = new MyThread(d, "Dhoni");
MyThread t2 = new MyThread(d, "Yuvraj");
t1.start();
t2.start();
}
}
In this scenario, the wish() method in the Display class is synchronized, ensuring that only one thread can execute it at any given time. As a result, each thread (t1 and t2) takes turns calling the wish() method.
Case Study
Display d1 = new Display();
Display d2 = new Display();
MyThread t1 = new MyThread(d1, "Dhoni");
MyThread t2 = new MyThread(d2, "Yuvraj");
t1.start();
t2.start();
Here, two instances of the Display class are created and passed to different threads, along with unique names. The output may vary due to the threads operating on separate Java objects (d1 and d2), meaning synchronization is not effective in this context.
Conclusion: Synchronization is essential when multiple threads access the same Java object. If they work with different objects, it is unnecessary.
Furthermore, each class in Java has a unique lock, known as the class-level lock. When a thread wants to execute a static synchronized method, it must acquire this class-level lock first. This lock is released automatically after the method execution is complete.
While a thread runs a static synchronized method, other threads cannot simultaneously execute any static synchronized method of the same class. They can, however, execute the following methods concurrently:
- static m3() (normal static method)
- synchronized m4() (normal synchronized instance method)
- m5() (normal instance method)
Example:
class X {
static synchronized void m1() {}
static synchronized void m2() {}
static void m3() {}
synchronized void m4() {}
void m5() {}
}
If thread t1 calls m1(), it acquires the class-level lock (CL(X)). If another thread t2 tries to call m1(), it will have to wait. Similarly, if t3 calls m2(), it also enters a waiting state. In contrast, t4 can execute m3() without any contention, and t5 and t6 can execute m4() and m5() respectively.
Synchronized Blocks
When only a few lines of code require synchronization, it’s not practical to declare the entire method as synchronized. Instead, those specific lines can be encapsulated within a synchronized block. The advantage of synchronized blocks is the reduction of thread waiting time, leading to enhanced system performance.
Synchronized blocks can be defined as follows:
- To lock the current object (this):
synchronized(this) {
// Only the thread that acquires the lock of the current object can execute this code
}
- To lock a specific object b:
synchronized(b) {
// Only the thread that acquires the lock of object 'b' can execute this code
}
- To lock the class-level lock:
synchronized(Display.class) {
// Only the thread that acquires the class-level lock for the "Display" class can execute this code
}
Locking Concept
The locking concept is applicable to object types and class types, but not to primitive types. Attempting to use a primitive type in a synchronized block will yield a compilation error, as a reference type is necessary.
Regarding acquiring multiple locks simultaneously, a thread can indeed lock multiple objects at once:
class X {
public synchronized void m1() {
// Thread holds lock of 'X' object
Y y = new Y();
synchronized(y) {
// Thread holds locks of 'X' and 'Y' objects
Z z = new Z();
synchronized(z) {
// Thread holds locks of 'X', 'Y', and 'Z' objects}
}
}
}
X x = new X();
x.m1();
Synchronized Statement
The synchronized statement refers to the lines inside a synchronized method or block. These statements are termed synchronized statements.
Conclusion
By mastering synchronization in Java threads, you enable your concurrent applications to exhibit reliable and predictable behavior. Carefully assess your use cases, select the appropriate synchronization mechanisms, and adhere to best practices for developing robust and efficient multi-threaded programs.
The first video titled "Java Thread Synchronization (Part 1) | Built-in locks" provides a thorough explanation of Java's synchronization mechanisms and their importance in thread management.
The second video, "Java Concurrency & Multithreading Complete Course in 2 Hours | Zero to Hero," delivers a comprehensive overview of Java concurrency and multithreading concepts, making it an excellent resource for developers looking to deepen their understanding.