Java Threads and Concurrent Programming
Java Threads: Mastering Concurrent Programming
Threads represent one of the most fundamental yet challenging aspects of modern Java development. As applications have evolved from simple, sequential programs to complex, interactive systems that must handle multiple users, process real-time data, and maintain responsiveness under load, understanding concurrent programming has become essential for every Java developer.
The Evolution from Sequential to Concurrent Programming
In the early days of computing, programs executed sequentially—one instruction after another in a predictable order. This model worked well for simple batch processing systems, but as user expectations grew and applications became more complex, developers needed ways to handle multiple tasks simultaneously.
Why Traditional Sequential Programming Falls Short:
User Experience: A single-threaded application that makes a database call or processes a large file would freeze the entire user interface, creating an unacceptable user experience.
Resource Utilization: Modern computers have multiple CPU cores, but sequential programs can only use one core at a time, leaving significant processing power unused.
I/O Bottlenecks: Applications often wait for disk reads, network calls, or database queries. During these waiting periods, the CPU sits idle instead of processing other tasks.
Scalability Limits: Web applications serving multiple users simultaneously cannot efficiently handle concurrent requests with a single-threaded approach.
Threads represent one of the most fundamental yet challenging aspects of modern Java development. Understanding threads is essential for building responsive, high-performance applications that can handle multiple tasks simultaneously. Java provides robust built-in support for multithreading through the Thread class and related concurrency utilities.
What are Threads?
A thread is an independent execution path within a program that allows multiple sequences of instructions to run concurrently within the same application. To understand threads, it's helpful to first understand the relationship between processes and threads in modern operating systems.
Processes vs. Threads: Understanding the Hierarchy
Processes: Think of a process as a complete program running on your operating system. Each Java application you start creates a new process. Processes are isolated from each other—they have separate memory spaces and cannot directly access each other's data.
Threads: Within each process, you can have multiple threads. These are like mini-programs that share the same memory space but can execute different tasks simultaneously. All threads within a process can access the same variables, objects, and resources.
Real-World Analogy: Imagine a restaurant (process) with multiple chefs (threads) working in the same kitchen. They share the same ingredients and equipment (memory), but each chef can prepare different dishes (tasks) simultaneously. If one chef waits for the oven to heat up, the others can continue chopping vegetables or preparing sauces.
Thread Fundamentals: How Concurrency Works
Concurrent vs. Parallel Execution:
- Concurrency: Multiple threads making progress by taking turns using the CPU (time-slicing)
- Parallelism: Multiple threads actually executing simultaneously on different CPU cores
Thread Lifecycle: Every thread goes through distinct states during its lifetime:
- NEW: Thread created but not yet started
- RUNNABLE: Thread is executing or ready to execute
- BLOCKED: Thread waiting for a monitor lock
- WAITING: Thread waiting indefinitely for another thread
- TIMED_WAITING: Thread waiting for a specified period
- TERMINATED: Thread has completed execution
Key Thread Characteristics:
1. Shared Memory Space: All threads within a process access the same heap memory, making data sharing efficient but requiring careful synchronization.
2. Independent Execution Stacks: Each thread has its own call stack, local variables, and program counter, allowing independent execution paths.
3. Lightweight Creation: Creating threads requires less system overhead than creating new processes.
4. Communication Mechanisms: Threads can communicate through shared objects, wait/notify mechanisms, and concurrent data structures.
5. Resource Sharing: Threads share file handles, network connections, and other system resources owned by the process.
Why Threads Matter in Modern Applications:
Responsiveness: Keep user interfaces responsive while performing background operations like file downloads or data processing.
Performance: Utilize multiple CPU cores effectively by distributing work across threads, leading to significant performance improvements on multi-core systems.
I/O Efficiency: While one thread waits for disk reads or network responses, other threads can continue processing, maximizing overall application throughput.
Modular Design: Separate different concerns (UI updates, data processing, network communication) into independent threads for cleaner architecture.
Scalability: Handle multiple concurrent users or requests efficiently, essential for server applications and web services.
Demonstrating the Power of Concurrent Execution
Let's examine a practical example that illustrates the dramatic difference between sequential and concurrent execution:
public class ThreadBasics {
// Without threads - sequential execution
public static void sequentialExample() {
System.out.println("=== Sequential Execution ===");
long startTime = System.currentTimeMillis();
// Task 1: Simulate heavy computation
heavyComputation("Task 1");
// Task 2: Simulate another heavy computation
heavyComputation("Task 2");
long endTime = System.currentTimeMillis();
System.out.println("Total time: " + (endTime - startTime) + "ms\n");
}
// With threads - concurrent execution
public static void concurrentExample() throws InterruptedException {
System.out.println("=== Concurrent Execution ===");
long startTime = System.currentTimeMillis();
// Create threads for parallel execution
Thread thread1 = new Thread(() -> heavyComputation("Task 1"));
Thread thread2 = new Thread(() -> heavyComputation("Task 2"));
// Start both threads
thread1.start();
thread2.start();
// Wait for both to complete
thread1.join();
thread2.join();
long endTime = System.currentTimeMillis();
System.out.println("Total time: " + (endTime - startTime) + "ms\n");
}
private static void heavyComputation(String taskName) {
System.out.println(taskName + " started by " + Thread.currentThread().getName());
// Simulate heavy work
try {
Thread.sleep(2000); // 2 seconds
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
System.out.println(taskName + " completed by " + Thread.currentThread().getName());
}
public static void main(String[] args) throws InterruptedException {
sequentialExample();
concurrentExample();
}
}
Understanding the Performance Impact:
Sequential Execution Analysis:
- Task 1 runs first and takes 2 seconds to complete
- Only after Task 1 finishes does Task 2 start
- Task 2 takes another 2 seconds to complete
- Total execution time: approximately 4 seconds
- Only one CPU core is utilized at a time
Concurrent Execution Analysis:
- Both Task 1 and Task 2 start simultaneously
- Each task still takes 2 seconds, but they run in parallel
- Total execution time: approximately 2 seconds (50% reduction!)
- Multiple CPU cores can be utilized simultaneously
Key Threading Concepts Demonstrated:
Thread Creation: new Thread(() -> heavyComputation("Task 1"))
creates a new thread with a lambda expression defining the task to execute.
Thread Starting: thread1.start()
actually begins thread execution. Never call run()
directly—this would execute the task in the current thread, defeating the purpose.
Thread Joining: thread1.join()
makes the main thread wait until thread1
completes. Without this, the main thread would finish before the worker threads, and the timing measurement would be inaccurate.
Thread Names: Each thread has a unique name (like "Thread-0", "Thread-1") that helps identify which thread is executing which task.
Real-World Applications: This pattern applies to scenarios like:
- Processing multiple files simultaneously
- Making concurrent API calls
- Handling multiple user requests in web applications
- Performing parallel calculations on large datasets
Creating Threads
Java provides several ways to create and start threads:
1. Extending Thread Class
public class ExtendingThreadExample {
// Method 1: Extend Thread class
static class MyThread extends Thread {
private final String taskName;
public MyThread(String taskName) {
this.taskName = taskName;
}
@Override
public void run() {
for (int i = 1; i <= 5; i++) {
System.out.println(taskName + " - Step " + i + " [" +
Thread.currentThread().getName() + "]");
try {
Thread.sleep(1000); // Pause for 1 second
} catch (InterruptedException e) {
System.out.println(taskName + " was interrupted");
return;
}
}
System.out.println(taskName + " completed!");
}
}
public static void main(String[] args) throws InterruptedException {
System.out.println("=== Extending Thread Class ===");
// Create thread instances
MyThread thread1 = new MyThread("Download Task");
MyThread thread2 = new MyThread("Upload Task");
// Start threads
thread1.start();
thread2.start();
// Wait for completion
thread1.join();
thread2.join();
System.out.println("All tasks completed!");
}
}
2. Implementing Runnable Interface
public class RunnableInterfaceExample {
// Method 2: Implement Runnable interface (preferred)
static class MyTask implements Runnable {
private final String taskName;
private final int iterations;
public MyTask(String taskName, int iterations) {
this.taskName = taskName;
this.iterations = iterations;
}
@Override
public void run() {
for (int i = 1; i <= iterations; i++) {
System.out.println(taskName + " - Iteration " + i + " [" +
Thread.currentThread().getName() + "]");
try {
Thread.sleep(500);
} catch (InterruptedException e) {
System.out.println(taskName + " was interrupted");
Thread.currentThread().interrupt();
return;
}
}
System.out.println(taskName + " finished!");
}
}
// Using lambda expressions for simple tasks
public static void demonstrateLambdaThreads() throws InterruptedException {
System.out.println("\n=== Using Lambda Expressions ===");
// Simple lambda thread
Thread lambdaThread = new Thread(() -> {
String threadName = Thread.currentThread().getName();
for (int i = 1; i <= 3; i++) {
System.out.println("Lambda task - Step " + i + " [" + threadName + "]");
try {
Thread.sleep(800);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return;
}
}
});
lambdaThread.setName("LambdaWorker");
lambdaThread.start();
lambdaThread.join();
}
public static void main(String[] args) throws InterruptedException {
System.out.println("=== Implementing Runnable Interface ===");
// Create runnable tasks
MyTask task1 = new MyTask("Data Processing", 4);
MyTask task2 = new MyTask("File Backup", 3);
// Create threads with runnable tasks
Thread thread1 = new Thread(task1);
Thread thread2 = new Thread(task2);
// Set thread names
thread1.setName("ProcessorThread");
thread2.setName("BackupThread");
// Start threads
thread1.start();
thread2.start();
// Wait for completion
thread1.join();
thread2.join();
demonstrateLambdaThreads();
System.out.println("All tasks completed!");
}
}
3. Using Callable Interface
import java.util.concurrent.*;
import java.util.*;
public class CallableExample {
// Callable allows returning values and throwing checked exceptions
static class CalculationTask implements Callable<Integer> {
private final int start;
private final int end;
public CalculationTask(int start, int end) {
this.start = start;
this.end = end;
}
@Override
public Integer call() throws Exception {
String threadName = Thread.currentThread().getName();
System.out.println("Calculating sum from " + start + " to " + end +
" [" + threadName + "]");
int sum = 0;
for (int i = start; i <= end; i++) {
sum += i;
Thread.sleep(10); // Simulate work
}
System.out.println("Calculation completed [" + threadName + "]");
return sum;
}
}
public static void main(String[] args) {
System.out.println("=== Using Callable Interface ===");
ExecutorService executor = Executors.newFixedThreadPool(3);
try {
// Create callable tasks
List<Callable<Integer>> tasks = Arrays.asList(
new CalculationTask(1, 100),
new CalculationTask(101, 200),
new CalculationTask(201, 300)
);
// Submit tasks and get futures
List<Future<Integer>> futures = executor.invokeAll(tasks);
// Collect results
int totalSum = 0;
for (int i = 0; i < futures.size(); i++) {
try {
Integer result = futures.get(i).get();
System.out.println("Task " + (i + 1) + " result: " + result);
totalSum += result;
} catch (ExecutionException e) {
System.err.println("Task " + (i + 1) + " failed: " + e.getCause());
}
}
System.out.println("Total sum: " + totalSum);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
System.err.println("Tasks were interrupted");
} finally {
executor.shutdown();
}
}
}
Thread Lifecycle and States
Understanding thread states is crucial for effective thread management:
public class ThreadLifecycleExample {
static class LifecycleThread extends Thread {
private final Object lock = new Object();
private volatile boolean shouldWait = false;
@Override
public void run() {
System.out.println("Thread entered RUNNABLE state");
try {
// Simulate some work
Thread.sleep(1000);
System.out.println("Thread woke up from TIMED_WAITING");
// Demonstrate WAITING state
synchronized (lock) {
shouldWait = true;
System.out.println("Thread entering WAITING state");
lock.wait(); // Thread enters WAITING state
System.out.println("Thread resumed from WAITING state");
}
// More work
Thread.sleep(500);
System.out.println("Thread finishing execution");
} catch (InterruptedException e) {
System.out.println("Thread was interrupted");
Thread.currentThread().interrupt();
}
}
public void wakeUp() {
synchronized (lock) {
lock.notify();
}
}
public boolean isWaiting() {
return shouldWait && getState() == State.WAITING;
}
}
public static void demonstrateThreadStates() throws InterruptedException {
System.out.println("=== Thread Lifecycle Demonstration ===");
LifecycleThread thread = new LifecycleThread();
// NEW state
System.out.println("1. Thread state: " + thread.getState()); // NEW
// Start thread - moves to RUNNABLE
thread.start();
Thread.sleep(100); // Give thread time to start
System.out.println("2. Thread state: " + thread.getState()); // RUNNABLE
// Wait for thread to enter TIMED_WAITING
Thread.sleep(600);
System.out.println("3. Thread state: " + thread.getState()); // TIMED_WAITING
// Wait for thread to enter WAITING state
while (!thread.isWaiting()) {
Thread.sleep(100);
}
System.out.println("4. Thread state: " + thread.getState()); // WAITING
// Wake up the waiting thread
Thread.sleep(1000);
thread.wakeUp();
// Wait for thread to complete
thread.join();
System.out.println("5. Thread state: " + thread.getState()); // TERMINATED
System.out.println("Thread lifecycle demonstration completed");
}
public static void demonstrateBlockedState() throws InterruptedException {
System.out.println("\n=== BLOCKED State Demonstration ===");
Object sharedLock = new Object();
Thread thread1 = new Thread(() -> {
synchronized (sharedLock) {
System.out.println("Thread1 acquired lock");
try {
Thread.sleep(3000); // Hold lock for 3 seconds
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
System.out.println("Thread1 releasing lock");
}
}, "Thread1");
Thread thread2 = new Thread(() -> {
System.out.println("Thread2 attempting to acquire lock");
synchronized (sharedLock) {
System.out.println("Thread2 acquired lock");
}
}, "Thread2");
thread1.start();
Thread.sleep(100); // Let thread1 acquire lock first
thread2.start();
Thread.sleep(100); // Let thread2 attempt to acquire lock
System.out.println("Thread2 state: " + thread2.getState()); // BLOCKED
thread1.join();
thread2.join();
System.out.println("BLOCKED state demonstration completed");
}
public static void main(String[] args) throws InterruptedException {
demonstrateThreadStates();
demonstrateBlockedState();
}
}
Thread Properties and Control
Threads have various properties that can be configured and controlled:
public class ThreadPropertiesExample {
static class WorkerThread extends Thread {
private final String taskName;
public WorkerThread(String taskName) {
this.taskName = taskName;
}
@Override
public void run() {
Thread currentThread = Thread.currentThread();
System.out.println("\n=== Thread Properties ===");
System.out.println("Task: " + taskName);
System.out.println("Thread Name: " + currentThread.getName());
System.out.println("Thread ID: " + currentThread.getId());
System.out.println("Priority: " + currentThread.getPriority());
System.out.println("Is Daemon: " + currentThread.isDaemon());
System.out.println("Thread Group: " + currentThread.getThreadGroup().getName());
System.out.println("Is Alive: " + currentThread.isAlive());
// Simulate work
for (int i = 1; i <= 3; i++) {
System.out.println(taskName + " - Working... " + i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
System.out.println(taskName + " was interrupted");
return;
}
}
System.out.println(taskName + " completed!");
}
}
static class DaemonThread extends Thread {
@Override
public void run() {
int count = 0;
try {
while (!Thread.currentThread().isInterrupted()) {
count++;
System.out.println("Daemon thread running... " + count);
Thread.sleep(1000);
}
} catch (InterruptedException e) {
System.out.println("Daemon thread interrupted");
}
System.out.println("Daemon thread ending");
}
}
public static void demonstrateThreadProperties() throws InterruptedException {
System.out.println("=== Thread Properties Demonstration ===");
// Create worker threads with different properties
WorkerThread worker1 = new WorkerThread("HighPriorityTask");
WorkerThread worker2 = new WorkerThread("LowPriorityTask");
// Set thread names
worker1.setName("HighPriorityWorker");
worker2.setName("LowPriorityWorker");
// Set thread priorities
worker1.setPriority(Thread.MAX_PRIORITY); // 10
worker2.setPriority(Thread.MIN_PRIORITY); // 1
// Start threads
worker1.start();
worker2.start();
// Wait for completion
worker1.join();
worker2.join();
}
public static void demonstrateDaemonThreads() throws InterruptedException {
System.out.println("\n=== Daemon Thread Demonstration ===");
DaemonThread daemonThread = new DaemonThread();
daemonThread.setName("BackgroundDaemon");
daemonThread.setDaemon(true); // Mark as daemon thread
System.out.println("Starting daemon thread...");
daemonThread.start();
// Main thread does some work
for (int i = 1; i <= 3; i++) {
System.out.println("Main thread working... " + i);
Thread.sleep(1500);
}
System.out.println("Main thread ending - daemon will automatically terminate");
// JVM will exit and daemon thread will be terminated automatically
}
public static void demonstrateThreadInterruption() throws InterruptedException {
System.out.println("\n=== Thread Interruption Demonstration ===");
Thread longRunningTask = new Thread(() -> {
try {
for (int i = 1; i <= 10; i++) {
System.out.println("Long running task - iteration " + i);
Thread.sleep(1000);
// Check for interruption
if (Thread.currentThread().isInterrupted()) {
System.out.println("Task detected interruption signal");
return;
}
}
} catch (InterruptedException e) {
System.out.println("Task was interrupted during sleep");
Thread.currentThread().interrupt(); // Restore interrupt status
}
});
longRunningTask.setName("LongRunningTask");
longRunningTask.start();
// Let it run for a bit
Thread.sleep(3500);
// Interrupt the thread
System.out.println("Interrupting long running task...");
longRunningTask.interrupt();
// Wait for it to finish
longRunningTask.join();
System.out.println("Thread interruption demonstration completed");
}
public static void main(String[] args) throws InterruptedException {
demonstrateThreadProperties();
demonstrateDaemonThreads();
demonstrateThreadInterruption();
}
}
Thread Communication and Coordination
Threads often need to communicate and coordinate their activities:
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.CyclicBarrier;
import java.util.concurrent.Semaphore;
public class ThreadCommunicationExample {
// Producer-Consumer pattern using wait/notify
static class ProducerConsumerExample {
private final Object lock = new Object();
private int data = 0;
private boolean dataReady = false;
class Producer extends Thread {
@Override
public void run() {
for (int i = 1; i <= 5; i++) {
synchronized (lock) {
data = i * 10;
dataReady = true;
System.out.println("Producer: Data produced = " + data);
lock.notify(); // Wake up waiting consumer
}
try {
Thread.sleep(1000); // Simulate production time
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
break;
}
}
}
}
class Consumer extends Thread {
@Override
public void run() {
for (int i = 1; i <= 5; i++) {
synchronized (lock) {
while (!dataReady) {
try {
System.out.println("Consumer: Waiting for data...");
lock.wait(); // Wait for data to be ready
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return;
}
}
System.out.println("Consumer: Data consumed = " + data);
dataReady = false; // Mark data as consumed
}
}
}
}
public void demonstrate() throws InterruptedException {
System.out.println("=== Producer-Consumer Example ===");
Producer producer = new Producer();
Consumer consumer = new Consumer();
consumer.start();
producer.start();
producer.join();
consumer.join();
System.out.println("Producer-Consumer demonstration completed\n");
}
}
// CountDownLatch example
static class CountDownLatchExample {
public void demonstrate() throws InterruptedException {
System.out.println("=== CountDownLatch Example ===");
int numWorkers = 3;
CountDownLatch startSignal = new CountDownLatch(1);
CountDownLatch doneSignal = new CountDownLatch(numWorkers);
// Create worker threads
for (int i = 1; i <= numWorkers; i++) {
new Thread(new Worker(i, startSignal, doneSignal)).start();
}
System.out.println("All workers created, starting work...");
startSignal.countDown(); // Release all workers
doneSignal.await(); // Wait for all workers to complete
System.out.println("All workers completed!\n");
}
class Worker implements Runnable {
private final int workerId;
private final CountDownLatch startSignal;
private final CountDownLatch doneSignal;
Worker(int workerId, CountDownLatch startSignal, CountDownLatch doneSignal) {
this.workerId = workerId;
this.startSignal = startSignal;
this.doneSignal = doneSignal;
}
@Override
public void run() {
try {
startSignal.await(); // Wait for start signal
System.out.println("Worker " + workerId + " started");
Thread.sleep(1000 + workerId * 500); // Simulate work
System.out.println("Worker " + workerId + " completed");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
doneSignal.countDown(); // Signal completion
}
}
}
}
// CyclicBarrier example
static class CyclicBarrierExample {
public void demonstrate() throws InterruptedException {
System.out.println("=== CyclicBarrier Example ===");
int numParticipants = 3;
CyclicBarrier barrier = new CyclicBarrier(numParticipants, () -> {
System.out.println("*** All participants reached barrier - proceeding! ***");
});
// Create participant threads
for (int i = 1; i <= numParticipants; i++) {
new Thread(new Participant(i, barrier)).start();
}
Thread.sleep(5000); // Let demonstration complete
System.out.println("CyclicBarrier demonstration completed\n");
}
class Participant implements Runnable {
private final int participantId;
private final CyclicBarrier barrier;
Participant(int participantId, CyclicBarrier barrier) {
this.participantId = participantId;
this.barrier = barrier;
}
@Override
public void run() {
try {
// Phase 1
System.out.println("Participant " + participantId + " working on phase 1");
Thread.sleep(1000 + participantId * 300);
System.out.println("Participant " + participantId + " completed phase 1");
barrier.await(); // Wait for all to complete phase 1
// Phase 2
System.out.println("Participant " + participantId + " working on phase 2");
Thread.sleep(800 + participantId * 200);
System.out.println("Participant " + participantId + " completed phase 2");
} catch (Exception e) {
Thread.currentThread().interrupt();
}
}
}
}
// Semaphore example
static class SemaphoreExample {
public void demonstrate() throws InterruptedException {
System.out.println("=== Semaphore Example ===");
Semaphore semaphore = new Semaphore(2); // Allow 2 concurrent accesses
// Create multiple threads trying to access limited resource
for (int i = 1; i <= 5; i++) {
new Thread(new ResourceUser(i, semaphore)).start();
}
Thread.sleep(8000); // Let demonstration complete
System.out.println("Semaphore demonstration completed\n");
}
class ResourceUser implements Runnable {
private final int userId;
private final Semaphore semaphore;
ResourceUser(int userId, Semaphore semaphore) {
this.userId = userId;
this.semaphore = semaphore;
}
@Override
public void run() {
try {
System.out.println("User " + userId + " requesting access...");
semaphore.acquire(); // Acquire permit
System.out.println("User " + userId + " accessing limited resource");
Thread.sleep(2000); // Use resource
System.out.println("User " + userId + " finished using resource");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
semaphore.release(); // Release permit
System.out.println("User " + userId + " released access");
}
}
}
}
public static void main(String[] args) throws InterruptedException {
new ProducerConsumerExample().demonstrate();
new CountDownLatchExample().demonstrate();
new CyclicBarrierExample().demonstrate();
new SemaphoreExample().demonstrate();
}
}
Thread Safety and Best Practices
import java.util.concurrent.atomic.AtomicInteger;
public class ThreadSafetyExample {
// Unsafe counter
static class UnsafeCounter {
private int count = 0;
public void increment() {
count++; // Not thread-safe!
}
public int getCount() {
return count;
}
}
// Safe counter using synchronization
static class SafeCounter {
private int count = 0;
public synchronized void increment() {
count++; // Thread-safe
}
public synchronized int getCount() {
return count;
}
}
// Atomic counter
static class AtomicCounter {
private final AtomicInteger count = new AtomicInteger(0);
public void increment() {
count.incrementAndGet(); // Thread-safe and lock-free
}
public int getCount() {
return count.get();
}
}
public static void demonstrateThreadSafety() throws InterruptedException {
System.out.println("=== Thread Safety Demonstration ===");
int numThreads = 10;
int incrementsPerThread = 1000;
int expectedTotal = numThreads * incrementsPerThread;
// Test unsafe counter
UnsafeCounter unsafeCounter = new UnsafeCounter();
Thread[] unsafeThreads = new Thread[numThreads];
for (int i = 0; i < numThreads; i++) {
unsafeThreads[i] = new Thread(() -> {
for (int j = 0; j < incrementsPerThread; j++) {
unsafeCounter.increment();
}
});
}
long startTime = System.currentTimeMillis();
for (Thread thread : unsafeThreads) {
thread.start();
}
for (Thread thread : unsafeThreads) {
thread.join();
}
long unsafeTime = System.currentTimeMillis() - startTime;
System.out.println("Unsafe counter result: " + unsafeCounter.getCount() +
" (expected: " + expectedTotal + ") Time: " + unsafeTime + "ms");
// Test safe counter
SafeCounter safeCounter = new SafeCounter();
Thread[] safeThreads = new Thread[numThreads];
for (int i = 0; i < numThreads; i++) {
safeThreads[i] = new Thread(() -> {
for (int j = 0; j < incrementsPerThread; j++) {
safeCounter.increment();
}
});
}
startTime = System.currentTimeMillis();
for (Thread thread : safeThreads) {
thread.start();
}
for (Thread thread : safeThreads) {
thread.join();
}
long safeTime = System.currentTimeMillis() - startTime;
System.out.println("Safe counter result: " + safeCounter.getCount() +
" (expected: " + expectedTotal + ") Time: " + safeTime + "ms");
// Test atomic counter
AtomicCounter atomicCounter = new AtomicCounter();
Thread[] atomicThreads = new Thread[numThreads];
for (int i = 0; i < numThreads; i++) {
atomicThreads[i] = new Thread(() -> {
for (int j = 0; j < incrementsPerThread; j++) {
atomicCounter.increment();
}
});
}
startTime = System.currentTimeMillis();
for (Thread thread : atomicThreads) {
thread.start();
}
for (Thread thread : atomicThreads) {
thread.join();
}
long atomicTime = System.currentTimeMillis() - startTime;
System.out.println("Atomic counter result: " + atomicCounter.getCount() +
" (expected: " + expectedTotal + ") Time: " + atomicTime + "ms");
}
public static void threadSafetyBestPractices() {
System.out.println("\n=== Thread Safety Best Practices ===");
System.out.println("1. Use immutable objects when possible");
System.out.println("2. Synchronize access to shared mutable state");
System.out.println("3. Prefer atomic classes for simple operations");
System.out.println("4. Use thread-safe collections (ConcurrentHashMap, etc.)");
System.out.println("5. Minimize shared state between threads");
System.out.println("6. Use ThreadLocal for thread-specific data");
System.out.println("7. Avoid deadlocks by acquiring locks in consistent order");
System.out.println("8. Use higher-level concurrency utilities (ExecutorService, etc.)");
System.out.println("9. Handle InterruptedException properly");
System.out.println("10. Test thoroughly with multiple threads");
}
public static void main(String[] args) throws InterruptedException {
demonstrateThreadSafety();
threadSafetyBestPractices();
}
}
Summary
Java threads provide powerful concurrency capabilities:
Key Concepts:
- Thread Creation: Extend Thread, implement Runnable, or use Callable
- Thread States: NEW, RUNNABLE, BLOCKED, WAITING, TIMED_WAITING, TERMINATED
- Thread Properties: Name, priority, daemon status, thread groups
- Communication: wait/notify, CountDownLatch, CyclicBarrier, Semaphore
Best Practices:
- Prefer Runnable: More flexible than extending Thread class
- Use Thread Pools: Better resource management than creating threads directly
- Handle Interruption: Properly respond to interrupt signals
- Ensure Thread Safety: Synchronize access to shared mutable state
- Avoid Blocking: Use non-blocking alternatives when possible
- Clean Shutdown: Properly terminate threads and clean up resources
Common Patterns:
- Producer-Consumer: For data processing pipelines
- Worker Pool: For parallel task execution
- Master-Worker: For divide-and-conquer algorithms
- Pipeline: For multi-stage data processing
Performance Considerations:
- Context Switching: Overhead of switching between threads
- Memory Usage: Each thread requires its own stack space
- Synchronization Costs: Locks and atomic operations have overhead
- Cache Effects: False sharing can degrade performance
Understanding threads is fundamental for building responsive, scalable Java applications that can effectively utilize modern multi-core processors.