Thread Synchronization in Java

Thread synchronization in Java is the mechanism used to control access to shared resources in a multi-threaded environment. It ensures data consistency, thread safety, and predictable behavior when multiple threads execute concurrently. This is a very high-frequency interview topic.

Synchronization becomes important because threads inside the same Java process share memory. Sharing memory is useful because it allows different threads to work with the same objects, counters, queues, files, caches, browser sessions, reports, or application state. The problem is that shared memory also allows two threads to read and modify the same value at the same time. When that happens without control, the result can be incorrect even if every individual line of code looks simple.

A common beginner assumption is that a small operation such as count++ is automatically safe. It is not. That operation involves multiple internal steps: read the value, increase it, and write it back. If two threads overlap during those steps, one update can overwrite the other. This is known as a race condition. Synchronization exists to protect these critical sections, where shared data must be accessed in a controlled way.

In interviews, thread synchronization is often used to test whether a candidate understands real concurrency rather than only syntax. A good answer explains that synchronization protects shared data, uses locks or monitors, allows only one thread at a time inside a protected section, and must be used carefully because excessive locking can reduce performance or even create deadlocks.

Thread Synchronization

Understanding the Real Problem Synchronization Solves

The real problem solved by synchronization is not that multiple threads exist. Multiple threads can run safely when they work on independent data. The problem appears when multiple threads touch the same mutable data and at least one thread modifies it. In that situation, thread scheduling can affect the final result. Since thread scheduling is controlled by the JVM and operating system, a program may work correctly once and fail on the next run.

This uncertainty is what makes concurrency bugs difficult. A race condition may not happen every time. It may appear only under load, only on a faster machine, only in production, or only when two operations overlap in a rare order. Synchronization reduces that uncertainty by creating a rule: only one thread may enter the protected code at a time for a given lock.

The protected code is called a critical section. It should contain only the smallest amount of code needed to keep the shared data consistent. If the critical section is too small, it may not protect the full operation. If it is too large, it may block other threads unnecessarily. Good synchronization is therefore a balance between correctness and performance.

What Happens During a Race Condition?

A race condition happens when the correctness of a program depends on the timing or order of thread execution. Consider two threads incrementing the same counter. If the counter is 10, both threads may read 10 before either writes back the new value. Both threads then compute 11, and both write 11. Two increments were requested, but the final result increased by only one. The program has lost an update.

Race conditions are not limited to counters. They can affect account balances, inventory counts, test result reports, file writes, cache updates, user sessions, shared WebDriver references, and any mutable object used by multiple threads. In a testing framework, a race condition may produce unstable results that look like random test failures. In a banking or e-commerce system, the same kind of bug can corrupt business data.

Synchronization prevents this kind of overlap by forcing threads to acquire a lock before entering the critical section. If one thread already holds the lock, another thread must wait. When the first thread exits the synchronized area, the lock is released and another waiting thread can proceed.

Monitor Locks and the synchronized Keyword

In Java, every object has an intrinsic lock, also called a monitor lock. The synchronized keyword uses this built-in locking mechanism. When a thread enters a synchronized instance method, it acquires the lock of the current object, which is this. When it enters a synchronized static method, it acquires the lock of the class object, such as Counter.class.

A synchronized block can lock any chosen object. This gives developers more control. Instead of locking an entire method or exposing the object's public lock, a class can use a private final lock object. That style is often cleaner because external code cannot accidentally lock the same object and interfere with internal synchronization.

The lock is released automatically when the synchronized method or block completes, even if an exception is thrown. This automatic release is one reason the synchronized keyword is easier to use than manual lock management. However, easier syntax does not remove the need for careful design. Choosing the wrong lock can make synchronization ineffective, and holding a lock too long can reduce concurrency.

Object Lock vs Class Lock

Object-level locking and class-level locking are common interview areas. A synchronized instance method locks one object. If two threads call synchronized instance methods on the same object, they compete for the same lock. If they call the same synchronized method on two different objects, each object has its own lock, so the threads may run at the same time.

Static synchronization is different. A static synchronized method belongs to the class, not to a particular object instance. Its lock is the class-level lock. That means all objects of the class share the same static synchronized lock. This is useful when the shared data is static because static data belongs to the class itself.

This difference matters in real design. If the shared variable is an instance field, object-level synchronization may be enough. If the shared variable is static and therefore common to all instances, class locking or another shared lock is needed. Synchronizing an instance method while modifying static data is a common mistake because each object may have its own lock while the data is still shared globally.

