0% completed
In a multithreaded application, threads often share common resources. Without proper control, simultaneous access can result in race conditions, where the outcome depends on the sequence or timing of the threads' execution.
When two or more threads access the same mutable data without coordination, the program can produce incorrect results. This is because thread operations can interleave in unpredictable ways (so-called race conditions). For example, consider two threads updating a shared counter: one increments (c++
) while another decrements (c--
). Even though c++
looks like a single operation, under the hood it consists of multiple steps: read the value, add 1, and write it back. If interleaved, one thread's update can overwrite the other’s update, leading to a lost increment or decrement. In short, unsynchronized access to shared data can break the correctness of our program.
Another issue is memory consistency. Without synchronization, changes made by one thread may not be visible to other threads promptly. Java’s memory model allows each thread to cache variables in CPU registers or local memory. If one thread updates a shared variable and another thread reads it without synchronization, the reader might see an old value (stale data). Proper synchronization ensures a happens-before relationship – meaning that writes by one thread happen-before subsequent reads by another, thus guaranteeing visibility of the latest values. In summary, synchronization is needed to prevent race conditions and to maintain a consistent view of memory across threads.
Failing to synchronize threads can cause erratic behavior that’s hard to debug. For instance, imagine a banking application where two threads update an account balance: without synchronization, one deposit might be lost, resulting in money vanishing due to a race condition. Or consider a configuration flag updated by one thread and read by others – if not handled correctly, other threads might never see the updated flag and continue running with outdated information. Thus, thread synchronization is essential for building correct and reliable multithreaded applications.
Java’s synchronized
keyword is used to ensure that only one thread at a time can access critical sections of code, protecting shared data from concurrent modifications. There are two main ways to use synchronization in Java:
Before, we learn to achieve synchronization, let's look at the problem arising without synchronization.
This example demonstrates a race condition by having two threads increment a shared counter without synchronization. The final counter value may be less than expected due to concurrent access.
Example Explanation:
increment()
method is not synchronized, so both threads may update the counter simultaneously.A synchronized method is declared with the synchronized
keyword. When a thread calls a synchronized method, it must acquire the intrinsic lock (monitor) of the object before executing the method. This prevents other threads from entering any synchronized method on the same object until the lock is released.
public synchronized void methodName() { // Critical section: code that accesses shared resources }
synchronized
ensures that only one thread can execute it at a time for a given object instance.In this example, we create a static synchronized method to increment a shared counter. Two threads will call this method concurrently, and synchronization ensures that the final counter value is consistent.
Code Explanation:
increment()
method is declared as synchronized
, ensuring mutual exclusion on the shared counter.increment()
method 1000 times concurrently.Synchronized blocks allow you to limit synchronization to a specific part of a method rather than synchronizing the entire method. This is useful for reducing overhead and increasing performance when only a small section of code (the critical section) requires exclusive access.
synchronized(lock) { // Critical section: code that accesses shared resources }
lock
is an object that serves as a monitor.In this example, we use a synchronized block with a dedicated lock object to increment a shared counter. This ensures that only the critical section that modifies the counter is synchronized, improving efficiency compared to synchronizing the entire method.
Code Explanation:
increment()
method uses a synchronized block, locking on a dedicated object lock
.increment()
method 1000 times concurrently......
.....
.....