The synchronized Keyword in Java
The synchronized keyword is a core mechanism in Java for achieving thread synchronization. It ensures mutual exclusivity when multiple threads access shared resources, thereby preventing data inconsistency issues.
1. Why is synchronized needed?
In a multi-threaded environment, when multiple threads simultaneously read and write the same shared resource (e.g., a member variable of an object), a "race condition" may occur. This is because a simple operation (such as count++) is actually performed in three steps at a low level: reading the current value, incrementing the value by one, and writing the new value back. If two threads interleave these three steps, it can lead to an incorrect final result.
2. Three Application Methods of synchronized
The core concept of synchronized is the "lock." A thread must acquire the lock before entering a code block protected by synchronized and automatically releases the lock upon exiting the block (including normal exit and exception exit). Other threads attempting to acquire the same lock will be blocked and forced to wait.
-
Method One: Synchronized Instance Methods
- Scope: The entire instance method.
- Lock Object: The instance of the object to which the method belongs (i.e.,
this). - Code Example:
public class Counter { private int count = 0; // Synchronized instance method public synchronized void increment() { count++; // This method can only be executed by one thread at any given moment } } - Explanation: Suppose there are two threads, Thread-A and Thread-B, both operating on the same Counter object
counter. When Thread-A enters theincrementmethod, it acquires the lock of thecounterobject instance. If Thread-B then attempts to callcounter.increment(), it will be blocked until Thread-A finishes executing the method and releases the lock.
-
Method Two: Synchronized Static Methods
- Scope: The entire static method.
- Lock Object: The Class object of the class to which the method belongs (e.g.,
Counter.class). - Code Example:
public class Counter { private static int staticCount = 0; // Synchronized static method public static synchronized void staticIncrement() { staticCount++; // This method can only be executed by one thread at any given moment } } - Explanation: Static methods belong to the class, not to any particular instance. Therefore, the lock is the Class object of the class. Regardless of how many Counter instances are created, there is only one such lock. This means that while one thread is executing
staticIncrement, all other threads are prevented from executing any synchronized static methods of the Counter class.
-
Method Three: Synchronized Blocks
- Scope: The portion enclosed by the code block
{}. This offers greater flexibility. - Lock Object: Can specify any object (
synchronized (lockObject)). Dedicated objects are often used to enhance clarity and control granularity. - Code Example:
public class FineGrainedCounter { private int count1 = 0; private int count2 = 0; // Create two distinct lock objects private final Object lock1 = new Object(); private final Object lock2 = new Object(); public void increment1() { synchronized (lock1) { // Use lock1 as the lock count1++; } } public void increment2() { synchronized (lock2) { // Use lock2 as the lock count2++; } } } - Explanation: This method provides finer-grained lock control. The
increment1andincrement2methods use different locks (lock1andlock2), allowing them to execute concurrently without interfering with each other. This often yields better performance than locking the entire object (synchronized (this)) or the entire method.
- Scope: The portion enclosed by the code block
3. In-Depth Understanding: Lock Storage and Characteristics
In the JVM, every object in the heap memory has an object header, part of which is called the "Mark Word." This is where lock information is stored. This explains why any object in Java can serve as a lock.
After JDK 1.6, significant optimizations were made to the synchronized lock, introducing a "lock upgrade" mechanism that greatly improved its performance. The upgrade path is as follows:
- No-lock State: Initially, the object is not locked by any thread.
- Biased Locking: If a thread acquires the lock, the lock enters a biased mode and records that thread's ID. When the same thread requests the lock again later, no synchronization operations are required, allowing direct acquisition with minimal overhead. This is suitable for scenarios where only one thread accesses the synchronized block.
- Lightweight Locking: When a second thread attempts to acquire the lock (causing lock contention that is not severe), the biased lock is upgraded to a lightweight lock. The waiting thread uses "spin-waiting" (repeatedly trying in a loop) to wait for the lock to be released, avoiding the costly overhead of thread switching at the operating system level.
- Heavyweight Locking (Monitor): If spin-waiting lasts too long (intense contention) or multiple threads compete simultaneously, the lightweight lock is upgraded to a heavyweight lock. At this point, threads failing to acquire the lock are suspended, entering a blocked state, and must wait to be awakened by the operating system scheduler. This was the reason for
synchronized's poor efficiency in earlier versions.
4. Summary and Best Practices
- Purpose:
synchronizedguarantees atomicity (operations within the synchronized block are indivisible) and visibility (once a thread modifies a shared variable, other threads immediately see the updated value). - Reentrancy: The same thread can acquire the same lock multiple times (e.g., a synchronized method can call another synchronized method) without deadlocking itself.
- Non-fair Lock:
synchronizedimplements a non-fair lock, meaning waiting threads do not acquire the lock in a strict first-come-first-served order. This can lead to thread "starvation" but generally offers higher throughput. - Best Practices:
- Minimize Synchronization Scope: Synchronize only the critical section of code that presents thread-safety issues, not the entire method.
- Use Dedicated Lock Objects: For synchronized blocks, use
private finalmember variables as lock objects. Avoid usingthisor objects accessible externally to prevent deadlocks. - Consider the Concurrency Utilities Package: For complex concurrent scenarios (such as read-write locks, semaphores, etc.), the
java.util.concurrentpackage provides more powerful and flexible tools (e.g.,ReentrantLock) that can serve as alternatives or supplements tosynchronized.