Why Thread Synchronization Is Needed

In multithreading, multiple threads may access and modify shared data simultaneously, leading to:

  • Race conditions
  • Inconsistent results
  • Data corruption

Example Problem (Race Condition)

class Counter {
    int count = 0;
    void increment() {
        count++;
    }
}
          

Multiple threads calling increment() → incorrect count

What Is Synchronization?

  • Ensures only one thread accesses a critical section at a time
  • Achieved using locks (monitors)
  • Prevents race conditions

Synchronization in Java (Ways)

  1. Synchronized Method
  2. Synchronized Block
  3. Static Synchronization
  4. Inter-thread Communication (wait, notify) (preview)

1️⃣ Synchronized Method

Locks the object (instance).

class Counter {
    int count = 0;
    synchronized void increment() {
        count++;
    }
}
          
  • ✔ Only one thread can execute this method per object
  • ✔ Lock = this

2️⃣ Synchronized Block (Preferred)

Locks specific code, not entire method.

class Counter {
    int count = 0;
    void increment() {
        synchronized (this) {
            count++;
        }
    }
}
          
  • ✔ Better performance
  • ✔ Fine-grained control

3️⃣ Static Synchronization

Locks the class-level object.

class Counter {
    static int count = 0;
    static synchronized void increment() {
        count++;
    }
}
          
  • ✔ Lock = Counter.class
  • ✔ Shared across all objects

Object Lock vs Class Lock (Interview Favorite)

Type Lock Used
Instance method Object lock (this)
Synchronized block Specified object
Static method Class lock (ClassName.class)

What Is a Monitor Lock?

  • Every Java object has an intrinsic lock (monitor)
  • synchronized acquires this lock
  • Released when thread exits synchronized area

Synchronization Example (Correct Output)

class Counter {
    int count = 0;
    synchronized void increment() {
        count++;
    }
}
public class Test {
    public static void main(String[] args) throws Exception {
        Counter c = new Counter();
        Thread t1 = new Thread(() -> {
            for(int i=0;i<1000;i++) c.increment();
        });
        Thread t2 = new Thread(() -> {
            for(int i=0;i<1000;i++) c.increment();
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(c.count); // 2000
    }
}
          

Quick Performance Notes

Synchronization adds coordination overhead because threads must acquire and release locks. This overhead is usually acceptable when the protected section is small and the shared data is important. The problem starts when synchronization is applied too broadly. If a method performs many operations but only one small part actually modifies shared data, synchronizing the full method can block other threads for longer than needed.

The best practice is to identify the actual critical section and keep it as small as possible. A synchronized block often gives better control than a synchronized method because it can protect exactly the code that needs protection. This does not mean synchronized methods are bad. They are simple and useful when the whole method is truly part of the critical section. The design decision should be based on the shared data, not on habit.

Problems Synchronization Does Not Automatically Solve

Synchronization prevents threads from entering the same protected section at the same time, but it does not automatically solve every multithreading issue. Deadlocks can still occur if threads acquire multiple locks in conflicting order. Starvation can still happen if some threads repeatedly fail to get access to a resource. Livelock can still happen when threads keep reacting to each other but fail to make progress.

Synchronization also cannot repair unclear ownership of shared data. If many unrelated parts of an application modify the same object, adding synchronized blocks in random places may only make the code harder to understand. In those situations, better object ownership, immutable data, concurrent collections, or message-based design may be a cleaner solution.

Alternatives to synchronized (Preview)

Java provides several alternatives when the synchronized keyword is not the best fit. The Lock interface and ReentrantLock provide explicit lock control. Atomic classes such as AtomicInteger support common updates without writing synchronized blocks. Concurrent collections such as ConcurrentHashMap are designed for safe access by multiple threads.

These alternatives do not make synchronization knowledge unnecessary. They build on the same basic problem: multiple threads are coordinating access to shared state. A developer who understands synchronized methods, synchronized blocks, object locks, and class locks can understand why these advanced tools exist and when they are useful.

Common Beginner Mistakes

Beginners often synchronize entire methods without checking whether the whole method is a critical section. They may synchronize on the wrong object, such as a new object created inside the method, which means every thread receives a different lock and no real coordination happens. Another common mistake is forgetting the difference between static and instance locks. Static data must be protected by a lock shared across all instances.

A more subtle mistake is assuming that synchronization makes logic correct automatically. Synchronization protects execution order for a critical section, but the developer still needs to choose the correct critical section, correct lock object, and correct data ownership model. Thread safety is a design responsibility, not just a keyword.

Interview-Ready Answers

Short Answer

Thread synchronization ensures that only one thread accesses shared resources at a time.

Detailed Answer

In Java, thread synchronization is achieved using the synchronized keyword to control access to critical sections of code. It prevents race conditions and ensures data consistency by allowing only one thread to hold a lock on an object or class at a time.

Thread Synchronization Examples (Interview Pack)

1. Race Condition (Without Synchronization)

class Counter {
    int count = 0;

    void increment() {
        count++;
    }

    public static void main(String[] args) throws Exception {
        Counter c = new Counter();

        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) c.increment();
        });
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) c.increment();
        });

        t1.start(); t2.start();
        t1.join(); t2.join();

        System.out.println(c.count);
    }
}
          

