Interview

10 Thread Synchronization Interview Questions and Answers

Prepare for your next technical interview with our guide on thread synchronization, covering key concepts and practical examples.

Thread synchronization is a critical concept in concurrent programming, ensuring that multiple threads can operate smoothly without interfering with each other. It is essential for maintaining data consistency and preventing race conditions in multi-threaded applications. Mastery of thread synchronization techniques is crucial for developing robust and efficient software systems.

This article provides a curated selection of interview questions focused on thread synchronization. By reviewing these questions and their detailed answers, you will gain a deeper understanding of synchronization mechanisms and be better prepared to demonstrate your expertise in technical interviews.

Thread Synchronization Interview Questions and Answers

1. Explain the concept of a race condition and provide an example in code.

A race condition occurs when multiple threads or processes attempt to modify shared data simultaneously, leading to unpredictable outcomes. This typically happens because operations are not atomic and can be interrupted, resulting in incorrect results.

Example in Python:

import threading

counter = 0

def increment_counter():
    global counter
    for _ in range(100000):
        counter += 1

threads = []
for _ in range(10):
    thread = threading.Thread(target=increment_counter)
    threads.append(thread)
    thread.start()

for thread in threads:
    thread.join()

print(f'Final counter value: {counter}')

In this example, multiple threads increment a shared counter variable. Due to the race condition, the final counter value may not be as expected. To prevent this, thread synchronization mechanisms like locks can be used:

import threading

counter = 0
lock = threading.Lock()

def increment_counter():
    global counter
    for _ in range(100000):
        with lock:
            counter += 1

threads = []
for _ in range(10):
    thread = threading.Thread(target=increment_counter)
    threads.append(thread)
    thread.start()

for thread in threads:
    thread.join()

print(f'Final counter value: {counter}')

In this modified example, a lock ensures only one thread can increment the counter at a time, preventing the race condition.

2. What is a mutex, and how does it differ from a semaphore? Provide a code example using a mutex.

A mutex is a locking mechanism used to synchronize access to a shared resource, ensuring only one thread can access it at a time. A semaphore, however, is a signaling mechanism that can allow multiple threads to access a limited number of resources concurrently. The key difference is that a mutex is binary (locked/unlocked), while a semaphore can have a count to allow multiple accesses.

Example using a mutex in Python:

import threading

counter = 0
counter_lock = threading.Lock()

def increment_counter():
    global counter
    for _ in range(1000):
        with counter_lock:
            counter += 1

threads = []
for _ in range(10):
    thread = threading.Thread(target=increment_counter)
    threads.append(thread)
    thread.start()

for thread in threads:
    thread.join()

print(f'Final counter value: {counter}')

In this example, the counter_lock mutex ensures only one thread can increment the counter at a time, preventing race conditions.

3. Write a Java program that demonstrates the use of the synchronized keyword to prevent concurrent access to a shared resource.

The synchronized keyword in Java controls access to a shared resource by multiple threads. When a method or block is declared as synchronized, the thread holds the monitor for that method’s object. If another thread executes any synchronized method, it will be blocked until the monitor is released.

Java program demonstrating synchronized:

class Counter {
    private int count = 0;

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

    public int getCount() {
        return count;
    }
}

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

        Runnable task = () -> {
            for (int i = 0; i < 1000; i++) {
                counter.increment();
            }
        };

        Thread thread1 = new Thread(task);
        Thread thread2 = new Thread(task);

        thread1.start();
        thread2.start();

        thread1.join();
        thread2.join();

        System.out.println("Final count: " + counter.getCount());
    }
}

4. Implement a simple producer-consumer problem using wait() and notify() methods in Java.

The producer-consumer problem involves producers generating data and placing it in a buffer, while consumers take data from the buffer. Proper synchronization ensures producers do not add data to a full buffer and consumers do not remove data from an empty buffer.

Java implementation using wait() and notify():

import java.util.LinkedList;
import java.util.Queue;

class ProducerConsumer {
    private final Queue<Integer> buffer = new LinkedList<>();
    private final int capacity = 5;

    public void produce() throws InterruptedException {
        int value = 0;
        while (true) {
            synchronized (this) {
                while (buffer.size() == capacity) {
                    wait();
                }
                buffer.add(value++);
                System.out.println("Produced " + value);
                notify();
                Thread.sleep(1000);
            }
        }
    }

    public void consume() throws InterruptedException {
        while (true) {
            synchronized (this) {
                while (buffer.isEmpty()) {
                    wait();
                }
                int value = buffer.poll();
                System.out.println("Consumed " + value);
                notify();
                Thread.sleep(1000);
            }
        }
    }

