Java Programming Synchronization And Locks Complete Guide

 Last Update:2025-06-22T00:00:00     .NET School AI Teacher - SELECT ANY TEXT TO EXPLANATION.    14 mins read      Difficulty-Level: beginner

Online Code run

🔔 Note: Select your programming language to check or run code at

💻 Run Code Compiler

Step-by-Step Guide: How to Implement Java Programming Synchronization and Locks

1. Introduction to Synchronization in Java

Java provides a mechanism called synchronization, which is used to control access to shared resources by multiple threads. Synchronization in Java can be achieved in two ways:

  • Using synchronized keyword.
  • Using java.util.concurrent.locks.Lock interface.

Example: Using synchronized Keyword

Problem: Multiple threads incrementing a shared counter.

Step-by-Step Guide:

  1. Create a class with a shared counter.
  2. Use the synchronized keyword to control access to the increment method.

Example Code:

public class Counter {
    private int count = 0;

    // Synchronized method to increment the counter
    public synchronized void increment() {
        count++;
    }

    // Method to return the counter value
    public int getCount() {
        return count;
    }

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

        // Create multiple threads to increment the counter
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                counter.increment();
            }
        });

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

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

        // Wait for both threads to finish
        t1.join();
        t2.join();

        // Print the final count
        System.out.println("Final count: " + counter.getCount());
    }
}

Output:

Final count: 2000

Explanation:

  • The increment method is marked as synchronized, which ensures that only one thread can execute this method at a time.
  • When t1 is executing increment, t2 has to wait until t1 finishes its execution.
  • This guarantees that the counter is incremented correctly, even when accessed by multiple threads.

2. Using Lock Interface

The Lock interface provides a more flexible and powerful mechanism for thread synchronization compared to the synchronized keyword. Lock allows us to lock and unlock resources explicitly.

Common Methods in Lock Interface:

  • void lock(): Acquires the lock.
  • void unlock(): Releases the lock.
  • boolean tryLock(): Attempts to acquire the lock without blocking the calling thread.
  • boolean tryLock(long time, TimeUnit unit): Attempts to acquire the lock for a specified waiting time.
  • Condition newCondition(): Returns a new Condition instance that is bound to this Lock instance.

Benefits:

  • More flexible than synchronized (e.g., explicit lock and unlock).
  • Allows conditional locking.
  • Supports timeout-based locking.

Example: Using ReentrantLock

Problem: Multiple threads updating a shared resource.

Step-by-Step Guide:

  1. Create a class with a shared resource and a ReentrantLock.
  2. Use the lock() and unlock() methods to control access to the shared resource.

Example Code:

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class Resource {
    private int value = 0;
    private Lock lock = new ReentrantLock();

    public void updateValue(int increment) {
        lock.lock(); // Explicitly acquire the lock
        try {
            // Critical section: only one thread can execute this block at a time
            value += increment;
            System.out.println(Thread.currentThread().getName() + " updated value to " + value);
        } finally {
            lock.unlock(); // Always release the lock in a finally block
        }
    }

    public static void main(String[] args) {
        Resource resource = new Resource();

        // Create multiple threads to update the resource
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                resource.updateValue(1);
            }
        }, "Thread-1");

        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                resource.updateValue(-1);
            }
        }, "Thread-2");

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

        try {
            t1.join();
            t2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println("Final value: " + resource.value);
    }
}

Output:

Thread-1 updated value to 1
Thread-2 updated value to 0
Thread-1 updated value to 1
Thread-2 updated value to 0
Thread-1 updated value to 1
Thread-2 updated value to 0
Thread-1 updated value to 1
Thread-2 updated value to 0
Thread-1 updated value to 1
Final value: 0

Explanation:

  • The ReentrantLock instance lock is used to control access to the value variable.
  • Each thread calls lock.lock() to acquire the lock before updating the value.
  • The try block contains the critical section where the shared resource is modified.
  • The finally block ensures that lock.unlock() is always called, even if an exception occurs, thus preventing deadlocks.
  • The output demonstrates that each thread updates the value safely without interference from other threads.