Output: May be less than 2000

Explanation: Demonstrates race condition

2. Synchronized Method (Fixing Race Condition)

class Counter {
    int count = 0;

    synchronized void increment() {
        count++;
    }

    public static void main(String[] args) throws Exception {
        Counter c = new Counter();

        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) c.increment();
        });
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) c.increment();
        });

        t1.start(); t2.start();
        t1.join(); t2.join();

        System.out.println(c.count);
    }
}
          

Output: 2000

3. Synchronized Block (Better Performance)

class Counter {
    int count = 0;

    void increment() {
        synchronized (this) {
            count++;
        }
    }
}
          

Explanation: Locks only critical section

Why: Better than synchronizing entire method

4. Object-Level Lock (Instance Synchronization)

class Task {
    synchronized void work() {
        System.out.println(Thread.currentThread().getName() + " working");
    }

    public static void main(String[] args) {
        Task obj = new Task();
        new Thread(obj::work).start();
        new Thread(obj::work).start();
    }
}
          

Explanation: Lock is on object instance

5. Two Objects → Two Locks (No Blocking)

class Task {
    synchronized void work() {
        System.out.println(Thread.currentThread().getName() + " working");
    }

    public static void main(String[] args) {
        new Thread(() -> new Task().work()).start();
        new Thread(() -> new Task().work()).start();
    }
}
          

Explanation: Different objects → no synchronization conflict

6. Static Synchronized Method (Class-Level Lock)

class Printer {
    static synchronized void print() {
        System.out.println(Thread.currentThread().getName());
    }

    public static void main(String[] args) {
        new Thread(Printer::print).start();
        new Thread(Printer::print).start();
    }
}
          

Explanation: Lock is on Class object

7. Object Lock vs Class Lock Difference

class Test {
    synchronized void instanceMethod() {}
    static synchronized void staticMethod() {}
}
          

Explanation: Instance method → object lock

Explanation: Static method → class lock

8. Synchronizing on Custom Lock Object

class Counter {
    int count = 0;
    final Object lock = new Object();

    void increment() {
        synchronized (lock) {
            count++;
        }
    }
}
          

Explanation: Avoids locking this

Why: Better design

9. Synchronization with Runnable

class Task implements Runnable {
    int count = 0;

    public synchronized void run() {
        count++;
    }
}
          

10. Deadlock Example (Very Important)

class Deadlock {
    static final Object A = new Object();
    static final Object B = new Object();

    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            synchronized (A) {
                synchronized (B) {
                    System.out.println("Thread 1");
                }
            }
        });

        Thread t2 = new Thread(() -> {
            synchronized (B) {
                synchronized (A) {
                    System.out.println("Thread 2");
                }
            }
        });

        t1.start();
        t2.start();
    }
}
          

Explanation: Both threads wait forever

Why: Classic deadlock

11. Avoiding Deadlock (Consistent Lock Order)

synchronized (A) {
    synchronized (B) {
        // safe
    }
}
          

Rule: Always acquire locks in same order

12. sleep() Does NOT Release Lock

class Demo {
    synchronized void work() throws Exception {
        Thread.sleep(1000);
    }
}
          

Explanation: Lock held during sleep

13. wait() Releases Lock

class Demo {
    synchronized void work() throws Exception {
        wait();
    }
}
          

Explanation: Lock released until notify()

14. notify() and notifyAll()

class Demo {
    synchronized void resume() {
        notifyAll();
    }
}
          

15. Synchronization Overhead (Concept)

// Over-synchronization reduces performance
          

Best Practice: Synchronize minimum code

16. Thread-Safe Increment Using AtomicInteger

