# Java Concurrency: Threads, Synchronization, and Executors
Table of Contents
Java concurrency enables multiple threads to execute simultaneously, improving application performance and responsiveness. This comprehensive guide covers the essential concepts and best practices.
1. Process vs Thread in Java
The fundamental difference lies in resource sharing and isolation levels.
Process
Definition: An independent execution environment with its own memory space and system resources. A process is an instance of a program in execution.
Characteristics:
- Separate memory space: Each process has its own heap, stack, and code segments
- Heavy-weight: More system overhead to create and manage
- Isolated: Processes cannot directly access each other’s memory
- Inter-process communication: Requires special mechanisms (pipes, sockets, shared memory)
- Crash isolation: One process crash doesn’t affect others
Thread
Definition: A lightweight unit of execution within a process that shares the process’s resources.
Characteristics:
- Shared memory space: All threads in a process share heap memory
- Light-weight: Less overhead to create and switch between
- Shared resources: Share memory, file handles, and other process resources
- Direct communication: Can communicate through shared variables
- Crash propagation: One thread crash can potentially crash the entire process
PROCESS A PROCESS B┌─────────────────────┐ ┌─────────────────────┐│ Heap Memory │ │ Heap Memory ││ ┌─────┐ ┌─────┐ │ │ ┌─────┐ ┌─────┐ ││ │Thread│Thread│ │ │ │Thread│Thread│ ││ │ 1 │ 2 │ │ │ │ 1 │ 2 │ ││ └─────┘ └─────┘ │ │ └─────┘ └─────┘ ││ Code Segment │ │ Code Segment ││ File Handles │ │ File Handles │└─────────────────────┘ └─────────────────────┘When to Use What?
Use Processes When:
- Complete isolation is required
- Fault tolerance is critical (one failure shouldn’t affect others)
- Security is important (processes can’t access each other’s memory)
- Running separate applications
- Language interoperability (calling programs in different languages)
Use Threads When:
- Performance is critical (lower overhead)
- Shared data needs to be accessed frequently
- Responsive UI applications (background processing)
- Producer-consumer scenarios
- Parallel processing of related tasks
In Java, a process is an executing instance of the JVM. Each time you run a Java application, a separate process is created by the OS. Java provides the Process and ProcessBuilder classes to create and manage operating system processes.
2. Creating Threads in Java
There are several ways to create threads in Java:
Method 1: Extending the Thread Class
class MyThread extends Thread { @Override public void run() { System.out.println("Thread is running: " + Thread.currentThread().getName()); for (int i = 0; i < 5; i++) { System.out.println(Thread.currentThread().getName() + " - Count: " + i); try { Thread.sleep(1000); } catch (InterruptedException e) { System.out.println("Thread interrupted"); } } }}
// Usagepublic class ThreadExample { public static void main(String[] args) { MyThread thread1 = new MyThread(); MyThread thread2 = new MyThread();
thread1.start(); // Don't call run() directly! thread2.start(); }}Method 2: Implementing Runnable Interface (Recommended)
class MyTask implements Runnable { private String taskName;
public MyTask(String name) { this.taskName = name; }
@Override public void run() { System.out.println(taskName + " is running on: " + Thread.currentThread().getName()); for (int i = 0; i < 5; i++) { System.out.println(taskName + " - Count: " + i); try { Thread.sleep(1000); } catch (InterruptedException e) { System.out.println("Task interrupted"); } } }}
// Usagepublic class RunnableExample { public static void main(String[] args) { Thread thread1 = new Thread(new MyTask("Task-1")); Thread thread2 = new Thread(new MyTask("Task-2"));
thread1.start(); thread2.start(); }}Method 3: Using Lambda Expressions (Java 8+)
public class LambdaThreadExample { public static void main(String[] args) { // Simple lambda Thread thread = new Thread(() -> { System.out.println("Lambda thread: " + Thread.currentThread().getName()); }); thread.start();
// Lambda with multiple statements Thread worker = new Thread(() -> { for (int i = 0; i < 5; i++) { System.out.println("Working... " + i); try { Thread.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); } } }); worker.start(); }}Method 4: Using Callable and Future
import java.util.concurrent.*;
class MyCallable implements Callable<Integer> { private int number;
public MyCallable(int number) { this.number = number; }
@Override public Integer call() throws Exception { int sum = 0; for (int i = 1; i <= number; i++) { sum += i; Thread.sleep(100); } return sum; }}
// Usagepublic class CallableExample { public static void main(String[] args) throws Exception { ExecutorService executor = Executors.newSingleThreadExecutor();
Future<Integer> future = executor.submit(new MyCallable(10)); System.out.println("Calculation started...");
// Do other work while calculation happens System.out.println("Doing other work...");
// Get the result (blocks if not ready) Integer result = future.get(); System.out.println("Sum: " + result);
executor.shutdown(); }}Key Points to Remember
- Always call
start(), notrun()directly - Implementing
Runnableis preferred over extendingThread(allows extending other classes) - Use thread pools (
ExecutorService) for better resource management - Handle
InterruptedExceptionproperly - Remember to shut down
ExecutorServiceinstances to avoid resource leaks
3. Synchronization in Java
Synchronization ensures thread safety by controlling access to shared resources, preventing race conditions and data inconsistency.
Why Synchronization is Needed
class Counter { private int count = 0;
// Without synchronization - NOT thread-safe public void increment() { count++; // This is actually three operations: // 1. Read count // 2. Increment value // 3. Write count back }
public int getCount() { return count; }}
// Problem demonstrationpublic class RaceConditionExample { public static void main(String[] args) throws InterruptedException { Counter counter = new 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(); t1.join(); t2.join();
// Expected: 2000, Actual: Unpredictable (likely less than 2000) System.out.println("Count: " + counter.getCount()); }}Synchronized Methods
class SynchronizedCounter { private int count = 0;
// Synchronized method - thread-safe public synchronized void increment() { count++; }
public synchronized void decrement() { count--; }
public synchronized int getCount() { return count; }}Synchronized Blocks
class BankAccount { private double balance = 0; private final Object lock = new Object();
public void deposit(double amount) { synchronized (lock) { balance += amount; System.out.println("Deposited: " + amount + ", Balance: " + balance); } }
public void withdraw(double amount) { synchronized (lock) { if (balance >= amount) { balance -= amount; System.out.println("Withdrawn: " + amount + ", Balance: " + balance); } else { System.out.println("Insufficient funds"); } } }
public double getBalance() { synchronized (lock) { return balance; } }}Static Synchronization
class DatabaseConnection { private static int connectionCount = 0;
// Synchronized on class object public static synchronized void incrementConnections() { connectionCount++; }
// Equivalent to above public static void incrementConnectionsAlt() { synchronized (DatabaseConnection.class) { connectionCount++; } }}4. Deadlock in Multithreading
Deadlock is a situation where two or more threads are blocked forever, waiting for each other to release resources.
Deadlock Example
public class DeadlockExample { private static final Object lock1 = new Object(); private static final Object lock2 = new Object();
public static void main(String[] args) { Thread thread1 = new Thread(() -> { synchronized (lock1) { System.out.println("Thread 1: Acquired lock1");
try { Thread.sleep(100); } catch (InterruptedException e) {}
System.out.println("Thread 1: Waiting for lock2..."); synchronized (lock2) { System.out.println("Thread 1: Acquired lock2"); } } });
Thread thread2 = new Thread(() -> { synchronized (lock2) { System.out.println("Thread 2: Acquired lock2");
try { Thread.sleep(100); } catch (InterruptedException e) {}
System.out.println("Thread 2: Waiting for lock1..."); synchronized (lock1) { System.out.println("Thread 2: Acquired lock1"); } } });
thread1.start(); thread2.start();
// This will result in deadlock! }}Coffman Conditions for Deadlock
All four conditions must be present simultaneously:
- Mutual Exclusion: Resources cannot be shared
- Hold and Wait: A thread holds at least one resource while waiting for others
- No Preemption: Resources cannot be forcibly taken from threads
- Circular Wait: A circular chain of threads waiting for each other’s resources
5. Avoiding Deadlocks
Strategy 1: Lock Ordering
public class LockOrderingSolution { private static final Object lock1 = new Object(); private static final Object lock2 = new Object();
public void method1() { synchronized (lock1) { // Always acquire lock1 first synchronized (lock2) { // Critical section } } }
public void method2() { synchronized (lock1) { // Same order: lock1 then lock2 synchronized (lock2) { // Critical section } } }}Strategy 2: Timeout-Based Locking
import java.util.concurrent.locks.*;import java.util.concurrent.TimeUnit;
public class TimeoutLockingSolution { private final Lock lock1 = new ReentrantLock(); private final Lock lock2 = new ReentrantLock();
public void transfer() throws InterruptedException { while (true) { boolean gotLock1 = lock1.tryLock(100, TimeUnit.MILLISECONDS); if (!gotLock1) { continue; }
try { boolean gotLock2 = lock2.tryLock(100, TimeUnit.MILLISECONDS); if (!gotLock2) { continue; }
try { // Perform transfer System.out.println("Transfer successful"); break; } finally { lock2.unlock(); } } finally { lock1.unlock(); } } }}Strategy 3: Single Lock Strategy
public class SingleLockSolution { private final Object lock = new Object(); private int resource1 = 0; private int resource2 = 0;
public void operation1() { synchronized (lock) { resource1++; resource2++; } }
public void operation2() { synchronized (lock) { resource2--; resource1--; } }}Strategy 4: Lock-Free Programming
import java.util.concurrent.atomic.*;
public class LockFreeSolution { private AtomicInteger counter = new AtomicInteger(0);
public void increment() { counter.incrementAndGet(); // Atomic operation }
public int getCount() { return counter.get(); }}6. The volatile Keyword
The volatile keyword ensures visibility and ordering of variables across multiple threads.
Visibility Problem
class VisibilityExample { private boolean flag = false; // Without volatile
public void writer() { flag = true; // Thread 1 sets flag }
public void reader() { while (!flag) { // Thread 2 might never see the change! // This could be an infinite loop } System.out.println("Flag is now true!"); }}Solution with volatile
class VolatileVisibilityExample { private volatile boolean flag = false; // With volatile
public void writer() { flag = true; // Change immediately visible to all threads }
public void reader() { while (!flag) { // Thread will see the change when it happens } System.out.println("Flag is now true!"); }}When to Use volatile
public class VolatileUseCase { // Good use case: Simple flag private volatile boolean running = true;
public void stop() { running = false; }
public void run() { while (running) { // Do work } }
// Bad use case: Counter (use AtomicInteger instead) private volatile int counter = 0; // NOT thread-safe for increment
public void increment() { counter++; // Still not atomic! }}volatile vs synchronized
public class VolatileVsSynchronized { // volatile: Only guarantees visibility private volatile int value = 0;
public void incrementVolatile() { value++; // NOT thread-safe (read-modify-write) }
// synchronized: Guarantees both visibility and atomicity public synchronized void incrementSync() { value++; // Thread-safe }}7. Wait and Notify Mechanism
The wait() and notify() mechanism provides inter-thread communication.
Basic Wait-Notify Pattern
class WaitNotifyExample { private boolean condition = false; private final Object lock = new Object();
// Waiting thread public void waitingMethod() throws InterruptedException { synchronized (lock) { while (!condition) { // Always use while loop, not if System.out.println("Waiting for condition..."); lock.wait(); // Releases the lock and waits System.out.println("Woke up, checking condition again..."); } System.out.println("Condition is true, proceeding..."); } }
// Notifying thread public void notifyingMethod() { synchronized (lock) { condition = true; System.out.println("Condition set to true, notifying..."); lock.notify(); // or lock.notifyAll(); } }}Producer-Consumer Pattern
import java.util.*;
class ProducerConsumer { private Queue<Integer> queue = new LinkedList<>(); private final int CAPACITY = 5; private final Object lock = new Object();
public void produce() throws InterruptedException { int value = 0; while (true) { synchronized (lock) { while (queue.size() == CAPACITY) { System.out.println("Queue full, producer waiting..."); lock.wait(); }
queue.add(value); System.out.println("Produced: " + value); value++;
lock.notifyAll(); // Wake up consumers Thread.sleep(1000); } } }
public void consume() throws InterruptedException { while (true) { synchronized (lock) { while (queue.isEmpty()) { System.out.println("Queue empty, consumer waiting..."); lock.wait(); }
int value = queue.poll(); System.out.println("Consumed: " + value);
lock.notifyAll(); // Wake up producers Thread.sleep(1000); } } }}Wait-Notify Best Practices
public class WaitNotifyBestPractices { private final Object lock = new Object(); private boolean condition = false;
public void correctWaitPattern() throws InterruptedException { synchronized (lock) { // ALWAYS use while loop, not if while (!condition) { lock.wait(); } // Process when condition is true } }
public void incorrectWaitPattern() throws InterruptedException { synchronized (lock) { // WRONG: Using if instead of while if (!condition) { lock.wait(); } // Might process even if condition is false (spurious wakeup) } }
public void notifyPattern() { synchronized (lock) { condition = true;
// Use notify() if only one thread needs to wake up lock.notify();
// Use notifyAll() if multiple threads might be waiting // lock.notifyAll(); } }}8. Executors in Java Concurrency
Executors provide a high-level abstraction for managing and controlling thread execution.
Core Executor Interfaces
import java.util.concurrent.*;
// Basic executor interfacepublic interface Executor { void execute(Runnable command);}
// Extended interface with lifecycle managementpublic interface ExecutorService extends Executor { void shutdown(); List<Runnable> shutdownNow(); boolean isShutdown(); boolean isTerminated(); <T> Future<T> submit(Callable<T> task); Future<?> submit(Runnable task);}
// Scheduling capabilitiespublic interface ScheduledExecutorService extends ExecutorService { ScheduledFuture<?> schedule(Runnable command, long delay, TimeUnit unit); ScheduledFuture<?> scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit);}Creating Executors
import java.util.concurrent.*;
public class ExecutorExamples {
public void demonstrateExecutors() { // 1. Single thread executor ExecutorService singleExecutor = Executors.newSingleThreadExecutor();
// 2. Fixed thread pool ExecutorService fixedPool = Executors.newFixedThreadPool(5);
// 3. Cached thread pool (creates threads as needed) ExecutorService cachedPool = Executors.newCachedThreadPool();
// 4. Scheduled executor ScheduledExecutorService scheduledPool = Executors.newScheduledThreadPool(3);
// 5. Work stealing pool (Java 8+) ExecutorService workStealingPool = Executors.newWorkStealingPool(); }}Using ExecutorService
import java.util.concurrent.*;import java.util.*;
public class ExecutorServiceExample {
public static void main(String[] args) throws Exception { ExecutorService executor = Executors.newFixedThreadPool(3);
// Submit Runnable tasks executor.submit(() -> { System.out.println("Task 1 executing on: " + Thread.currentThread().getName()); });
// Submit Callable task Future<Integer> future = executor.submit(() -> { Thread.sleep(1000); return 42; });
System.out.println("Result: " + future.get());
// Submit multiple tasks List<Callable<String>> tasks = Arrays.asList( () -> "Task 1 result", () -> "Task 2 result", () -> "Task 3 result" );
List<Future<String>> results = executor.invokeAll(tasks);
for (Future<String> result : results) { System.out.println(result.get()); }
// Shutdown executor executor.shutdown(); executor.awaitTermination(1, TimeUnit.MINUTES); }}Scheduled Executor
import java.util.concurrent.*;
public class ScheduledExecutorExample {
public static void main(String[] args) { ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(2);
// Schedule one-time task with delay scheduler.schedule(() -> { System.out.println("Task executed after 3 seconds"); }, 3, TimeUnit.SECONDS);
// Schedule recurring task at fixed rate scheduler.scheduleAtFixedRate(() -> { System.out.println("Recurring task: " + System.currentTimeMillis()); }, 0, 2, TimeUnit.SECONDS);
// Schedule with fixed delay between executions scheduler.scheduleWithFixedDelay(() -> { System.out.println("Task with fixed delay"); }, 1, 3, TimeUnit.SECONDS);
// Shutdown after 10 seconds scheduler.schedule(() -> { scheduler.shutdown(); }, 10, TimeUnit.SECONDS); }}Thread Pool Configuration
import java.util.concurrent.*;
public class CustomThreadPoolExample {
public static void main(String[] args) { // Custom thread pool with all parameters ThreadPoolExecutor executor = new ThreadPoolExecutor( 5, // core pool size 10, // maximum pool size 60, // keep alive time TimeUnit.SECONDS, // time unit new LinkedBlockingQueue<>(100), // work queue new ThreadPoolExecutor.CallerRunsPolicy() // rejection policy );
// Configure additional settings executor.allowCoreThreadTimeOut(true); executor.prestartAllCoreThreads();
// Use the executor for (int i = 0; i < 20; i++) { final int taskId = i; executor.submit(() -> { System.out.println("Task " + taskId + " on " + Thread.currentThread().getName()); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } }); }
executor.shutdown(); }}Best Practices
- Always shutdown executors to avoid resource leaks
- Use appropriate pool size based on CPU-bound vs I/O-bound tasks
- Handle rejected tasks with proper rejection policies
- Monitor thread pools for performance tuning
- Use
invokeAll()orinvokeAny()for batch operations - Prefer ExecutorService over manual thread creation
- Use try-with-resources or ensure proper cleanup
- Set meaningful thread names for easier debugging
// Proper executor cleanupExecutorService executor = Executors.newFixedThreadPool(5);try { // Submit tasks executor.submit(() -> System.out.println("Task"));} finally { executor.shutdown(); try { if (!executor.awaitTermination(60, TimeUnit.SECONDS)) { executor.shutdownNow(); } } catch (InterruptedException e) { executor.shutdownNow(); Thread.currentThread().interrupt(); }}