    public static void main(String[] args) {
        ProducerConsumer pc = new ProducerConsumer();

        Thread producerThread = new Thread(() -> {
            try {
                pc.produce();
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        });

        Thread consumerThread = new Thread(() -> {
            try {
                pc.consume();
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        });

        producerThread.start();
        consumerThread.start();
    }
}

5. What are reentrant locks, and how do they differ from regular locks? Provide a code example using ReentrantLock in Java.

Reentrant locks allow the same thread to acquire the lock multiple times without causing a deadlock. This is useful when a thread needs to re-enter a synchronized block or method it already holds the lock for.

Example using ReentrantLock in Java:

import java.util.concurrent.locks.ReentrantLock;

public class ReentrantLockExample {
    private final ReentrantLock lock = new ReentrantLock();

    public void outerMethod() {
        lock.lock();
        try {
            System.out.println("Outer method acquired the lock");
            innerMethod();
        } finally {
            lock.unlock();
        }
    }

    public void innerMethod() {
        lock.lock();
        try {
            System.out.println("Inner method acquired the lock");
        } finally {
            lock.unlock();
        }
    }

    public static void main(String[] args) {
        ReentrantLockExample example = new ReentrantLockExample();
        example.outerMethod();
    }
}

In this example, the outerMethod acquires the lock and then calls innerMethod, which also acquires the same lock. Since the lock is reentrant, the same thread can acquire it multiple times without causing a deadlock.

6. Write a Python program that uses threading and a Lock object to ensure thread-safe increments of a shared counter.

In Python, the threading module provides a Lock object to synchronize threads. The Lock object ensures only one thread can access the shared resource at a time.

Example demonstrating thread-safe increments of a shared counter:

import threading

class Counter:
    def __init__(self):
        self.value = 0
        self._lock = threading.Lock()

    def increment(self):
        with self._lock:
            self.value += 1

def worker(counter, num_increments):
    for _ in range(num_increments):
        counter.increment()

counter = Counter()
num_threads = 10
num_increments = 1000

threads = []
for _ in range(num_threads):
    thread = threading.Thread(target=worker, args=(counter, num_increments))
    threads.append(thread)
    thread.start()

for thread in threads:
    thread.join()

print(f'Final counter value: {counter.value}')

7. Describe how to manage a thread pool in Java. Provide a code example using ExecutorService.

Managing a thread pool in Java involves using the ExecutorService interface, which provides a higher-level replacement for working with threads directly. A thread pool reuses a fixed number of threads to execute tasks, which helps in managing system resources efficiently.

Example using ExecutorService:

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class ThreadPoolExample {
    public static void main(String[] args) {
        ExecutorService executorService = Executors.newFixedThreadPool(5);

        for (int i = 0; i < 10; i++) {
            executorService.submit(() -> {
                System.out.println("Task executed by: " + Thread.currentThread().getName());
            });
        }

        executorService.shutdown();
    }
}

8. Explain the use of atomic variables in Java. Provide a code example using AtomicInteger.

Atomic variables in Java, such as AtomicInteger, are part of the java.util.concurrent.atomic package. They perform atomic operations on single variables, ensuring thread-safety without explicit synchronization mechanisms.

Example using AtomicInteger:

import java.util.concurrent.atomic.AtomicInteger;

public class AtomicExample {
    private AtomicInteger counter = new AtomicInteger(0);

    public void increment() {
        counter.incrementAndGet();
    }

    public int getCounter() {
        return counter.get();
    }

    public static void main(String[] args) {
        AtomicExample example = new AtomicExample();
        Runnable task = () -> {
            for (int i = 0; i < 1000; i++) {
                example.increment();
            }
        };

        Thread thread1 = new Thread(task);
        Thread thread2 = new Thread(task);

        thread1.start();
        thread2.start();

        try {
            thread1.join();
            thread2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println("Final counter value: " + example.getCounter());
    }
}

9. Describe the purpose of condition variables in Java and provide a code example using Condition.

Condition variables in Java manage communication between threads, allowing them to wait for certain conditions to be met. They are part of the java.util.concurrent.locks package and are used with Lock objects.

Example using Condition:

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

public class ConditionExample {
    private Lock lock = new ReentrantLock();
    private Condition condition = lock.newCondition();
    private boolean ready = false;

    public void awaitCondition() throws InterruptedException {
        lock.lock();
        try {
            while (!ready) {
                condition.await();
            }
        } finally {
            lock.unlock();
        }
    }

    public void signalCondition() {
        lock.lock();
        try {
            ready = true;
            condition.signalAll();
        } finally {
            lock.unlock();
        }
    }

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

        Thread t1 = new Thread(() -> {
            try {
                example.awaitCondition();
                System.out.println("Condition met, thread proceeding.");
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        });

        Thread t2 = new Thread(() -> {
            example.signalCondition();
            System.out.println("Condition signaled.");
        });

        t1.start();
        Thread.sleep(1000);
        t2.start();
    }
}

10. Explain the concept of thread-local storage in Java and provide a code example using ThreadLocal.

Thread-local storage in Java is implemented using the ThreadLocal class. Each thread accessing a ThreadLocal variable has its own, independently initialized copy of the variable.

Example using ThreadLocal:

public class ThreadLocalExample {
    private static ThreadLocal<Integer> threadLocalValue = ThreadLocal.withInitial(() -> 1);

    public static void main(String[] args) {
        Runnable task = () -> {
            int value = threadLocalValue.get();
            System.out.println(Thread.currentThread().getName() + " initial value: " + value);
            threadLocalValue.set(value + 1);
            System.out.println(Thread.currentThread().getName() + " updated value: " + threadLocalValue.get());
        };

        Thread thread1 = new Thread(task);
        Thread thread2 = new Thread(task);

        thread1.start();
        thread2.start();
    }
}

In this example, each thread gets its own initial value of 1 for the threadLocalValue variable. When the threads update this value, they do so independently, demonstrating thread-local storage.

Previous

10 Azure SQL Database Interview Questions and Answers

Back to Interview
Next

10 Virtual DOM Interview Questions and Answers