import java.util.concurrent.atomic.AtomicInteger;

class Demo {
    static AtomicInteger count = new AtomicInteger();

    public static void main(String[] args) {
        count.incrementAndGet();
        System.out.println(count.get());
    }
}
          

Explanation: No explicit synchronization

Why: Lock-free

17. Synchronized vs Non-Synchronized Methods

class Demo {
    synchronized void syncMethod() {}
    void normalMethod() {}
}
          

Explanation: Non-sync method runs concurrently

18. Synchronization with Multiple Methods

class Demo {
    synchronized void m1() {}
    synchronized void m2() {}
}
          

Explanation: Same lock → one thread at a time

19. Common Interview Trap

synchronized (new Object()) {
    // useless lock
}
          

Explanation: New object every time → no real synchronization

20. Interview Summary – Thread Synchronization

synchronized
          

Key Points:

  • Prevents race conditions
  • Uses intrinsic locks
  • Object lock vs Class lock
  • Deadlock risk exists
  • Synchronize minimal code

Synchronization and Performance

Synchronization improves correctness, but it is not free. When a thread enters a synchronized section, it must acquire a lock. If no other thread holds the lock, this may be quick. If another thread already holds the lock, the current thread must wait. Waiting threads do not make progress, and too much waiting can reduce the benefit of multithreading.

Performance problems usually come from locking more code than necessary. A synchronized method may be simple, but it locks the entire method. If only one line updates shared data while the rest of the method performs slow work, such as file access, database calls, logging, or network communication, then other threads may be blocked longer than needed. A synchronized block can be better because it protects only the critical section.

Good synchronization therefore keeps critical sections short. Do the non-shared work outside the lock. Enter the synchronized block only when shared mutable data must be read or updated consistently. Exit the lock as soon as the shared operation is complete. This approach preserves correctness while allowing more concurrent work to continue.

Another performance consideration is lock contention. If many threads frequently compete for the same lock, that lock becomes a bottleneck. Sometimes the solution is to reduce sharing. Sometimes it is to split one large lock into smaller locks that protect independent data. Sometimes it is to use classes from java.util.concurrent that are designed for high-concurrency access.

What Synchronization Does Not Solve

Synchronization prevents certain race conditions, but it does not solve every concurrency problem. It does not automatically prevent deadlocks. A deadlock can happen when two or more threads hold locks while waiting for each other to release other locks. The program may stop making progress even though no exception is thrown.

Synchronization also does not automatically prevent starvation. Starvation happens when a thread waits for a long time because other threads repeatedly get access to a resource first. It also does not prevent livelock, where threads keep responding to each other but still fail to make useful progress. These problems require broader concurrency design, not just adding the synchronized keyword.

Synchronization also does not make bad shared-state design good. If many parts of an application freely modify the same object, synchronized blocks may become scattered and difficult to reason about. In that case, a better design may be to reduce sharing, use immutable objects, pass messages between tasks, or isolate ownership of mutable data.

Deadlock Risk and Lock Ordering

Deadlock is one of the most important risks associated with synchronization. A classic deadlock occurs when Thread 1 holds lock A and waits for lock B, while Thread 2 holds lock B and waits for lock A. Neither thread can continue because each one is waiting for the other. The application may appear frozen.

One of the simplest ways to reduce deadlock risk is to acquire locks in a consistent order. If every thread always locks A before B, the circular wait is avoided. This rule becomes especially important in code that transfers data between two objects, updates two shared resources, or coordinates multiple services.

Another good habit is to avoid nested locks when possible. If a task can be redesigned to need only one lock at a time, it is usually easier to maintain. If nested locks are necessary, keep the locked section short and document the lock ordering rule clearly. Concurrency code should not rely on hidden assumptions that future developers may accidentally break.

sleep(), wait(), notify(), and Locks

Beginners often confuse sleep() and wait(). The difference is important in synchronized code. When a thread calls Thread.sleep() while holding a lock, it does not release that lock. Other threads that need the same lock must continue waiting until the sleeping thread wakes up and exits the synchronized section.

The wait() method behaves differently. It must be called from synchronized code, and it releases the object's monitor while the thread waits. Another thread can later call notify() or notifyAll() on the same object to wake waiting threads. This is part of inter-thread communication, where threads coordinate based on conditions rather than simply blocking each other.

In modern application code, low-level wait/notify is often replaced by higher-level concurrency utilities such as blocking queues, latches, semaphores, futures, and executors. Still, understanding wait and notify is useful because they explain how monitor-based coordination works behind the scenes.

