Multithreading and Concurrency in Java
Master concurrent Java execution. Learn Thread lifecycle, Runnable interface, thread pools, executor services, and synchronizations.
Downloaded from PrepIQ (https://prepiq.online)
Q1. What is Multithreading in Java? Explain the concept and advantages.
Multithreading is executing multiple threads simultaneously within a single process. A thread is a lightweight sub-process that shares memory with other threads but has its own execution path and stack.
| Concept | Definition | Benefit |
|---|---|---|
| Thread | Lightweight execution unit | Parallel execution |
| Process | Heavy program instance | Isolation |
| Context Switching | Switch between threads | Concurrent execution |
| Time Slicing | OS allocates CPU time | Fair scheduling |
// Multithreading Example
public class MultiThreadingDemo {
public static void main(String[] args) {
// Create threads
Thread t1 = new Thread(() -> {
for (int i = 0; i < 5; i++) {
System.out.println('Thread 1: ' + i);
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 5; i++) {
System.out.println('Thread 2: ' + i);
}
});
// Start threads (not run)
t1.start();
t2.start();
// Both threads execute simultaneously
}
}
// Advantages of Multithreading:
// 1. Responsiveness - UI stays responsive
// 2. Resource sharing - threads share memory
// 3. Economy - thread creation is cheaper than processes
// 4. Scalability - better CPU utilization
// 5. Concurrent execution - tasks run in parallel
// 6. Better performance - on multi-core systems
Q2. What are the ways to create threads in Java? Explain Thread class vs Runnable.
Threads can be created in two ways: extending Thread class or implementing Runnable interface. Runnable is preferred as Java doesn't support multiple inheritance. Both approaches have different advantages.
| Method | Syntax | Advantages | Disadvantages |
|---|---|---|---|
| Thread Class | class MyThread extends Thread | Direct access to Thread methods | Can't extend other class |
| Runnable Interface | class MyRun implements Runnable | Can extend another class | Need Thread wrapper |
// Method 1: Extending Thread class
public class MyThread extends Thread {
public void run() {
System.out.println('Thread is running');
}
}
public class Main {
public static void main(String[] args) {
MyThread t = new MyThread();
t.start(); // Don't call run() directly
}
}
// Method 2: Implementing Runnable (PREFERRED)
public class MyRunnable implements Runnable {
public void run() {
System.out.println('Runnable is running');
}
}
public class Main {
public static void main(String[] args) {
MyRunnable runnable = new MyRunnable();
Thread t = new Thread(runnable);
t.start();
}
}
// Method 3: Lambda expression (Java 8+)
new Thread(() -> {
System.out.println('Lambda thread');
}).start();
// Method 4: Anonymous class
new Thread(new Runnable() {
public void run() {
System.out.println('Anonymous thread');
}
}).start();
// Important: Always call start(), never run()
// start() creates new thread and calls run()
// run() executes in same thread
t.start(); // Correct
t.run(); // Wrong - doesn't create new thread
Q3. What is Thread Lifecycle in Java? Explain all thread states.
Thread lifecycle consists of five states: New, Runnable, Running, Blocked/Waiting, and Terminated. Understanding state transitions is crucial for multithreading.
| State | Description | Transition |
|---|---|---|
| New | Thread created but not started | start() called |
| Runnable | Ready to run, waiting for CPU | Scheduler selects |
| Running | Currently executing | Context switch |
| Blocked/Waiting | Waiting for resource/event | Resource available |
| Terminated | Finished execution | End of run() |
public class ThreadLifecycleDemo {
public static void main(String[] args) {
Thread t = new Thread(() -> {
System.out.println('Thread is running');
try {
Thread.sleep(2000); // Waiting state
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println('Thread finished');
});
System.out.println('State: ' + t.getState()); // NEW
t.start();
System.out.println('State: ' + t.getState()); // RUNNABLE
try {
t.join(); // Wait for thread to finish
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println('State: ' + t.getState()); // TERMINATED
}
}
// Thread state transitions:
// New -> Runnable: start() called
// Runnable -> Running: Scheduler selects
// Running -> Runnable: Context switch
// Running -> Waiting: sleep(), wait(), join()
// Waiting -> Runnable: notify(), time expires
// Running -> Terminated: run() ends
// Thread.State enum values:
// NEW, RUNNABLE, BLOCKED, WAITING, TIMED_WAITING, TERMINATED
Q4. What is Synchronization? Explain synchronized keyword and mutex concept.
Synchronization prevents multiple threads from accessing shared resources simultaneously, causing data corruption. It ensures only one thread accesses critical section at a time using locks/monitors.
// Problem: Without Synchronization
public class Counter {
private int count = 0;
public void increment() {
count++; // NOT atomic - race condition!
}
}
// Two threads increment() simultaneously:
// Expected: count = 2
// Actual: count = 1 (race condition)
// Solution 1: Synchronized method
public class SynchronizedCounter {
private int count = 0;
public synchronized void increment() {
count++; // Only one thread at a time
}
public synchronized int getCount() {
return count;
}
}
// Solution 2: Synchronized block
public class SyncBlockCounter {
private int count = 0;
private final Object lock = new Object();
public void increment() {
synchronized(lock) { // Lock on specific object
count++;
}
}
}
// How Synchronization Works:
// 1. Each object has internal lock (monitor)
// 2. synchronized keyword acquires lock
// 3. Other threads wait for lock release
// 4. Lock automatically released when block ends
// Synchronized vs Unsynchronized Performance:
public class SyncPerformanceDemo {
public static void main(String[] args) {
Counter unsync = new Counter();
SynchronizedCounter sync = new SynchronizedCounter();
// Unsynchronized: ~10ms (race conditions!)
long start = System.currentTimeMillis();
for (int i = 0; i < 1_000_000; i++) {
unsync.increment();
}
System.out.println('Unsync time: ' +
(System.currentTimeMillis() - start) + 'ms');
// Synchronized: ~100ms (safe but slower)
start = System.currentTimeMillis();
for (int i = 0; i < 1_000_000; i++) {
sync.increment();
}
System.out.println('Sync time: ' +
(System.currentTimeMillis() - start) + 'ms');
}
}
Q5. What is the volatile keyword in Java? When and why should you use it?
The volatile keyword ensures that a variable's value is always read from main memory and written to main memory immediately, preventing CPU cache issues and ensuring visibility between threads.
// Problem: Without volatile
public class VolatileDemo {
private boolean flag = false;
public static void main(String[] args) {
VolatileDemo demo = new VolatileDemo();
// Thread 1: Loop reading flag
new Thread(() -> {
while (!demo.flag) {
// May never see flag=true (cached)
}
System.out.println('Flag is true');
}).start();
// Thread 2: Set flag after delay
new Thread(() -> {
try {
Thread.sleep(100);
} catch (InterruptedException e) {}
demo.flag = true;
System.out.println('Flag set to true');
}).start();
}
}
// Solution: Use volatile
public class VolatileFix {
private volatile boolean flag = false; // Keyword added
public static void main(String[] args) {
VolatileFix demo = new VolatileFix();
// Thread 1
new Thread(() -> {
while (!demo.flag) {
// Reads from main memory every time
}
System.out.println('Flag is true');
}).start();
// Thread 2
new Thread(() -> {
try {
Thread.sleep(100);
} catch (InterruptedException e) {}
demo.flag = true;
System.out.println('Flag set to true');
}).start();
}
}
// Volatile guarantees:
// 1. Visibility - all threads see latest value
// 2. Atomicity - only for read/write (not ++ operation)
// 3. No caching - always read from main memory
// When NOT to use volatile:
// - Complex operations (use synchronized)
// - Compound operations like ++ (use Atomic classes)
// - Multiple fields (use synchronized)
// Better alternative: Atomic classes
import java.util.concurrent.atomic.AtomicBoolean;
public class AtomicExample {
private AtomicBoolean flag = new AtomicBoolean(false);
public static void main(String[] args) {
AtomicExample demo = new AtomicExample();
new Thread(() -> {
while (!demo.flag.get()) {
// Thread-safe reading
}
}).start();
new Thread(() -> {
demo.flag.set(true);
}).start();
}
}
Q6. What are wait(), notify(), and notifyAll() methods? How do they enable thread communication?
wait(), notify(), and notifyAll() enable inter-thread communication. wait() makes thread wait for notification, notify() wakes one thread, notifyAll() wakes all waiting threads. Must be used within synchronized block.
// Producer-Consumer Example
public class ProducerConsumer {
private int value = 0;
private boolean produced = false;
// Producer thread
public synchronized void produce() {
while (produced) {
try {
wait(); // Wait if already produced
} catch (InterruptedException e) {}
}
value = (int) (Math.random() * 100);
System.out.println('Produced: ' + value);
produced = true;
notify(); // Wake up consumer
}
// Consumer thread
public synchronized void consume() {
while (!produced) {
try {
wait(); // Wait until produced
} catch (InterruptedException e) {}
}
System.out.println('Consumed: ' + value);
produced = false;
notify(); // Wake up producer
}
}
public class Main {
public static void main(String[] args) {
ProducerConsumer pc = new ProducerConsumer();
new Thread(() -> {
for (int i = 0; i < 5; i++) {
pc.produce();
}
}).start();
new Thread(() -> {
for (int i = 0; i < 5; i++) {
pc.consume();
}
}).start();
}
}
// How it works:
// 1. Producer produces value, calls notify()
// 2. Consumer wakes up, consumes
// 3. Consumer calls notify() to wake producer
// 4. Process repeats
// wait() vs sleep():
// wait(): Releases lock, thread waits
// sleep(): Keeps lock, thread sleeps
// Comparison: notify() vs notifyAll()
notify(); // Wakes one thread (unpredictable)
notifyAll(); // Wakes all threads (safer)
// Best Practice: Use BlockingQueue (easier)
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
public class BlockingQueueDemo {
private BlockingQueue<Integer> queue = new LinkedBlockingQueue<>();
public void produce() throws InterruptedException {
queue.put(100); // Automatically handles waiting
}
public int consume() throws InterruptedException {
return queue.take(); // Automatically handles waiting
}
}
Q7. What are Thread Pools and ExecutorService? How do they improve performance?
Thread pools manage a pool of reusable threads, eliminating overhead of creating/destroying threads. ExecutorService provides high-level API for asynchronous task execution and thread management.
import java.util.concurrent.*;
public class ThreadPoolDemo {
public static void main(String[] args) {
// Create thread pool with 5 threads
ExecutorService executor = Executors.newFixedThreadPool(5);
// Submit tasks
for (int i = 0; i < 10; i++) {
final int taskId = i;
executor.submit(() -> {
System.out.println('Task ' + taskId + ' running on ' +
Thread.currentThread().getName());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {}
});
}
// Shutdown executor
executor.shutdown(); // No new tasks accepted
try {
// Wait for all tasks to complete
executor.awaitTermination(1, TimeUnit.MINUTES);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println('All tasks completed');
}
}
// Different ExecutorService types:
// 1. FixedThreadPool - fixed number of threads
ExecutorService fixed = Executors.newFixedThreadPool(5);
// 2. CachedThreadPool - dynamic threads (reuse when idle)
ExecutorService cached = Executors.newCachedThreadPool();
// 3. SingleThreadExecutor - single thread (sequential execution)
ExecutorService single = Executors.newSingleThreadExecutor();
// 4. ScheduledThreadPool - scheduled tasks
ScheduledExecutorService scheduled =
Executors.newScheduledThreadPool(5);
scheduled.schedule(() -> System.out.println('Task'), 5, TimeUnit.SECONDS);
// 5. Custom ThreadPoolExecutor
ThreadPoolExecutor custom = new ThreadPoolExecutor(
5, // Core threads
10, // Max threads
60, // Keep-alive time
TimeUnit.SECONDS,
new LinkedBlockingQueue<>() // Task queue
);
// Return values from tasks
ExecutorService executor = Executors.newFixedThreadPool(2);
Future<Integer> future = executor.submit(() -> {
Thread.sleep(2000);
return 42;
});
try {
Integer result = future.get(); // Blocks until result
System.out.println('Result: ' + result);
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
executor.shutdown();
// Advantages over manual threading:
// 1. Thread reuse - eliminates creation overhead
// 2. Bounded resources - max threads controlled
// 3. Queue management - automatic task queuing
// 4. Automatic cleanup - shutdown management
// 5. Better performance - especially for many tasks
Q8. What are Race Conditions and Deadlocks? How do you prevent them?
Race conditions occur when multiple threads access shared data simultaneously, causing unpredictable results. Deadlocks occur when threads wait for resources held by each other. Prevention requires careful synchronization and lock ordering.
// Race Condition Example
public class RaceCondition {
private int counter = 0;
public void increment() {
counter++; // NOT atomic
}
public static void main(String[] args) {
RaceCondition rc = new RaceCondition();
// 10 threads, each increments 1000 times
for (int i = 0; i < 10; i++) {
new Thread(() -> {
for (int j = 0; j < 1000; j++) {
rc.increment();
}
}).start();
}
// Expected: 10000, Actual: ~5000 (race condition)
try {
Thread.sleep(1000);
System.out.println('Counter: ' + rc.counter);
} catch (InterruptedException e) {}
}
}
// Solution: Synchronization
public class FixedCounter {
private int counter = 0;
public synchronized void increment() {
counter++; // Thread-safe
}
}
// Deadlock Example
public class Deadlock {
private Object lock1 = new Object();
private Object lock2 = new Object();
public void method1() {
synchronized(lock1) {
System.out.println('Method1: lock1 acquired');
try { Thread.sleep(100); } catch (InterruptedException e) {}
synchronized(lock2) { // Waits for lock2
System.out.println('Method1: lock2 acquired');
}
}
}
public void method2() {
synchronized(lock2) { // Acquired first
System.out.println('Method2: lock2 acquired');
try { Thread.sleep(100); } catch (InterruptedException e) {}
synchronized(lock1) { // Waits for lock1 (held by method1)
System.out.println('Method2: lock1 acquired');
}
}
}
public static void main(String[] args) {
Deadlock dl = new Deadlock();
new Thread(dl::method1).start();
new Thread(dl::method2).start();
// Both threads wait for each other - DEADLOCK!
}
}
// Deadlock Prevention:
// 1. Lock Ordering - always acquire in same order
public class FixedDeadlock {
private Object lock1 = new Object();
private Object lock2 = new Object();
public void method1() {
synchronized(lock1) { // Always lock1 first
synchronized(lock2) {
// Safe
}
}
}
public void method2() {
synchronized(lock1) { // Always lock1 first
synchronized(lock2) {
// Safe
}
}
}
}
// 2. Timeout - acquire locks with timeout
ReentrantLock lock = new ReentrantLock();
if (lock.tryLock(1, TimeUnit.SECONDS)) {
try {
// Do work
} finally {
lock.unlock();
}
}
// 4 conditions for deadlock (prevent any one):
// 1. Mutual Exclusion
// 2. Hold and Wait
// 3. No Preemption
// 4. Circular Wait
Q9. What are Atomic operations and Atomic classes in Java? When should you use them?
Atomic classes provide thread-safe operations on single variables without synchronization. They use Compare-And-Swap (CAS) operations for lock-free thread safety, offering better performance than synchronized blocks for simple operations.
import java.util.concurrent.atomic.*;
// Problem: Without atomicity
public class NonAtomic {
private int counter = 0;
public void increment() {
counter++; // NOT atomic
}
}
// Solution: Atomic classes
public class AtomicDemo {
private AtomicInteger counter = new AtomicInteger(0);
public void increment() {
counter.incrementAndGet(); // Atomic operation
}
public int getCounter() {
return counter.get();
}
public static void main(String[] args) {
AtomicDemo demo = new AtomicDemo();
// 10 threads, each increments 10000 times
for (int i = 0; i < 10; i++) {
new Thread(() -> {
for (int j = 0; j < 10000; j++) {
demo.increment();
}
}).start();
}
try {
Thread.sleep(2000);
System.out.println('Counter: ' + demo.getCounter()); // 100000
} catch (InterruptedException e) {}
}
}
// Common Atomic Classes
// 1. AtomicInteger
AtomicInteger ai = new AtomicInteger(0);
ai.incrementAndGet(); // ++i
ai.decrementAndGet(); // --i
ai.addAndGet(5); // Add 5
ai.getAndAdd(5); // Get old, add 5
ai.compareAndSet(0, 1); // CAS operation
// 2. AtomicLong
AtomicLong al = new AtomicLong(0);
al.incrementAndGet();
// 3. AtomicBoolean
AtomicBoolean flag = new AtomicBoolean(false);
flag.set(true);
flag.get();
// 4. AtomicReference
AtomicReference<String> ref = new AtomicReference<>('initial');
ref.set('new value');
// 5. AtomicIntegerArray
AtomicIntegerArray array = new AtomicIntegerArray(10);
array.incrementAndGet(0);
// Compare-And-Swap (CAS) internally
public class CASDemo {
public static void main(String[] args) {
AtomicInteger ai = new AtomicInteger(10);
// CAS: if value == 10, set to 20
boolean success = ai.compareAndSet(10, 20);
System.out.println('Success: ' + success); // true
System.out.println('Value: ' + ai.get()); // 20
// CAS: if value == 10, set to 30 (fails)
success = ai.compareAndSet(10, 30);
System.out.println('Success: ' + success); // false
}
}
// Performance: Atomic vs Synchronized
// For 1M increments:
// Synchronized: ~100ms
// Atomic: ~10ms (10x faster)
// Reason: No lock contention, lock-free CAS
Q10. What are Concurrent Collections in Java? Compare with synchronized collections.
Concurrent collections (ConcurrentHashMap, CopyOnWriteArrayList) are thread-safe without full synchronization. They use segmentation and fine-grained locking, providing better performance than Collections.synchronizedMap().
import java.util.concurrent.*;
import java.util.*;
// Comparison: Synchronized vs Concurrent
// OLD: Using synchronized collections
Map<String, Integer> syncMap = Collections.synchronizedMap(
new HashMap<>());
// Entire map locked - slow for concurrent access
// BETTER: Using ConcurrentHashMap
ConcurrentHashMap<String, Integer> concurrentMap =
new ConcurrentHashMap<>();
// Segment locking - multiple threads can access different segments
public class ConcurrentCollectionsDemo {
public static void main(String[] args) {
// ConcurrentHashMap
ConcurrentHashMap<String, Integer> map =
new ConcurrentHashMap<>();
// Safe concurrent operations
map.put('key1', 100);
map.putIfAbsent('key2', 200);
map.replace('key1', 100, 150);
map.compute('key3', (k, v) -> 300);
// Safe iteration (no ConcurrentModificationException)
for (Map.Entry<String, Integer> entry : map.entrySet()) {
System.out.println(entry.getKey() + ' -> ' + entry.getValue());
}
// CopyOnWriteArrayList
CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();
list.add('Item1');
list.add('Item2');
// Safe iteration during modifications
list.forEach(System.out::println);
// ConcurrentLinkedQueue
ConcurrentLinkedQueue<Integer> queue = new ConcurrentLinkedQueue<>();
queue.add(1);
queue.add(2);
queue.poll();
// ConcurrentSkipListMap (sorted, thread-safe)
ConcurrentSkipListMap<Integer, String> skipMap =
new ConcurrentSkipListMap<>();
skipMap.put(1, 'One');
skipMap.put(2, 'Two');
// ConcurrentSkipListSet (sorted set, thread-safe)
ConcurrentSkipListSet<Integer> skipSet = new ConcurrentSkipListSet<>();
skipSet.add(1);
skipSet.add(2);
}
}
// Concurrent Collections Comparison
public class PerformanceComparison {
public static void main(String[] args) throws InterruptedException {
// Synchronized HashMap
long start = System.currentTimeMillis();
Map<String, Integer> syncMap = Collections.synchronizedMap(
new HashMap<>());
ExecutorService executor = Executors.newFixedThreadPool(10);
for (int i = 0; i < 100; i++) {
final int index = i;
executor.submit(() -> {
for (int j = 0; j < 1000; j++) {
syncMap.put('key' + index + j, j);
}
});
}
executor.shutdown();
executor.awaitTermination(1, TimeUnit.MINUTES);
System.out.println('Synchronized: ' +
(System.currentTimeMillis() - start) + 'ms');
// ConcurrentHashMap
start = System.currentTimeMillis();
ConcurrentHashMap<String, Integer> concMap =
new ConcurrentHashMap<>();
executor = Executors.newFixedThreadPool(10);
for (int i = 0; i < 100; i++) {
final int index = i;
executor.submit(() -> {
for (int j = 0; j < 1000; j++) {
concMap.put('key' + index + j, j);
}
});
}
executor.shutdown();
executor.awaitTermination(1, TimeUnit.MINUTES);
System.out.println('ConcurrentHashMap: ' +
(System.currentTimeMillis() - start) + 'ms');
// Result: ConcurrentHashMap ~5x faster
}
}
// Concurrent Collections Summary:
// ConcurrentHashMap: Multiple segments, fine-grained locking
// CopyOnWriteArrayList: Copy-on-write semantics
// ConcurrentLinkedQueue: Lock-free queue
// ConcurrentSkipListMap: Sorted concurrent map
// ConcurrentSkipListSet: Sorted concurrent set
Multithreading and Concurrency in Java
Master concurrent Java execution. Learn Thread lifecycle, Runnable interface, thread pools, executor services, and synchronizations.
What is Multithreading in Java? Explain the concept and advantages.
Multithreading is executing multiple threads simultaneously within a single process. A thread is a lightweight...
What are the ways to create threads in Java? Explain Thread class vs Runnable.
Threads can be created in two ways: extending Thread class or implementing Runnable interface. Runnable is pre...
What is Thread Lifecycle in Java? Explain all thread states.
Thread lifecycle consists of five states: New, Runnable, Running, Blocked/Waiting, and Terminated. Understandi...
What is Synchronization? Explain synchronized keyword and mutex concept.
Synchronization prevents multiple threads from accessing shared resources simultaneously, causing data corrupt...
What is the volatile keyword in Java? When and why should you use it?
The volatile keyword ensures that a variable's value is always read from main memory and written to main memor...
What are wait(), notify(), and notifyAll() methods? How do they enable thread communication?
wait(), notify(), and notifyAll() enable inter-thread communication. wait() makes thread wait for notification...
What are Thread Pools and ExecutorService? How do they improve performance?
Thread pools manage a pool of reusable threads, eliminating overhead of creating/destroying threads. ExecutorS...
What are Race Conditions and Deadlocks? How do you prevent them?
Race conditions occur when multiple threads access shared data simultaneously, causing unpredictable results....
What are Atomic operations and Atomic classes in Java? When should you use them?
Atomic classes provide thread-safe operations on single variables without synchronization. They use Compare-An...
What are Concurrent Collections in Java? Compare with synchronized collections.
Concurrent collections (ConcurrentHashMap, CopyOnWriteArrayList) are thread-safe without full synchronization....