Master Concurrency in Java: From Synchronized to ReentrantLock
In modern software development, multithreading is essential for building responsive applications.Java has supported multithreading since its inception, evolving from basic monitors to a sophisticated concurrency API.
This blog explores the two primary tools for thread synchronization: the built-in synchronized keyword and the flexible ReentrantLock.

The Foundation: Implicit Locking with synchronized
Every object in Java is associated with an intrinsic lock (or monitor). The synchronized keyword is the simplest way to achieve mutual exclusion, ensuring that only one thread can execute a critical section at a time
• How it works: When a thread enters a synchronized block, it automatically acquires the monitor lock and releases it when the block exits, even if an exception occurs.
• Method vs. Block: You can synchronize entire methods or specific blocks of code. Synchronizing only the "critical section" is a best practice to reduce contention and improve performance.
• JVM Optimizations: The JVM uses a strategy called lock escalation to keep synchronization cheap. It starts with Biased Locking (optimizing for cases where one thread repeatedly uses a lock) and inflates to Thin or Fat locks only when contention occurs
Explicit Control: The Lock Interface and ReentrantLock
Introduced in Java 5, the java.util.concurrent.locks package provides explicit locks that offer far more control than synchronized. The most common implementation is ReentrantLock.
Unlike synchronized, explicit locks require manual management. You must call lock() before the critical section and always call unlock() in a finally block to prevent deadlocks
Why use ReentrantLock over synchronized?

1. tryLock(): This allows a thread to attempt to acquire a lock without blocking indefinitely. It returns false if the lock is unavailable, letting your application remain responsive.
2. Timed Lock Acquisition: You can specify a timeout, which is invaluable for preventing deadlocks and meeting SLAs.
3. Fairness Policy: You can configure a "fair" lock that grants access to the thread that has been waiting the longest, preventing "starvation".
4. Interruptible Waiting: Use lockInterruptibly() to allow a waiting thread to be cancelled via an interrupt, which is essential for graceful shutdowns.
Advanced Communication: Condition Objects
While synchronized uses Object.wait() and notify(), explicit locks use Condition objects. This allows a single lock to have multiple wait-sets.
Modern Evolution: Virtual Threads and "Pinning"

With the arrival of Virtual Threads in Java 21, the choice of lock has become even more critical. In Java 21 through 23, using synchronized inside a virtual thread can cause pinning, where the virtual thread gets "stuck" to its carrier platform thread, defeating the purpose of high-scale concurrency.
While this issue is largely resolved in Java 24, developers working on earlier versions of the JDK are encouraged to use ReentrantLock for long-running operations within virtual threads to ensure the system can properly yield execution.
Best Practices for Robust Multithreading
• Prefer ExecutorService: Avoid raw thread creation. Use thread pools to manage task execution efficiently and avoid memory exhaustion.
• Keep It Small: Keep synchronized blocks as short as possible to minimize thread contention.
• Use Atomic Variables: For simple counters or flags, use AtomicInteger or AtomicBoolean instead of locks. They use low-level CPU instructions like Compare-and-Swap (CAS) and are much faster.
• Lock Ordering: To prevent deadlocks, always acquire multiple locks in a consistent, predefined global order.
• Thread Safety by Default: Whenever possible, use immutable objects or thread-safe collections like ConcurrentHashMap to reduce the need for manual locking.
Conclusion
Choosing between synchronized and ReentrantLock depends on your needs. Use synchronized for simple, method-level exclusion where safety and ease of use are paramount.
Shift to ReentrantLock when you need advanced features like timeouts, fairness, or multiple conditions for complex coordination. By following these patterns, you can build scalable, high-performance Java applications that handle concurrency with ease.