Alternatives to synchronized

The synchronized keyword is fundamental, but it is not the only way to write thread-safe Java code. The java.util.concurrent package provides many alternatives. ReentrantLock gives more explicit lock control, including timed lock attempts and interruptible lock acquisition. Atomic classes such as AtomicInteger support common operations without explicit synchronized blocks.

Concurrent collections are another important alternative. Classes such as ConcurrentHashMap and concurrent queues are designed for safe access by multiple threads. They often provide better scalability than manually synchronizing a regular collection, especially under heavy concurrent access.

Sometimes the best alternative is not another lock, but a different design. Immutable objects remove the need for synchronization because their state cannot change after creation. Thread-local data avoids sharing by giving each thread its own copy. Message queues can move shared work into a controlled flow. These design choices often reduce complexity more effectively than adding locks everywhere.

Synchronization in Testing and Automation

Synchronization is not only a backend development topic. It matters in automation testing as well. Parallel test execution often uses multiple threads. If tests share the same driver, same test data, same report object, same file, or same environment state, synchronization issues can create unstable failures.

For example, two parallel Selenium tests writing to the same report object must coordinate access, or the output may become mixed. Two tests using the same user account may overwrite each other's data. Two threads updating a shared counter of passed and failed cases may produce incorrect totals. These are testing versions of the same race-condition problems seen in application code.

Good automation design often reduces shared state instead of synchronizing everything. Each parallel test may use its own WebDriver instance, its own test data, and isolated report entries. When shared reporting or logging is unavoidable, synchronization or thread-safe utilities should be used deliberately.

Common Mistakes in Thread Synchronization

One common mistake is synchronizing on the wrong object. If every call synchronizes on new Object(), each thread gets a different lock, so no real protection happens. A lock must be shared by all threads that need to coordinate access to the same data.

Another mistake is confusing instance locks and class locks. Synchronizing an instance method protects access for one object. It does not protect static data across all objects unless all access is coordinated through a shared class-level lock. This mistake often appears in interview questions because it reveals whether the candidate understands what object is actually being locked.

Over-synchronization is also common. Some developers make every method synchronized to be safe. This can reduce concurrency, hide design problems, and create performance bottlenecks. Synchronization should protect specific shared mutable state, not become a blanket replacement for understanding the code.

Interview-Ready Explanation

A short interview answer can be: thread synchronization ensures that only one thread at a time can access a shared critical section, preventing race conditions and keeping shared data consistent.

A stronger answer is: in Java, synchronization is commonly achieved using the synchronized keyword. A synchronized instance method locks the current object, a synchronized static method locks the class object, and a synchronized block locks the object specified in the block. Synchronization uses monitor locks and should be applied only around the minimum code that accesses shared mutable data.

A complete interview answer should also mention the tradeoffs. Synchronization can reduce performance if used excessively. It can also create deadlock risk if multiple locks are acquired in inconsistent order. For some use cases, alternatives such as AtomicInteger, ReentrantLock, concurrent collections, immutable objects, or executor-based designs may be better.

Choosing the Right Synchronization Strategy

Choosing a synchronization strategy starts with identifying the shared mutable data. If there is no shared mutable data, synchronization may not be needed at all. If the data is shared but never modified after construction, immutable design may be enough. If the data is modified by one thread and read by others, a visibility mechanism such as synchronization, volatile, or an atomic reference may be required. If several threads both read and write, stronger coordination is usually needed.

The next question is the scope of protection. Sometimes one object owns the data, and synchronizing methods on that object is clear. Sometimes only one field or one small operation needs protection, and a synchronized block is better. Sometimes several fields must be updated together to maintain an invariant. In that case, all related updates must happen under the same lock, or another thread may observe an inconsistent partial state.

Developers should also think about who can access the lock. Synchronizing on this is simple, but external code can also synchronize on the same object if it has a reference. A private final lock object is often safer for internal coordination because only the class itself can use that lock. This reduces accidental interference from outside code.

Finally, choose the simplest tool that correctly solves the problem. For a basic counter, an AtomicInteger may be clearer than a synchronized method. For a multi-step invariant, a synchronized block may be appropriate. For complex waiting conditions, higher-level utilities may be better than wait and notify. Good synchronization is not about using the most advanced tool; it is about making the shared-state rule correct, understandable, and maintainable.

Key Takeaway

Synchronization protects shared data, not threads. Use it carefully and minimally to balance correctness and performance.