
Ever noticed how a web server handles many users at once without breaking a sweat? That’s thanks to concurrency. In Java, concurrency means multiple threads working in parallel so programs remain fast and responsive. In this guide, we will explore Java concurrency basics in a comprehensive manner so that you can understand and apply them in your real-world project efficiently and effectively.
What is Concurrency in Java?
Concurrency in Java is the ability to run multiple threads simultaneously within a program, like having multiple chefs in a kitchen handling different orders at the same time. Each thread operates independently, yet they all share the same kitchen resources – your CPU and memory.
Here’s the beautiful part: even with just one CPU core, Java creates the illusion of parallel execution by rapidly switching between threads. It’s like a master chef who can flip pancakes, stir soup, and plate desserts in what seems like the same instant.
But here’s what most tutorials won’t tell you upfront – concurrency isn’t just about speed. It’s about building applications that stay responsive under pressure, utilize modern multi-core processors effectively, and handle multiple tasks without breaking a sweat.
Concurrency vs Parallelism: The Critical Difference
This confusion trips up even experienced developers, so let me clear it up once and for all.
Concurrency is about dealing with multiple tasks at once – managing them, coordinating them, making progress on all of them. Think of it as juggling: you’re not throwing all balls simultaneously, but you’re managing multiple balls in the air.
Parallelism is about actually doing multiple things at the exact same time. This requires multiple CPU cores – like having multiple jugglers each handling their own set of balls.
In Java, you write concurrent code, and the JVM decides whether to run it in parallel based on available hardware. Smart, right?
Java Concurrency Basics: Threads
Let me share something that took me years to truly understand: threads are not just about performance – they’re about designing better programs.
Creating Threads: The Right Way
There are multiple ways to create threads in Java, but I’ll show you the approaches that actually matter in professional development:
// Method 1: Implementing Runnable (Recommended)
public class TaskRunner implements Runnable {
private final String taskName;
public TaskRunner(String taskName) {
this.taskName = taskName;
}
@Override
public void run() {
System.out.println("Executing task: " + taskName +
" on thread: " + Thread.currentThread().getName());
// Your actual task logic goes here
}
}
// Creating and starting the thread
Thread worker = new Thread(new TaskRunner("Database Backup"));
worker.start();
// Method 2: Using Lambda Expressions (Modern Java)
Thread modernWorker = new Thread(() -> {
System.out.println("Modern thread execution on: " +
Thread.currentThread().getName());
// Task logic here
});
modernWorker.start();
JavaWhy I recommend Runnable over extending Thread?
Implementing Runnable gives you flexibility. Your class can still extend another class if needed, and you’re following the composition-over-inheritance principle that makes code maintainable.
Thread Lifecycle and States
Understanding thread states isn’t academic fluff – it’s practical knowledge that will save you debugging hours:
- NEW: Thread created but not started
- RUNNABLE: Thread is executing or ready to execute
- BLOCKED: Thread is blocked, waiting for a monitor lock
- WAITING: Thread is waiting indefinitely for another thread
- TIMED_WAITING: Thread is waiting for a specified period
- TERMINATED: Thread has completed execution
Thread thread = new Thread(() -> {
try {
Thread.sleep(1000); // TIMED_WAITING state
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
System.out.println("Before start: " + thread.getState()); // NEW
thread.start();
System.out.println("After start: " + thread.getState()); // RUNNABLE
JavaUsing Executors and Thread Pools: The Professional Approach
Here’s where most developers level up their concurrency game. Raw thread creation is like manually managing memory in C – possible, but why would you when better tools exist?

Why Use Thread Pools?
Creating threads is expensive. Each thread consumes memory (typically 1MB for the stack), and constant thread creation/destruction kills performance. Thread pools solve this by reusing threads for multiple tasks.
Think of thread pools as hiring a permanent kitchen staff instead of hiring and firing chefs for every order. Much more efficient, right?
Simple ThreadPool Example
import java.util.concurrent.*;
import java.util.List;
import java.util.ArrayList;
public class ThreadPoolDemo {
public static void main(String[] args) {
// Create a fixed-size thread pool
ExecutorService executor = Executors.newFixedThreadPool(4);
// Submit multiple tasks
List<Future<String>> futures = new ArrayList<>();
for (int i = 1; i <= 10; i++) {
final int taskId = i;
Future<String> future = executor.submit(() -> {
// Simulate some work
Thread.sleep(1000);
return "Task " + taskId + " completed by " +
Thread.currentThread().getName();
});
futures.add(future);
}
// Collect results
for (Future<String> future : futures) {
try {
System.out.println(future.get());
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
}
// Always shut down the executor
executor.shutdown();
}
}
JavaTypes of Thread Pools: Choose Your Weapon
// Fixed Thread Pool - Best for CPU-bound tasks
ExecutorService fixedPool = Executors.newFixedThreadPool(4);
// Cached Thread Pool - Great for many short-lived tasks
ExecutorService cachedPool = Executors.newCachedThreadPool();
// Single Thread Executor - When order matters
ExecutorService singleThread = Executors.newSingleThreadExecutor();
// Scheduled Thread Pool - For recurring tasks
ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(2);
scheduler.scheduleAtFixedRate(() -> {
System.out.println("Periodic task executed");
}, 0, 5, TimeUnit.SECONDS);
JavaCommon Concurrency Issues: The Gotchas That Bite
Your knowledge on Java concurrency basics aren’t complete unless you are aware of the common issues and gotchas. Let me share some common mistakes so you can avoid them.
Race Conditions & Synchronization
Race conditions happen when multiple threads access shared data simultaneously, and the outcome depends on timing. It’s like two people trying to edit the same document simultaneously – chaos ensues.
public class CounterExample {
private int count = 0;
// WRONG: Not thread-safe
public void incrementUnsafe() {
count++; // This is actually three operations: read, increment, write
}
// RIGHT: Thread-safe with synchronization
public synchronized void incrementSafe() {
count++;
}
// BETTER: Using atomic classes for simple operations
private AtomicInteger atomicCount = new AtomicInteger(0);
public void incrementAtomic() {
atomicCount.incrementAndGet();
}
}
JavaDeadlock: The Threading Nightmare
Deadlocks occur when two or more threads wait for each other indefinitely. Here’s how to prevent them:
public class DeadlockPrevention {
private static final Object lock1 = new Object();
private static final Object lock2 = new Object();
// WRONG: Can cause deadlock
public void method1() {
synchronized (lock1) {
synchronized (lock2) {
// Work here
}
}
}
public void method2() {
synchronized (lock2) { // Different order!
synchronized (lock1) {
// Work here
}
}
}
// RIGHT: Always acquire locks in the same order
public void safeMethod1() {
synchronized (lock1) {
synchronized (lock2) {
// Work here
}
}
}
public void safeMethod2() {
synchronized (lock1) { // Same order as safeMethod1
synchronized (lock2) {
// Work here
}
}
}
}
JavaBest Practices For Java Concurrency Basics
Here are some good practices that could save you countless debugging sessions:
1. Use High-Level Concurrency Utilities
Instead of managing synchronization manually, leverage Java’s concurrent collections and utilities:
// Instead of synchronized HashMap
Map<String, Integer> safeMap = new ConcurrentHashMap<>();
// Instead of synchronized ArrayList
List<String> safeList = new CopyOnWriteArrayList<>();
// For producer-consumer scenarios
BlockingQueue<Task> taskQueue = new LinkedBlockingQueue<>();
Java2. Properly Size Your Thread Pools
For CPU-bound tasks: Number of cores + 1 For I/O-bound tasks: Number of cores × (1 + wait time / service time)
// CPU-bound tasks
int cores = Runtime.getRuntime().availableProcessors();
ExecutorService cpuPool = Executors.newFixedThreadPool(cores + 1);
// I/O-bound tasks (rough estimate)
ExecutorService ioPool = Executors.newFixedThreadPool(cores * 2);
Java3. Always Handle InterruptedException Properly
public void interruptibleTask() {
try {
// Some long-running operation
Thread.sleep(10000);
} catch (InterruptedException e) {
// Restore interrupted status
Thread.currentThread().interrupt();
// Handle the interruption gracefully
System.out.println("Task was interrupted");
}
}
Java4. Use CompletableFuture for Complex Async Operations
CompletableFuture<String> future = CompletableFuture
.supplyAsync(() -> fetchUserData())
.thenApplyAsync(userData -> processUserData(userData))
.thenApplyAsync(processedData -> generateReport(processedData))
.exceptionally(throwable -> {
System.err.println("Error in async chain: " + throwable.getMessage());
return "Default report";
});
// Non-blocking way to get result
future.thenAccept(report -> System.out.println("Report: " + report));
JavaFAQs: Quick Answers on Java Concurrency Basics
You can create threads by implementing the Runnable interface (recommended) or extending the Thread class. The modern approach uses ExecutorService with thread pools for better resource management.
Multithreading is one way to achieve concurrency in Java. Concurrency is the broader concept of managing multiple tasks, while multithreading specifically uses multiple threads to achieve this.
Use synchronization mechanisms like synchronized blocks, atomic classes (AtomicInteger, AtomicReference), or concurrent collections (ConcurrentHashMap) to ensure thread-safe access to shared resources.
Use thread pools whenever you have multiple tasks to execute, especially short-lived or recurring tasks. Thread pools reuse threads, reducing the overhead of thread creation and destruction.
execute() runs tasks without returning results and can’t handle checked exceptions. submit() returns a Future object that allows you to get results and handle exceptions properly.
Advanced Topics: What’s Next?
As you master these basics, consider exploring:
- Virtual Threads (Project Loom) – Available in Java 19+, these lightweight threads can revolutionize how we handle concurrency
- Reactive Programming with libraries like RxJava or Project Reactor
- Fork/Join Framework for divide-and-conquer algorithms
- Lock-free programming with atomic operations
Pro Tip💡: Learn about differences between ForkJoinPool and ThreadPoolExecutor
Real-World Performance Tips
From my experience building high-traffic applications:
- Monitor thread pool metrics in production – active threads, queue size, and task completion rates tell you everything
- Use thread dumps when debugging concurrency issues – they reveal exactly what threads are doing
- Avoid creating threads in hot paths – always use pre-initialized thread pools
- Consider virtual threads for I/O-heavy applications in modern Java versions
Wrapping It Up
Java concurrency basics aren’t just about making code faster – they’re about building robust, scalable applications that can handle real-world demands. Start with simple thread pools, understand the fundamentals, and gradually explore advanced topics.
The key is to practice with small examples before tackling complex scenarios. Every expert was once a beginner who made mistakes, learned from them, and kept pushing forward.
Remember: concurrency is a tool, not a goal. Use it when it solves real problems, not just because you can. Your future self (and your users) will thank you for building applications that are both powerful and reliable.
Ready to dive deeper? Check out these essential resources:
- Oracle’s Official Java Concurrency Tutorial
- “Java Concurrency in Practice” by Brian Goetz (the definitive guide)
- OpenJDK’s documentation on Virtual Threads
Keep experimenting, keep learning, and most importantly – don’t be afraid to make mistakes. They’re the best teachers in the concurrency world!s like a cook in your hive of operations, handling one task while other threads do theirs. Multiple cooks doing different dishes using the same kitchen resources is precisely like how multiple threads in your Java application will use the same CPU and memory.
However, it doesn’t necessarily mean they are running exactly at the same instant (unless you have multiple CPU cores). Instead, it’s like the chef rapidly switching between tasks, giving the illusion of parallel execution.
But here’s the catch: too many threads buzzing at once? That’s how you get stung. (Deadlocks, race conditions… trust me, you don’t want that mess.)
Discover more from CodeSamplez.com
Subscribe to get the latest posts sent to your email.
Leave a Reply