3. Conditional Variables with Lock

Often, threads need to wait for a certain condition to be true before they can proceed. The Condition interface (part of the Lock package) helps achieve this.

Common Methods in Condition Interface:

  • void await(): Causes the current thread to wait until it is signalled or interrupted.
  • void signal(): Wakes up one waiting thread.
  • void signalAll(): Wakes up all waiting threads.

Example: Producer-Consumer Problem

The producer-consumer problem is a classic example where a producer thread produces items and a consumer thread consumes items from a shared buffer. We use ReentrantLock and Condition to synchronize these threads.

Step-by-Step Guide:

  1. Create a SharedBuffer class with a fixed-size buffer.
  2. Use ReentrantLock to control access to the buffer.
  3. Use Condition variables to handle producer and consumer synchronization.

Example Code:

import java.util.LinkedList;
import java.util.Queue;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class SharedBuffer {
    private final int BUFFER_SIZE = 5;
    private Queue<Integer> buffer = new LinkedList<>();
    private Lock lock = new ReentrantLock();
    private Condition notFull = lock.newCondition();
    private Condition notEmpty = lock.newCondition();

    public void produce(int item) throws InterruptedException {
        lock.lock();
        try {
            while (buffer.size() == BUFFER_SIZE) {
                System.out.println("Buffer is full. Producer is waiting.");
                notFull.await(); // Wait until buffer is not full
            }
            buffer.add(item);
            System.out.println("Produced: " + item);
            notEmpty.signal(); // Signal a waiting consumer
        } finally {
            lock.unlock();
        }
    }

    public int consume() throws InterruptedException {
        lock.lock();
        try {
            while (buffer.isEmpty()) {
                System.out.println("Buffer is empty. Consumer is waiting.");
                notEmpty.await(); // Wait until buffer is not empty
            }
            int item = buffer.poll();
            System.out.println("Consumed: " + item);
            notFull.signal(); // Signal a waiting producer
            return item;
        } finally {
            lock.unlock();
        }
    }

    public static void main(String[] args) {
        SharedBuffer sharedBuffer = new SharedBuffer();

        // Producer thread
        Thread producer = new Thread(() -> {
            for (int i = 1; i <= 10; i++) {
                try {
                    sharedBuffer.produce(i);
                    Thread.sleep((int) (Math.random() * 500)); // Simulate work
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
            }
        }, "Producer");

        // Consumer thread
        Thread consumer = new Thread(() -> {
            for (int i = 1; i <= 10; i++) {
                try {
                    sharedBuffer.consume();
                    Thread.sleep((int) (Math.random() * 500)); // Simulate work
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
            }
        }, "Consumer");

        producer.start();
        consumer.start();
    }
}

Sample Output:

Produced: 1
Consumed: 1
Produced: 2
Produced: 3
Produced: 4
Produced: 5
Buffer is full. Producer is waiting.
Consumed: 2
Produced: 6
Consumed: 3
Produced: 7
Consumed: 4
Produced: 8
Consumed: 5
Produced: 9
Consumed: 6
Produced: 10
Consumed: 7
Consumed: 8
Consumed: 9
Consumed: 10

Explanation:

  • The SharedBuffer class maintains a queue of fixed size (BUFFER_SIZE).
  • The produce method adds items to the buffer if there is space available. If the buffer is full, the producer waits until signaled by the consumer.
  • The consume method removes items from the buffer if items are available. If the buffer is empty, the consumer waits until signaled by the producer.
  • The notFull and notEmpty conditions are used to manage the synchronization between the producer and consumer threads.
  • This example demonstrates how Lock and Condition can be used to solve classic synchronization problems.

4. Read-Write Locks

In some scenarios, you might have multiple readers accessing a shared resource concurrently, while writes are exclusive. The ReadWriteLock interface provides a way to optimize performance in such situations.

Common Methods in ReadWriteLock Interface:

  • Lock readLock(): Returns a lock for reading.
  • Lock writeLock(): Returns a lock for writing.

Benefits:

  • Multiple threads can read concurrently.
  • Only one thread can write at a time.
  • Improves performance by allowing concurrent read access.

Example: Using ReentrantReadWriteLock

Problem: Concurrent read and exclusive write access to a shared resource.

Step-by-Step Guide:

  1. Create a class with a shared resource and a ReentrantReadWriteLock.
  2. Use the readLock() and writeLock() methods to control access to the shared resource.

Example Code:

import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

public class SharedData {
    private int data = 0;
    private ReadWriteLock rwLock = new ReentrantReadWriteLock();

    public void writeData(int newData) {
        rwLock.writeLock().lock();
        try {
            // Critical section: only one thread can write at a time
            System.out.println(Thread.currentThread().getName() + " writing data: " + newData);
            data = newData;
        } finally {
            rwLock.writeLock().unlock();
        }
    }

    public int readData() {
        rwLock.readLock().lock();
        try {
            // Critical section: multiple threads can read concurrently
            System.out.println(Thread.currentThread().getName() + " reading data: " + data);
            return data;
        } finally {
            rwLock.readLock().unlock();
        }
    }

    public static void main(String[] args) {
        SharedData sharedData = new SharedData();

        // Create multiple reader threads
        Thread reader1 = new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                sharedData.readData();
                try {
                    Thread.sleep(500); // Simulate work
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
            }
        }, "Reader-1");

        Thread reader2 = new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                sharedData.readData();
                try {
                    Thread.sleep(500); // Simulate work
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
            }
        }, "Reader-2");

        // Create a writer thread
        Thread writer = new Thread(() -> {
            for (int i = 1; i <= 5; i++) {
                sharedData.writeData(i);
                try {
                    Thread.sleep(1000); // Simulate work
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
            }
        }, "Writer");

        reader1.start();
        reader2.start();
        writer.start();

        try {
            reader1.join();
            reader2.join();
            writer.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

Sample Output:

Reader-1 reading data: 0
Reader-2 reading data: 0
Writer writing data: 1
Reader-1 reading data: 1
Reader-2 reading data: 1
Writer writing data: 2
Reader-1 reading data: 2
Reader-2 reading data: 2
Writer writing data: 3
Reader-1 reading data: 3
Reader-2 reading data: 3
Writer writing data: 4
Reader-1 reading data: 4
Reader-2 reading data: 4
Writer writing data: 5
Reader-1 reading data: 5
Reader-2 reading data: 5

Explanation:

  • The SharedData class contains an integer data and a ReentrantReadWriteLock instance.
  • The writeData method acquires the write lock before modifying the data, ensuring that only one thread can write at a time.
  • The readData method acquires the read lock before reading the data, allowing multiple threads to read concurrently.
  • The example demonstrates that multiple readers can read the data concurrently without waiting, while a writer must wait until all readers have finished reading.
  • The output shows how readers and writers interact with the shared data.

5. Benefits of Using Lock vs. synchronized

synchronized Keyword:

  • Simpler to use: Automatically locks and unlocks the object.
  • Implicitly reentrant: Can acquire the same lock multiple times.
  • Less flexible: No timeout, no waiting condition checking, no lock polling.

Lock Interface:

  • Explicit control: You have to manually lock and unlock.
  • More flexible: Supports timeouts, lock polling, multiple conditions.
  • Better performance: Allows more efficient lock management.

When to Use synchronized:

  • Simple use cases: Quick and easy to implement.
  • Few concurrent threads: Performance overhead is negligible.
  • No need for advanced features: No timeout, no multiple conditions.

When to Use Lock:

  • Complex synchronization: Multiple conditions, timeouts, lock polling.
  • Fine-grained control: Explicit lock and unlock.
  • Performance-critical applications: Better control over lock management.

6. Additional Tips for Synchronization and Locks

  • Minimize the scope of synchronized blocks: Reduce contention and improve performance.
  • Avoid holding locks while performing long-running operations: Prevent deadlocks.
  • Use volatile for simple flags: For variables that are only read and written, volatile can be a lighter alternative to synchronization.
  • Prefer higher-level concurrency utilities: Consider java.util.concurrent package classes like ExecutorService, BlockingQueue, and Semaphore for more complex scenarios.

7. Summary

In this guide, we covered the following topics:

  1. Introduction to Synchronization:

    • Using the synchronized keyword for method and block synchronization.
    • Ensuring thread-safe access to shared resources.
  2. Introduction to Locks:

    • Using ReentrantLock for more flexible locking.
    • Explicitly managing lock acquisition and release using lock() and unlock() methods.
  3. Conditional Variables:

    • Using Condition variables for producer-consumer problems and other synchronization scenarios.
    • Controlling thread execution based on specific conditions.
  4. Read-Write Locks:

    • Using ReadWriteLock for concurrent read access and exclusive write access.
    • Improving performance in scenarios with multiple readers and a single writer.
  5. Comparing synchronized and Lock:

    • Understanding the pros and cons of each approach.
    • Choosing the right synchronization mechanism based on your requirements.

Top 10 Interview Questions & Answers on Java Programming Synchronization and Locks

Top 10 Questions and Answers on Java Programming Synchronization and Locks

1. What is synchronization in Java?

Example:

public class Counter {
    private int count = 0;

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

    public synchronized int getCount() {
        return count;
    }
}

2. How does the synchronized keyword work in Java?

The synchronized keyword can be applied to methods or blocks of code to restrict their concurrent execution to a single thread.

  • Method Synchronization: When a thread enters a synchronized method, it acquires the intrinsic lock (monitor) of the object containing the method. Other threads attempting to call this method or other synchronized methods on the same object must wait until the first thread releases the lock.
  • Block Synchronization: You may synchronize only parts of a method where shared resources are accessed, rather than the entire method. This can improve performance by allowing more fine-grained control over critical sections.

Example:

public class Counter {
    private int count = 0;

    public void increment() {
        synchronized (this) { // synchronizing just the increment operation
            count++;
        }
    }
}

3. What are ReentrantLocks in Java and why should you use them?

A ReentrantLock is a more flexible threading construct available in Java's concurrency package (java.util.concurrent.locks). Unlike synchronized, ReentrantLock provides additional features like fairness policies, ability to check if a lock is held, and non-blocking attempts to acquire locks.

Example:

import java.util.concurrent.locks.ReentrantLock;

public class Counter {
    private int count = 0;
    private ReentrantLock lock = new ReentrantLock();

    public void increment() {
        lock.lock(); // try to acquire the lock
        try {
            count++;
        } finally {
            lock.unlock(); // ensure lock is released even if an exception occurs
        }
    }
}

4. Can you acquire a lock multiple times in a single thread with a ReentrantLock?

Yes, ReentrantLock allows a thread to acquire the same lock multiple times without causing deadlock. This is known as reentrancy. Each acquisition increases the lock's hold count, and each release decreases it. The lock will only be fully released when the hold count drops to zero.

Example:

public void nestedLock() {
    lock.lock();
    lock.lock();
    try {
        // critical section...
    } finally {
        lock.unlock();
        lock.unlock();
    }
}

5. What are the differences between synchronized methods and synchronized blocks?

  • Synchronized Methods: These lock the entire method, which means any other thread that attempts to access the synchronized method on the same instance will have to wait. It can be useful but could lead to performance issues if the method is long and not all code within the method requires synchronization.
  • Synchronized Blocks: By locking only specific code blocks that modify shared resources, developers can reduce the scope of synchronized operations. This can make the program perform better by reducing contention.

6. How can you avoid deadlocks in Java?

Deadlocks occur when two or more threads are waiting for each other to release the lock they have acquired, resulting in a standstill. To avoid deadlocks:

  • Order Locks: Always follow the same order in which locks are acquired across your application.
  • Timeouts: Use ReentrantLock's tryLock() method with a timeout to prevent indefinite blocking.
  • Thread Dump Analysis: Regularly analyze thread dumps to understand potential deadlock scenarios.

Example:

boolean acquireLocks(Lock a, Lock b) {
    boolean gotFirst = false;
    boolean gotSecond = false;

    try {
        gotFirst = a.tryLock(1, TimeUnit.SECONDS);
        gotSecond = b.tryLock(1, TimeUnit.SECONDS);
    } catch (InterruptedException e) {}

    if (gotFirst && gotSecond) return true; // locks acquired successfully
    if (gotFirst) a.unlock(); // release first lock if second couldn't be acquired
    if (gotSecond) b.unlock();
    return false; // failed to get either lock
}

7. What is a ReadWriteLock? How is it useful?

A ReadWriteLock is a lock that grants read access to multiple threads but write access to only one thread at a time. This is useful when data is mostly read by multiple threads but written only occasionally by one. The most common implementation is ReentrantReadWriteLock.

Example:

import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

public class Cache {
    private Map<String, String> data = new HashMap<>();
    private ReadWriteLock rwLock = new ReentrantReadWriteLock();

    public String readData(String key) {
        rwLock.readLock().lock();
        try {
            return data.get(key);
        } finally {
            rwLock.readLock().unlock();
        }
    }

    public void writeData(String key, String value) {
        rwLock.writeLock().lock();
        try {
            data.put(key, value);
        } finally {
            rwLock.writeLock().unlock();
        }
    }
}

8. What is a Condition in Java and how is it used?

A Condition is used in conjunction with locks to manage the flow of threads based on certain states or criteria. They provide advanced thread coordination capabilities such as waiting until another action changes the state, or signaling other threads that a certain condition has been met.

Example:

public class BoundedBuffer {
    final Lock lock = new ReentrantLock();
    final Condition notFull = lock.newCondition();
    final Condition notEmpty = lock.newCondition();

    final Object[] items = new Object[100];
    int putptr, takeptr, count;

    public void put(Object x) throws InterruptedException {
        lock.lock();
        try {
            while (count == items.length)
                notFull.await();
            items[putptr] = x;
            if (++putptr == items.length) putptr = 0;
            ++count;
            notEmpty.signal();
        } finally {
            lock.unlock();
        }
    }

    public Object take() throws InterruptedException {
        lock.lock();
        try {
            while (count == 0)
                notEmpty.await();
            Object x = items[takeptr];
            if (++takeptr == items.length) takeptr = 0;
            --count;
            notFull.signal();
            return x;
        } finally {
            lock.unlock();
        }
    }
}

9. What happens if an exception occurs in a synchronized block?

If an exception occurs within a synchronized block, the lock associated with the block will still be released automatically when the exception propagates out of the try block, thus preventing resource leaks.

Example:

public void updateCount(int amount) {
    synchronized (this) {
        try {
            count += amount;
            // possible exceptions here
        } finally {
            // The lock will be released even if an exception occurs
        }
    }
}

10. How can you implement busy waiting in Java?

Busy waiting is a programming practice where a thread repeatedly checks the status of a condition in a loop, without yielding the CPU to other threads. In Java, while it’s generally discouraged (as it wastes CPU), you can implement busy waiting using conditions.

Note: Busy waiting should be avoided because it can lead to high CPU usage and degraded system performance. Use proper thread coordination techniques instead, such as wait() and notify() or Conditions.

Example of busy-waiting (discouraged):

public class BusyWaiter {
    private volatile boolean ready;

    public void waitForEvent() throws Exception {
        while (!ready) {
            // busy-wait without sleeping or yielding CPU
        }
        System.out.println("Event has occurred!");
    }

    public void setReady(boolean ready) {
        this.ready = ready;
    }
}

Better approach using Conditions:

You May Like This Related .NET Topic

Login to post a comment.