The synchronized Keyword in Java

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 the increment method, it acquires the lock of the counter object instance. If Thread-B then attempts to call counter.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 increment1 and increment2 methods use different locks (lock1 and lock2), 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.

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: synchronized guarantees 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: synchronized implements 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:
    1. Minimize Synchronization Scope: Synchronize only the critical section of code that presents thread-safety issues, not the entire method.
    2. Use Dedicated Lock Objects: For synchronized blocks, use private final member variables as lock objects. Avoid using this or objects accessible externally to prevent deadlocks.
    3. Consider the Concurrency Utilities Package: For complex concurrent scenarios (such as read-write locks, semaphores, etc.), the java.util.concurrent package provides more powerful and flexible tools (e.g., ReentrantLock) that can serve as alternatives or supplements to synchronized.