Concurrency is about dealing with multiple tasks at once. It's a design principle where tasks can be in progress at the same time, managed by interleaving their execution on a single CPU core. It's about structuring a program to handle multiple workflows.
Parallelism is about doing multiple tasks at once. It's an execution model where tasks run simultaneously on multiple CPU cores. It's about improving performance by running things in parallel.
| Feature | Concurrency | Parallelism |
|---|---|---|
| Nature | A way to structure a program. | A way to execute a program. |
| CPU Cores | Can be achieved with a single CPU core. | Requires multiple CPU cores. |
| Goal | To improve responsiveness and handle multiple events. | To improve performance and throughput. |
| Example | A web server handling multiple client requests by switching between them. | A video editor rendering multiple frames on different cores simultaneously. |
A context switch is the process of storing the state of a thread or process so that it can be restored and resume execution at a later point. This allows multiple processes or threads to share a single CPU. Context switching is computationally expensive as it involves saving and loading registers, memory maps, and updating various kernel data structures. But it is necessary to achieve concurrency.
You can set a thread as a daemon thread by calling thread.setDaemon(true) before it is started.
Multitasking is an operating system feature that allows multiple tasks or processes to run simultaneously, giving the illusion of parallel execution. The OS divides system resources among these tasks and switches between them rapidly.
Types of Multitasking:
Key Differences:
Every Java program has at least one thread called the main thread, which is created automatically by the JVM when the program starts. The main thread is responsible for executing the main() method and is a non-daemon thread.
public class MainThreadExample { public static void main(String[] args) { System.out.println("Current thread: " + Thread.currentThread().getName()); System.out.println("Is daemon: " + Thread.currentThread().isDaemon()); } }
Output:
Current thread: main
Is daemon: false
You can run multiple threads in Java by creating instances of the Thread class or implementing the Runnable interface and starting them with the start() method. When you call start(), each thread runs concurrently.
Example 1: Extending the Thread class
class MyThread extends Thread { @Override public void run() { System.out.println("Thread: " + Thread.currentThread().getName()); } } public class Main { public static void main(String[] args) { MyThread t1 = new MyThread(); MyThread t2 = new MyThread(); t1.start(); // Starts thread t1 t2.start(); // Starts thread t2 } }
Example 2: Implementing Runnable
class MyRunnable implements Runnable { @Override public void run() { System.out.println("Thread: " + Thread.currentThread().getName()); } } public class Main { public static void main(String[] args) { Thread t1 = new Thread(new MyRunnable()); Thread t2 = new Thread(new MyRunnable()); t1.start(); // Starts thread t1 t2.start(); // Starts thread t2 } }
You can also use lambda expressions for simple tasks:
Thread t1 = new Thread(() -> System.out.println("Lambda Thread 1")); Thread t2 = new Thread(() -> System.out.println("Lambda Thread 2")); t1.start(); t2.start();
Each call to start() launches a new thread that executes its run() method independently.
Yes, you can overload the run() method, but it's considered bad practice because:
start() method only calls the no-argument run() methodrun() methods won't be executed by the threadclass MyThread extends Thread { // This will be called by start() @Override public void run() { System.out.println("Thread running"); } // This overloaded method will NOT be called by start() public void run(String message) { System.out.println("Overloaded run: " + message); } }
If you don't override run():
run() method from the Thread class will be executedYou can override start(), but it's not recommended because:
run() method won't be called automaticallyThread classclass MyThread extends Thread { @Override public void start() { // This will NOT create a new thread System.out.println("Custom start method"); // run() won't be called automatically } @Override public void run() { System.out.println("Thread running"); } }
Best Practice: Always call super.start() if you must override start() to maintain proper thread behavior.
A process is any program in a working state that has its own memory space and resources. You can identify processes using:
Tools to view processes:
ps, top, htop commandsjps command for Java processesThreads are lightweight units within a process. To view threads:
Windows:
Linux/Unix:
# View threads for a specific process ps -T -p <PID> # Or use top with thread view top -H -p <PID>
Java-specific:
# List Java threads jstack <PID> # Or use jcmd jcmd <PID> Thread.print
Thread-based multitasking is generally better for the following reasons:
| Aspect | Process-based | Thread-based |
|---|---|---|
| Memory Usage | High (separate address space) | Low (shared address space) |
| Context Switching | Expensive | Inexpensive |
| Communication | Complex (IPC mechanisms) | Simple (shared memory) |
| Creation Overhead | High | Low |
| Resource Sharing | Difficult | Easy |
Advantages of Thread-based Multitasking:
When to use Process-based:
start(): This method is used to begin the execution of a new thread. It registers the thread with the thread scheduler, allocates resources, and invokes the run() method in a separate call stack. Calling start() on a thread more than once will throw an IllegalThreadStateException.run(): This method contains the code that constitutes the thread's task. If you call run() directly, it executes in the current thread, just like any other method call. No new thread is created.The start() method performs two primary tasks:
run() method for the threadImportant Notes:
start() method creates a new call stack for the threadstart() method itselfNo, you cannot start a thread twice. Attempting to call start() on a thread that has already been started will result in an IllegalThreadStateException.
Thread thread = new Thread(() -> System.out.println("Running")); thread.start(); // This works fine thread.start(); // This throws IllegalThreadStateException
Why this restriction exists:
NEW state onceRUNNABLE and cannot go back to NEWSolution: Create a new Thread object if you need to run the same task again.
If you call run() directly instead of start(), the following happens:
run() method executes like any normal method callThread thread = new Thread(() -> { System.out.println("Thread: " + Thread.currentThread().getName()); }); thread.run(); // Executes in main thread // Output: Thread: main thread.start(); // Creates new thread // Output: Thread: Thread-0
Key Difference:
run() = method call in current threadstart() = creates new thread and calls run() in that threadA thread in Java goes through several states:
start() has not yet been called.synchronized block/method.Object.wait() or Thread.join()).Thread.sleep(long) or Object.wait(long)).run() method has exited).The thread.join() method causes the current thread to pause its execution until the thread t has completed. This is a way to ensure that a task in one thread is completed before another thread proceeds. Overloaded versions allow you to specify a timeout.
Thread worker = new Thread(() -> { // ... do some work }); worker.start(); worker.join(); // The main thread waits here until the worker thread is terminated. System.out.println("Worker thread finished.");
| Feature | sleep() | wait() |
|---|---|---|
| Class | java.lang.Thread (static method) | java.lang.Object (instance method) |
| Lock Release | Does not release the monitor lock. | Releases the monitor lock. |
| Context | Can be called from any context. | Must be called from within a synchronized block or method. |
| Waking up | Wakes up automatically after the specified time. | Wakes up only when notify() or notifyAll() is called on the same object, or due to a timeout. |
| Purpose | Pauses execution for a specified time. | Used for inter-thread communication and coordination. |
Interruption is a mechanism to signal a thread that it should stop what it's doing and do something else. A thread is interrupted by calling interrupt() on its Thread object.
A thread can check if it has been interrupted using isInterrupted() (which doesn't clear the interrupted status) or Thread.interrupted() (which is static and clears the status).
Methods like sleep(), wait(), and join() throw an InterruptedException when the waiting/sleeping thread is interrupted, providing a way to handle the interruption request.
Thread task = new Thread(() -> { while (!Thread.currentThread().isInterrupted()) { System.out.println("Working..."); try { Thread.sleep(1000); } catch (InterruptedException e) { // Interruption received while sleeping System.out.println("Interrupted! Cleaning up and stopping."); Thread.currentThread().interrupt(); // Re-interrupt the thread to set the flag } } }); task.start(); // After some time... task.interrupt(); // Request the thread to stop
Thread.yield() is a hint to the thread scheduler that the current thread is willing to yield its current use of a processor. The scheduler is free to ignore this hint. It's a way to suggest that other threads of the same priority should be allowed to run. Its use is rare and it's not a reliable way to control thread execution.
ThreadLocal provides thread-local variables. Each thread that accesses a ThreadLocal variable has its own, independently initialized copy of the variable. This is a way to achieve thread-safety for a mutable object without resorting to synchronization, as each thread operates on its own instance.
Common use cases include storing user-specific data (e.g., transaction ID, user session) throughout the execution of a request in a server application.
Common ThreadLocal Problems:
Solutions:
// Always clean up ThreadLocal values ThreadLocal<String> threadLocal = new ThreadLocal<>(); try { threadLocal.set("value"); // Use the value } finally { threadLocal.remove(); // Important: clean up } // Use InheritableThreadLocal for inheritance InheritableThreadLocal<String> inheritable = new InheritableThreadLocal<>();
Modern Alternative: Use ScopedValue (Java 25+) which provides automatic cleanup and better security.
Related: See our deep-dive: Scoped Values in Java 25: Cleaner and Safer Way to Share Data in Concurrent Applications
| Aspect | notify() | notifyAll() |
|---|---|---|
| Threads Woken | Only one arbitrary thread | All waiting threads |
| Use Case | When you know only one thread should proceed | When multiple threads might need to proceed |
| Performance | Slightly better (wakes only one) | More overhead (wakes all) |
| Safety | Can cause starvation | Prevents starvation |
Example:
synchronized(lock) { if (condition) { lock.wait(); // Thread waits here } // After notify() or notifyAll() } // In another thread synchronized(lock) { condition = true; lock.notify(); // Wakes one thread // OR lock.notifyAll(); // Wakes all threads }
Best Practice: Use notifyAll() unless you have a specific reason to use notify().
| Feature | wait() | sleep() |
|---|---|---|
| Class | Object class method | Thread class method |
| Lock Release | Releases the lock | Does not release the lock |
| Context | Must be in synchronized block | Can be called anywhere |
| Waking Up | Only by notify() or notifyAll() | Automatically after timeout |
| Purpose | Inter-thread communication | Pause execution |
| Feature | yield() | sleep() |
|---|---|---|
| Purpose | Hints scheduler to yield CPU | Pauses execution for specific time |
| Time | No guaranteed time | Guaranteed minimum time |
| Thread State | Remains RUNNABLE | Changes to TIMED_WAITING |
| Reliability | Scheduler may ignore hint | Always pauses execution |
| Use Case | Voluntary CPU sharing | Fixed time delays |
| Feature | wait() | join() |
|---|---|---|
| Purpose | Inter-thread communication | Wait for thread completion |
| Lock Required | Must be in synchronized block | No synchronization required |
| Waking Up | notify() or notifyAll() | Thread completes execution |
| Timeout | wait(long timeout) | join(long timeout) |
| Use Case | Producer-consumer patterns | Sequential execution |
| Feature | interrupt() | stop() (deprecated) |
|---|---|---|
| Safety | Safe, cooperative | Unsafe, forceful |
| Thread State | Sets interrupt flag | Immediately terminates |
| Resource Cleanup | Allows cleanup | No cleanup opportunity |
| Status | Current and recommended | Deprecated since Java 1.2 |
| Exception | Throws InterruptedException | No exception handling |
Why stop() is deprecated:
Use interrupt() instead:
Thread thread = new Thread(() -> { while (!Thread.currentThread().isInterrupted()) { // Do work } // Cleanup code }); thread.interrupt(); // Safe way to stop
If an uncaught exception is thrown from a thread's run() method, the thread will terminate. You can define a global handler for such exceptions using Thread.setDefaultUncaughtExceptionHandler() or a specific one for a thread using thread.setUncaughtExceptionHandler(). Without a handler, the exception stack trace is typically printed to the console.
The Executor Framework, part of the java.util.concurrent package, provides a high-level API for creating and managing threads. It decouples task submission from the mechanics of how each task will be run, including details of thread creation, scheduling, and lifecycle management.
Key interfaces:
Executor: A simple interface with a single execute(Runnable) method.ExecutorService: A sub-interface of Executor that adds features for managing the lifecycle of the executor and the tasks (e.g., submit(), shutdown()).ScheduledExecutorService: A sub-interface of ExecutorService that can schedule commands to run after a given delay, or to execute periodically.Related: Learn practical examples of using the Executor Framework in our Spring Boot Concurrency Guide.
Runnable, Callable) from execution policy.newFixedThreadPool(int nThreads): Creates a thread pool that reuses a fixed number of threads. If all threads are active, new tasks will wait in a queue.newCachedThreadPool(): Creates a thread pool that creates new threads as needed, but will reuse previously constructed threads when they are available. It's suitable for executing many short-lived asynchronous tasks.newSingleThreadExecutor(): Creates an executor that uses a single worker thread. Tasks are guaranteed to execute sequentially.newScheduledThreadPool(int corePoolSize): Creates a thread pool that can schedule commands to run after a given delay, or to execute periodically.execute(Runnable): Takes a Runnable object. It's "fire and forget"; you cannot get the result of the task or check if it completed successfully. It returns void.submit(Runnable) or submit(Callable): Can take both Runnable and Callable tasks. It returns a Future object, which can be used to check the status of the task, block until it's complete, and retrieve its result (if it was a Callable).A Future represents the result of an asynchronous computation. It provides methods to:
isDone(): Check if the computation is complete.isCancelled(): Check if the task was cancelled.cancel(boolean): Attempt to cancel the task.get(): Wait for the computation to complete and then retrieve its result. This is a blocking call.get(long, TimeUnit): Wait for a specified time for the result.| Feature | Runnable | Callable |
|---|---|---|
| Introduced | Java 1.0 | Java 5.0 |
| Method | void run() | V call() |
| Return Value | Cannot return a value. | Can return a value of type V. |
| Exception | Cannot throw checked exceptions. | Can throw checked exceptions. |
| Usage | Used with Thread and Executor.execute(). | Used with ExecutorService.submit(). |
| Feature | Future | CompletableFuture |
|---|---|---|
| Blocking | Always blocking | Non-blocking with callbacks |
| Composition | No composition | Rich composition methods |
| Exception Handling | Basic | Advanced (exceptionally, handle) |
| Manual Completion | No | Yes |
| Chaining | No | Yes (thenApply, thenCompose) |
| Multiple Results | No | Yes (allOf, anyOf) |
Future Example:
Future<String> future = executor.submit(() -> "Result"); String result = future.get(); // Blocking
CompletableFuture Example:
CompletableFuture<String> future = CompletableFuture .supplyAsync(() -> "Result") .thenApply(s -> s.toUpperCase()) .exceptionally(throwable -> "Error"); // Non-blocking with callbacks
| Feature | Thread.sleep() | Object.wait() |
|---|---|---|
| Class | Thread class method | Object class method |
| Lock | Does not release lock | Releases the lock |
| Context | Can be called anywhere | Must be in synchronized block |
| Waking | Automatic after timeout | Only by notify() or notifyAll() |
| Purpose | Pause execution | Inter-thread communication |
| Feature | Thread.sleep() | Thread.yield() |
|---|---|---|
| Guarantee | Guaranteed pause | Hint to scheduler |
| Time | Specific time duration | No time guarantee |
| Thread State | Changes to TIMED_WAITING | Remains RUNNABLE |
| Reliability | Always pauses | Scheduler may ignore |
| Use Case | Fixed delays | Voluntary CPU sharing |
| Feature | Thread.sleep() | Thread.join() |
|---|---|---|
| Purpose | Pause current thread | Wait for another thread |
| Target | Self | Another thread |
| Condition | Time-based | Thread completion |
| Interruption | Can be interrupted | Can be interrupted |
| Use Case | Delays | Sequential execution |
| Feature | Thread.sleep() | LockSupport.park() |
|---|---|---|
| Purpose | Pause for specific time | Pause until unparked |
| Time | Time-based | Event-based |
| Interruption | Throws InterruptedException | Returns silently |
| Use Case | Delays | Low-level synchronization |
| Level | High-level | Low-level |
| Feature | Thread.sleep() | TimeUnit.sleep() |
|---|---|---|
| Units | Milliseconds only | Multiple time units |
| Readability | Less readable | More readable |
| Flexibility | Limited | More flexible |
| Exception | InterruptedException | InterruptedException |
Example:
// Thread.sleep() - milliseconds only Thread.sleep(1000); // TimeUnit.sleep() - multiple units TimeUnit.SECONDS.sleep(1); TimeUnit.MINUTES.sleep(1); TimeUnit.HOURS.sleep(1);
| Feature | Thread.sleep() | ScheduledExecutorService |
|---|---|---|
| Purpose | Pause current thread | Schedule future execution |
| Thread | Blocks current thread | Uses separate thread |
| Reusability | One-time | Reusable |
| Precision | Less precise | More precise |
| Use Case | Simple delays | Complex scheduling |
| Feature | Thread.sleep() | CountDownLatch.await() |
|---|---|---|
| Purpose | Pause for time | Wait for countdown |
| Condition | Time-based | Event-based |
| Interruption | Can be interrupted | Can be interrupted |
| Use Case | Delays | Synchronization |
| Flexibility | Fixed time | Dynamic condition |
ThreadPoolExecutor is the core, highly configurable class behind the factory methods of Executors. It allows fine-grained control over the thread pool's behavior.
Key constructor parameters:
corePoolSize: The number of threads to keep in the pool, even if they are idle.maximumPoolSize: The maximum number of threads allowed in the pool.keepAliveTime: When the number of threads is greater than the core, this is the maximum time that excess idle threads will wait for new tasks before terminating.workQueue: The queue to use for holding tasks before they are executed.threadFactory: The factory to use when the executor creates a new thread.handler: The handler to use when a task is rejected.When the work queue is full and the number of threads has reached maximumPoolSize, the RejectedExecutionHandler is invoked. The default policies are:
AbortPolicy (default): Throws a RejectedExecutionException.CallerRunsPolicy: The task is executed by the calling thread itself.DiscardPolicy: The task is silently discarded.DiscardOldestPolicy: The oldest task in the queue is discarded, and the new task is submitted.You should use shutdown() and awaitTermination().
shutdown(): Initiates an orderly shutdown. The executor stops accepting new tasks, but previously submitted tasks will be executed.awaitTermination(long, TimeUnit): Blocks until all tasks have completed after a shutdown request, or the timeout occurs, or the current thread is interrupted.ExecutorService executor = Executors.newFixedThreadPool(10); // ... submit tasks executor.shutdown(); try { if (!executor.awaitTermination(60, TimeUnit.SECONDS)) { executor.shutdownNow(); // Forcefully shut down } } catch (InterruptedException e) { executor.shutdownNow(); }
shutdown(): Allows currently running tasks and tasks in the queue to complete.shutdownNow(): Attempts to stop all actively executing tasks, halts the processing of waiting tasks, and returns a list of the tasks that were awaiting execution. It does this by interrupting the worker threads.It's an ExecutorService that can schedule tasks to be executed in the future.
schedule(Callable<V>, long, TimeUnit): Executes a Callable once after a given delay.schedule(Runnable, long, TimeUnit): Executes a Runnable once after a given delay.scheduleAtFixedRate(Runnable, long, long, TimeUnit): Executes a task periodically after an initial delay. The next task is scheduled relative to the start of the previous execution. If a task takes longer than the period, the next one will start immediately after the current one finishes.scheduleWithFixedDelay(Runnable, long, long, TimeUnit): Executes a task periodically. The next task is scheduled relative to the end of the previous execution.Introduced in Java 7, the Fork/Join framework is designed for work that can be broken into smaller pieces recursively. It uses a special ForkJoinPool executor which implements a "work-stealing" algorithm: idle threads try to "steal" tasks from the deques (double-ended queues) of busy threads. This improves CPU utilization.
It's ideal for CPU-intensive tasks like data processing, searching, and sorting on multi-core machines.
These are the base classes for tasks that run within a ForkJoinPool.
RecursiveTask<V>: A task that returns a result. Its compute() method should be overridden.RecursiveAction: A task that does not return a result (similar to Runnable).A CachedThreadPool or a FixedThreadPool with a large number of threads would be a good choice.
CachedThreadPool: It can create a large number of threads to handle many concurrent I/O operations. Since the threads are mostly waiting, having many of them doesn't overload the CPU. It will reuse threads that finish their tasks.FixedThreadPool: You could also use a FixedThreadPool, but the size would need to be tuned. A common formula for I/O-bound tasks is NumberOfThreads = NumberOfCores * (1 + WaitTime / ServiceTime). Since wait time is high, the pool size should be significantly larger than the number of CPU cores.Using a small fixed-size pool would lead to tasks waiting in the queue while the CPU is underutilized.
A FixedThreadPool with 8 threads (Executors.newFixedThreadPool(8)).
Synchronization is a mechanism to control the access of multiple threads to any shared resource. It's a core tool for preventing concurrency issues like race conditions and memory consistency errors. Java provides synchronization via the synchronized keyword and the java.util.concurrent.locks package.
In Java, every object has an intrinsic lock (or monitor) associated with it. The synchronized keyword works by acquiring and releasing this lock. When a thread enters a synchronized method or block, it acquires the monitor of the corresponding object. Other threads attempting to acquire the same monitor will be blocked until the lock is released.
this object for instance methods, or the Class object for static methods. It's simple to use but can lead to performance issues if the method is long and only a small part of it needs protection.
public synchronized void doWork() { // entire method is locked }
public void doWork() { // ... non-critical section synchronized(this) { // critical section } // ... non-critical section }
A race condition occurs when multiple threads access and manipulate shared data concurrently, and the final outcome depends on the particular order in which the threads are scheduled.
Example: A simple counter.
class Counter { private int count = 0; public void increment() { count++; // This is not atomic! It's read-modify-write. } }
If two threads call increment() at the same time, they might both read the value of count (e.g., 5), both increment it to 6, and both write 6 back. The final value will be 6 instead of the correct 7.
A deadlock is a situation where two or more threads are blocked forever, waiting for each other to release the resources they need.
Prevention Strategies:
tryLock: Use lock.tryLock(timeout, unit) from the Lock interface. This allows a thread to back off if it cannot acquire a lock within a certain time, preventing it from holding one lock while waiting indefinitely for another.A ReentrantLock is a concrete implementation of the Lock interface. It has the same basic behavior as a synchronized block but with extended capabilities:
tryLock() allows a thread to attempt to acquire a lock for a certain period.Condition objects, which are more flexible than wait/notify.Reentrancy means that a thread can acquire a lock it already holds. The lock maintains a hold count. Each time the thread acquires the lock, the count is incremented. Each time it releases the lock (via unlock()), the count is decremented. The lock is only fully released when the count reaches zero. This is crucial for preventing a thread from deadlocking with itself in recursive calls.
synchronized is simpler, less error-prone, and often optimized by the JVM. It should be your default choice.ReentrantLock when you need its advanced features:
Condition objects.A key difference in practice is that unlock() must be called in a finally block to guarantee the lock is released, even if exceptions occur. synchronized handles this automatically.
lock.lock(); try { // critical section } finally { lock.unlock(); }
A ReadWriteLock maintains a pair of associated locks—one for read-only operations and one for writing. The read lock may be held simultaneously by multiple reader threads, as long as there are no writers. The write lock is exclusive.
This is a performance optimization for data structures that are read far more often than they are modified.
Introduced in Java 8, StampedLock is a more advanced read-write lock. It supports three modes: reading, writing, and optimistic reading.
writeLock() returns a stamp. Exclusive access.readLock() returns a stamp. Non-exclusive.tryOptimisticRead() returns a stamp without any locking. After reading, you call validate(stamp) to check if a write has occurred in the meantime. If so, you need to acquire a proper read lock and try again.Optimistic reading can provide significant performance gains in read-heavy scenarios by avoiding the overhead of acquiring a full read lock.
A Semaphore is a synchronization aid that controls access to a shared resource through the use of a counter. It maintains a set of "permits".
acquire(): Blocks until a permit is available, and then takes one.release(): Adds a permit, potentially releasing a blocking acquirer.A semaphore initialized with one permit is equivalent to a mutex lock. It's useful for limiting the number of concurrent threads accessing a specific part of your application (e.g., limiting concurrent database connections).
| Feature | Semaphore | Mutex |
|---|---|---|
| Permits | Can have multiple permits | Only one permit (binary) |
| Ownership | No ownership concept | Has ownership (only owner can release) |
| Use Case | Resource pooling, rate limiting | Critical section protection |
| Initialization | new Semaphore(n) | new Semaphore(1) |
| Release | Any thread can release | Only the acquiring thread can release |
Example:
// Semaphore - multiple permits Semaphore semaphore = new Semaphore(3); // 3 permits semaphore.acquire(); // Thread 1 semaphore.acquire(); // Thread 2 semaphore.acquire(); // Thread 3 // Thread 4 waits until one of the above releases // Mutex - single permit Semaphore mutex = new Semaphore(1); mutex.acquire(); // Only one thread at a time
| Feature | CountDownLatch | CyclicBarrier |
|---|---|---|
| Reusability | One-time use | Reusable (cyclic) |
| Direction | One-way (countdown) | Two-way (meeting point) |
| Threads | One or more wait for others | All threads wait for each other |
| Reset | Cannot be reset | Automatically resets |
| Action | No action on countdown | Optional action on barrier trip |
CountDownLatch Example:
CountDownLatch latch = new CountDownLatch(3); // 3 threads count down // 1 thread waits for all to complete
CyclicBarrier Example:
CyclicBarrier barrier = new CyclicBarrier(3, () -> System.out.println("All threads reached the barrier")); // All 3 threads wait for each other // When all arrive, barrier action executes
| Feature | ReentrantLock | synchronized |
|---|---|---|
| Flexibility | More flexible | Less flexible |
| Fairness | Can be fair or unfair | Always unfair |
| Interruption | Supports interruption | No interruption support |
| Timeout | Supports tryLock(timeout) | No timeout support |
| Conditions | Multiple Condition objects | Single wait/notify |
| Performance | Slightly slower | Slightly faster |
| Memory | More memory overhead | Less memory overhead |
When to use ReentrantLock:
When to use synchronized:
| Feature | volatile | synchronized |
|---|---|---|
| Scope | Single variable | Block or method |
| Atomicity | No (except for simple assignments) | Yes |
| Performance | Faster | Slower |
| Blocking | Never blocks | Can block |
| Use Case | Visibility only | Atomicity + Visibility |
volatile Example:
private volatile boolean flag = false; // Only ensures visibility, not atomicity
synchronized Example:
private int counter = 0; public synchronized void increment() { counter++; // Atomic and visible }
| Feature | AtomicInteger | synchronized int |
|---|---|---|
| Performance | Faster (lock-free) | Slower (lock-based) |
| Blocking | Never blocks | Can block |
| Memory | More memory | Less memory |
| Operations | Limited atomic operations | Any operation |
| Scalability | Better under contention | Worse under contention |
AtomicInteger Example:
AtomicInteger counter = new AtomicInteger(0); counter.incrementAndGet(); // Atomic operation
synchronized Example:
private int counter = 0; public synchronized int increment() { return ++counter; // Atomic but blocking }
| Feature | Executor | ExecutorService |
|---|---|---|
| Methods | Only execute(Runnable) | execute(), submit(), shutdown(), etc. |
| Future Support | No | Yes |
| Lifecycle Management | No | Yes |
| Task Tracking | No | Yes |
| Use Case | Simple task execution | Advanced task management |
Executor Example:
Executor executor = Executors.newFixedThreadPool(5); executor.execute(() -> System.out.println("Task"));
ExecutorService Example:
ExecutorService executor = Executors.newFixedThreadPool(5); Future<String> future = executor.submit(() -> "Result"); executor.shutdown();
A CountDownLatch is a synchronization aid that allows one or more threads to wait until a set of operations being performed in other threads completes. It is initialized with a count.
await(): Causes the current thread to wait until the latch's count reaches zero.countDown(): Decrements the count of the latch.It is a one-time use object; its count cannot be reset. Useful for "start gates" (all threads wait for a signal to start) or "end gates" (a main thread waits for all workers to finish).
A CyclicBarrier is a reusable synchronization aid that allows a set of threads to all wait for each other to reach a common barrier point. Threads call await(), which blocks until all parties have invoked await() on the barrier. At that point, the barrier is "tripped," and all waiting threads are released.
The barrier is "cyclic" because it can be reset and used again after the waiting threads are released. An optional Runnable can be provided to be executed once per barrier trip.
| Feature | CountDownLatch | CyclicBarrier |
|---|---|---|
| Reusability | One-time use. The count cannot be reset. | Reusable. Resets automatically after all threads pass the barrier. |
| Waiting | One or more threads can wait for a set of other threads to complete tasks. | Threads wait for each other to reach a common point. |
| Direction | It's a one-way street. The main thread waits for workers. | It's a meeting point. All threads are workers and wait for each other. |
| Action | No action is performed when the count reaches zero. | An optional Runnable action can be executed when the barrier is tripped. |
Introduced in Java 7, Phaser is a more flexible and powerful version of CyclicBarrier and CountDownLatch.
CyclicBarrier.It's useful for more complex, multi-phase parallel computations where the number of participating threads might vary.
synchronized or ReentrantLock). This can be inefficient if contention is rare.StampedLock's optimistic read mode is an example of this pattern.Lock striping is a technique to improve concurrency by using an array of locks instead of a single lock. Each hash bucket or segment of a data structure is protected by its own lock. This allows different threads to operate on different parts of the data structure simultaneously, as long as they don't need to access the same segment. ConcurrentHashMap uses this technique.
Lock coarsening is a JVM optimization technique where a sequence of adjacent synchronized blocks that use the same lock object are merged into a single larger synchronized block. This reduces the overhead of repeatedly acquiring and releasing the lock.
Lock elision is a JVM optimization where the JIT compiler can completely remove a lock acquisition if it determines that an object's lock is not actually contended by multiple threads (e.g., the object is thread-local and never escapes).
A Condition object (obtained from a Lock via lock.newCondition()) provides a more powerful and flexible alternative to the traditional wait(), notify(), and notifyAll() methods.
await(): Equivalent to wait().signal(): Equivalent to notify().signalAll(): Equivalent to notifyAll().The key advantage is that you can have multiple Condition objects associated with a single Lock. This allows for more fine-grained control, for example, in a bounded buffer problem, you can have one condition for "not full" and another for "not empty".
There are several ways. The "Initialization-on-demand holder idiom" is generally considered the best approach as it's lazy, thread-safe, and doesn't require synchronization.
public class ThreadSafeSingleton { private ThreadSafeSingleton() {} private static class SingletonHolder { private static final ThreadSafeSingleton INSTANCE = new ThreadSafeSingleton(); } public static ThreadSafeSingleton getInstance() { return SingletonHolder.INSTANCE; } }
Why it's thread-safe: The JVM guarantees that the static initializer for SingletonHolder is executed only once, when getInstance() is called for the first time, and that this initialization is thread-safe.
synchronized and the default ReentrantLock are non-fair.new ReentrantLock(true).lock.tryLock() attempts to acquire the lock immediately and returns true if successful, false otherwise. It does not block. An overloaded version tryLock(long time, TimeUnit unit) will try to acquire the lock for a specified duration before giving up. This is very useful for preventing deadlocks and for implementing responsive systems.
jstack, jvisualvm, or by sending a SIGQUIT signal (on Linux/macOS). The thread dump will include information about which threads are deadlocked and the locks they are waiting for.ThreadMXBean: Programmatically, you can use ThreadMXBean.findDeadlockedThreads() to get an array of thread IDs that are involved in a deadlock.Using ReentrantLock and Condition objects:
class BoundedBuffer<T> { private final T[] buffer; private final Lock lock = new ReentrantLock(); private final Condition notFull = lock.newCondition(); private final Condition notEmpty = lock.newCondition(); private int count, putIndex, takeIndex; public BoundedBuffer(int capacity) { buffer = (T[]) new Object[capacity]; } public void put(T item) throws InterruptedException { lock.lock(); try { while (count == buffer.length) { notFull.await(); // Buffer is full, wait for space } buffer[putIndex] = item; if (++putIndex == buffer.length) putIndex = 0; count++; notEmpty.signal(); // Signal that buffer is no longer empty } finally { lock.unlock(); } } public T take() throws InterruptedException { lock.lock(); try { while (count == 0) { notEmpty.await(); // Buffer is empty, wait for item } T item = buffer[takeIndex]; if (++takeIndex == buffer.length) takeIndex = 0; count--; notFull.signal(); // Signal that buffer is no longer full return item; } finally { lock.unlock(); } } }
The JMM defines the rules that govern how threads interact through memory. It specifies when changes to a variable made by one thread become visible to other threads. Without the JMM, it would be impossible to write correct, reliable concurrent code on modern multi-core, multi-level cache architectures. It's an abstraction over how hardware memory actually works.
long and double) are atomic. Operations like i++ are not atomic.volatile and synchronized keywords are used to ensure visibility.A "happens-before" relationship is a guarantee that memory writes by one specific statement are visible to another specific statement. If action A happens-before action B, then the results of A are visible to and ordered before B.
Key happens-before rules:
volatile variable happens-before every subsequent read of that same volatile variable.start() on a thread happens-before any action in the started thread.join() on that thread.| Feature | synchronized | volatile |
|---|---|---|
| Guarantees | Atomicity and Visibility | Visibility only |
| Mechanism | Acquires and releases a monitor lock. | Uses memory barriers. |
| Scope | Can protect blocks of code and methods. | Applies only to a single variable. |
| Blocking | Can cause threads to block. | Never causes threads to block. |
| Overhead | Higher overhead due to locking. | Lower overhead. |
To improve performance, compilers, the JIT, and CPUs can reorder instructions that have no data dependency. For example, two independent variable assignments might be swapped. While this is safe for single-threaded code, it can cause unpredictable behavior in multi-threaded code if not handled correctly with synchronization mechanisms. volatile and synchronized prevent harmful reordering.
The java.util.concurrent.atomic package provides classes like AtomicInteger, AtomicLong, and AtomicReference that support lock-free, thread-safe programming on single variables. They use low-level, hardware-specific atomic instructions like Compare-And-Swap (CAS) instead of locks.
This is much more efficient than using synchronized for simple atomic operations like incrementing a counter.
// Thread-safe counter using AtomicInteger AtomicInteger atomicCounter = new AtomicInteger(0); atomicCounter.incrementAndGet(); // Atomically increments and returns the new value
CAS is an atomic instruction provided by most modern CPUs. It takes three operands: a memory location V, an expected old value A, and a new value B. The instruction atomically updates the value in V to B only if the current value in V matches A. It returns the original value of V. This allows an algorithm to check if a value has changed since it was last read before updating it. Atomic variables use CAS internally.
Safe publication is about ensuring that an object's state is fully visible to other threads after its construction. An object is safely published if:
volatile field or an AtomicReference.Improper publication (e.g., assigning a new object to a plain shared variable without synchronization) can lead to other threads seeing a partially constructed object.
The JMM provides special guarantees for final fields to ensure safe initialization. Once an object's constructor finishes, any thread that sees a reference to that object is guaranteed to see the correct values of its final fields, without needing any explicit synchronization. This is why String objects are immutable and thread-safe.
Standard collections like ArrayList and HashMap are not thread-safe. If multiple threads modify them concurrently, their internal state can become corrupted, leading to exceptions (ConcurrentModificationException) or incorrect behavior.
While you can wrap them with Collections.synchronizedMap() or Collections.synchronizedList(), these "synchronized collections" use a single lock to protect the entire collection, leading to poor performance under high contention. Concurrent collections are designed for much better performance in multi-threaded environments.
ConcurrentHashMap is a high-performance, thread-safe implementation of the Map interface.
ConcurrentModificationException: Its iterators are weakly consistent and will never throw this exception.CopyOnWriteArrayList is a thread-safe variant of ArrayList. All mutative operations (add, set, remove) are implemented by making a fresh copy of the underlying array.
Use Case: It's ideal for collections that are read far more often than they are modified, such as listener lists.
A BlockingQueue is a queue that supports operations that wait for the queue to become non-empty when retrieving an element, and wait for space to become available in the queue when storing an element. It's a fundamental building block for producer-consumer patterns.
Key methods:
put(e): Blocks until space is available, then adds the element.take(): Blocks until an element is available, then retrieves and removes it.offer(e, time, unit): Tries to add an element, waiting up to a specified time if necessary.poll(time, unit): Tries to retrieve an element, waiting up to a specified time if necessary.ArrayBlockingQueue: A fixed-size blocking queue backed by an array. It offers fairness policy.LinkedBlockingQueue: An optionally bounded blocking queue backed by a linked list. It has higher throughput than ArrayBlockingQueue but is less predictable in performance.PriorityBlockingQueue: An unbounded blocking queue that uses priority ordering for its elements.SynchronousQueue: A queue with zero capacity. A put() operation must wait for a corresponding take() operation, and vice-versa. It's used for handoff scenarios.DelayQueue: An unbounded blocking queue of Delayed elements, where an element can only be taken when its delay has expired.| Feature | ArrayBlockingQueue | LinkedBlockingQueue |
|---|---|---|
| Data Structure | Array | Linked List |
| Bounds | Always bounded (fixed capacity). | Optionally bounded (can be unbounded). |
| Locking | Single lock for both put and take. | Two locks: one for put, one for take. |
| Performance | Generally lower throughput due to single lock. | Generally higher throughput as producers and consumers can operate in parallel. |
| Fairness | Can be configured to be fair. | Not fair. |
A ConcurrentSkipListMap is a concurrent, sorted map. It's the concurrent equivalent of TreeMap. It uses a skip list data structure to achieve scalability. It allows for concurrent access and modification while maintaining sorted order.
It's a sub-interface of ConcurrentMap that provides navigation methods for a sorted map, such as headMap, tailMap, subMap, ceilingEntry, floorEntry. ConcurrentSkipListMap is the main implementation of this interface.
A BlockingDeque is a "double-ended queue" that supports blocking operations. You can add or remove elements from both the head and the tail, and it will block if the deque is full (on insertion) or empty (on removal). LinkedBlockingDeque is the primary implementation.
The iterators of collections like ConcurrentHashMap and CopyOnWriteArrayList are weakly consistent. This means:
ConcurrentModificationException.A bounded BlockingQueue, such as ArrayBlockingQueue or a bounded LinkedBlockingQueue.
put() method will block, automatically throttling the producers until the consumers catch up and free up space in the queue. This prevents the application from running out of memory by queuing an infinite number of tasks.Collections.synchronizedMap() is a factory method that returns a thread-safe wrapper around a regular Map. It achieves thread safety by synchronizing every single method on the map object's monitor.
ConcurrentHashMap is better because:
ConcurrentModificationException, which is cumbersome and inefficient. ConcurrentHashMap's iterator is weakly consistent and safe to use.A SynchronousQueue is used when you want to hand off a task directly from a producer thread to a consumer thread, without any buffering. It's an excellent tool for managing handoffs in thread pools. For example, a CachedThreadPool uses a SynchronousQueue internally. When a task is submitted, it's queued. If a worker thread is available to take it, the handoff happens immediately. If not, a new thread is created to handle the task.
A DelayQueue is a BlockingQueue where elements can only be taken when their delay has expired. Elements must implement the Delayed interface, which has a getDelay() method.
Use Case: Implementing a simple cache where items expire after a certain time. You can add items to a DelayQueue with their expiration time. A background thread can then take() from the queue, and it will receive expired items, which it can then remove from the cache.
Collections.newSetFromMap(new ConcurrentHashMap<T, Boolean>()): This is the standard, high-performance way to create a thread-safe Set backed by a ConcurrentHashMap.CopyOnWriteArraySet: A thread-safe set suitable for read-heavy scenarios.Collections.synchronizedSet(): A wrapper that provides basic thread safety with lower concurrency.CompletableFuture, introduced in Java 8, is an advanced implementation of Future. It provides a vast array of methods for composing, combining, and handling asynchronous computations in a non-blocking way.
Key Improvements:
get() method.CompletableFuture instances (e.g., thenCombine, allOf, anyOf).exceptionally, handle).CompletableFuture and complete it manually later with a value or an exception.Related: See practical examples of
CompletableFutureusage in our Spring Boot Concurrency Guide.
These are methods for chaining actions to be executed upon completion of a future.
thenApply(Function): Takes the result of the completed future, applies a function to it, and returns a new CompletableFuture with the function's result. (Transforming the result).thenAccept(Consumer): Takes the result of the completed future and passes it to a Consumer. It returns CompletableFuture<Void>. (Consuming the result).thenRun(Runnable): Executes a Runnable after the future completes. It doesn't get the result. It returns CompletableFuture<Void>. (Action after completion).thenCombine(other, BiFunction): Waits for both futures to complete, then combines their results using a BiFunction.thenCompose(Function): A powerful method for chaining dependent futures. If the next action itself returns a CompletableFuture, thenCompose is used to flatten the result (CompletableFuture<CompletableFuture<T>> becomes CompletableFuture<T>).allOf(futures...): Returns a new CompletableFuture that completes when all of the given futures complete.anyOf(futures...): Returns a new CompletableFuture that completes when any of the given futures complete.Thread starvation occurs when a thread is perpetually denied access to resources it needs to make progress. This can happen if:
A livelock is a situation where two or more threads are actively trying to resolve a contention issue but are unable to make progress. The threads are not blocked; they are busy responding to each other's actions. For example, two people trying to pass in a hallway might repeatedly step aside in the same direction, blocking each other indefinitely.
Amdahl's Law is a formula used to find the maximum theoretical speedup you can get from parallelizing a program. It states that the speedup is limited by the sequential portion of the program. If P is the proportion of the program that can be made parallel, and N is the number of processors, the maximum speedup is 1 / ((1 - P) + (P / N)).
The key takeaway is that even with infinite processors, the speedup is limited by the part of the code that must run sequentially.
It's a classic concurrency pattern that decouples the process of producing work (Producers) from the process of consuming that work (Consumers). The two are connected by a shared, thread-safe queue (usually a BlockingQueue).
This pattern improves modularity and helps smooth out workloads where the rate of production and consumption might vary.
This pattern allows concurrent read access to an object but requires exclusive access for write operations. It's designed to improve performance in situations where a shared resource is read much more frequently than it is written to. ReadWriteLock is a direct implementation of this pattern.
Work-stealing is used by the ForkJoinPool. Each worker thread has its own double-ended queue (deque) of tasks. When a thread finishes all tasks in its own deque, it looks at the deques of other busy threads and "steals" a task from the tail of their queue. This balances the load efficiently and reduces contention, as threads usually take work from the head of their own deque and steal from the tail of others'.
A memory barrier (or memory fence) is a low-level instruction that forces the CPU to enforce an ordering constraint on memory operations. It ensures that operations before the barrier are completed before operations after the barrier are started. volatile writes and synchronized releases insert memory barriers to ensure visibility and ordering.
False sharing is a performance-degrading issue in multi-core systems. It occurs when two threads on different cores modify variables that are located on the same cache line. A cache line is the smallest unit of memory that can be transferred between main memory and a CPU cache. Even though the threads are modifying different variables, the hardware's cache coherency protocol will invalidate the entire cache line for both cores every time one of them performs a write. This causes excessive cache invalidations and memory traffic.
It can be mitigated by padding data structures to ensure that independent variables do not share a cache line.
Thread.sleep(0) is sometimes used as a hint to the operating system's thread scheduler to yield the CPU to another thread. It suggests that if there are other runnable threads, one of them should be scheduled to run. However, the exact behavior is OS-dependent and it's not a reliable way to manage thread execution. Thread.yield() is a more idiomatic, though still advisory, way to express this intent.
Busy-spinning (or busy-waiting) is a technique where a thread repeatedly checks a condition in a tight loop without relinquishing the CPU (e.g., while (condition) {}). This consumes a lot of CPU cycles. It is only appropriate in rare, low-level situations where the wait time is expected to be extremely short and the cost of blocking and re-scheduling the thread is higher than the cost of the wasted CPU cycles.
No. A constructor cannot be declared as synchronized. The reason is that only the thread creating the object has access to it during construction, so there is no need for synchronization. The synchronized keyword on a method synchronizes on the this object's lock, but this is not fully available to other threads until the constructor has finished.
An object is immutable if its state cannot be changed after it is created. Immutable objects are inherently thread-safe.
Examples include String, Integer, and the record classes in modern Java.
ExecutorService to manage a pool of worker threads. The size of the pool would be tuned based on whether the task is more I/O-bound (downloading pages) or CPU-bound (parsing HTML).Runnable or Callable that performs the following steps:
a. Take a URL from a shared queue.
b. Download the web page (I/O-bound).
c. Parse the HTML to extract new links (CPU-bound).
d. Put the newly found links into a shared queue or set to be processed.BlockingQueue<URL> for the URLs to be crawled. This provides thread-safe access and back-pressure.ConcurrentHashMap<URL, Boolean> or a ConcurrentSkipListSet to keep track of visited URLs to avoid duplicate work and crawling loops.CountDownLatch or Phaser could be used to determine when the crawl is "complete" (e.g., when the queue is empty and all worker threads are idle).public class ModernWebCrawler { private final Set<URL> visitedUrls = ConcurrentHashMap.newKeySet(); private final BlockingQueue<URL> urlQueue = new LinkedBlockingQueue<>(); public void crawl(URL startUrl) throws InterruptedException { urlQueue.offer(startUrl); try (var scope = StructuredTaskScope.open()) { // Create multiple crawler tasks for (int i = 0; i < 100; i++) { // 100 virtual threads scope.fork(() -> crawlWorker()); } scope.join(); } } private void crawlWorker() { while (!urlQueue.isEmpty()) { URL url = urlQueue.poll(); if (url != null && visitedUrls.add(url)) { try { String content = downloadPage(url); List<URL> links = extractLinks(content); links.forEach(urlQueue::offer); } catch (Exception e) { // Handle error } } } } }
Benefits of Modern Approach:
You can use the exceptionally() or handle() methods.
exceptionally(Function<Throwable, T>): This provides a way to recover from an exception. It's called only if the preceding stage completes exceptionally. It receives the exception and can return a default/fallback value.handle(BiFunction<T, Throwable, U>): This is more general. It's called regardless of whether the preceding stage completed normally or exceptionally. It receives both the result (which will be null if an exception occurred) and the exception (which will be null if it completed normally). It allows you to transform the result in either case.Introduced in Java 9, VarHandle is a replacement for the low-level, unsafe methods and provides a modern, safe, and efficient way to perform atomic and ordered operations on fields. It's a more powerful and general-purpose version of the Atomic* classes. It can be used to perform CAS operations, volatile reads/writes (memory-fenced operations), and other fine-grained memory ordering operations on object fields and array elements.
Structured Concurrency, introduced as a preview feature in Java 21 and finalized in Java 25, is a programming paradigm that treats groups of related tasks running in different threads as a single unit of work. This approach streamlines error handling and cancellation while managing the lifecycle of concurrent operations cohesively.
Key Benefits:
Basic Usage:
try (var scope = StructuredTaskScope.open()) { var task1 = scope.fork(() -> fetchUserData(userId)); var task2 = scope.fork(() -> fetchOrderHistory(userId)); var task3 = scope.fork(() -> fetchPreferences(userId)); scope.join(); UserData user = task1.get(); List<Order> orders = task2.get(); Preferences prefs = task3.get(); return buildUserProfile(user, orders, prefs); }
Related: Learn more about Structured Concurrency in our comprehensive guide.
Scoped Values, finalized in Java 25, provide a safer and more structured way of sharing data within a thread and its child threads compared to ThreadLocal variables. They address the limitations of ThreadLocal including unconstrained mutability and unbounded lifetime.
Key Benefits:
Basic Usage:
public class UserContext { public static final ScopedValue<String> user = ScopedValue.newInstance(); } // Setting and using scoped values ScopedValue.where(UserContext.user, "alice").run(() -> { // User is "alice" in this scope processUserRequest(); }); // Safe access with default value String currentUser = UserContext.user.orElse("anonymous");
Related: Learn more about Scoped Values in our detailed guide.
Stable Values, introduced as a preview feature in Java 25, provide a standardized way to lazily initialize constants with thread-safety and JVM optimizations. They solve the problems of traditional lazy initialization patterns like double-checked locking.
Key Benefits:
Basic Usage:
public class DatabaseConfig { private static final StableValue<ConnectionPool> connectionPool = StableValue.of(); public static ConnectionPool getConnectionPool() { return connectionPool.orElseSet(() -> new ConnectionPool(loadDatabaseConfig())); } }
Related: Learn more about Stable Values in our comprehensive guide.
Virtual threads, a major feature in modern Java (preview in 19/20, final in 21), are extremely lightweight threads managed by the JVM, not the OS. A large number of virtual threads run on a small number of OS "carrier" threads.
InputStream.read()) that scales like asynchronous, non-blocking code. You can have millions of virtual threads running concurrently.Important Note: In Java 24, virtual threads no longer get pinned in synchronized blocks, addressing a major limitation that previously required using ReentrantLock instead of synchronized for optimal performance.
This dramatically simplifies writing highly concurrent server applications.
Related: Learn more about Virtual Threads in our Spring Boot Concurrency Guide and Concurrency Limits and Modern Solutions.
A high-contention logger can become a bottleneck because multiple threads are trying to write to the same resource (a file or the console) through a synchronized block.
LinkedBlockingQueue or a specialized lock-free queue).This is the architecture used by modern high-performance logging frameworks like Log4j2 and Logback.
| Scenario | Recommended Approach | Reason |
|---|---|---|
| I/O-bound tasks | Virtual Threads | Lightweight, perfect for blocking I/O operations |
| CPU-intensive tasks | Fixed Thread Pool (size = CPU cores) | Optimal resource utilization |
| Task coordination | Structured Concurrency | Clear error handling and cancellation |
| Data sharing | Scoped Values (Java 25+) | Safer than ThreadLocal |
| Lazy initialization | Stable Values (Java 25+) | Thread-safe with JVM optimizations |
| Simple async operations | CompletableFuture | Rich API for composition |
Related: Learn about modern concurrency solutions and overcoming traditional limitations in our Concurrency Limits and Modern Solutions guide.
| Task Type | Recommended Pool Size | Reasoning |
|---|---|---|
| CPU-bound | Number of CPU cores | Avoid context switching overhead |
| I/O-bound | 2-4x CPU cores | Allow for blocking operations |
| Mixed workload | 1.5-2x CPU cores | Balance between CPU and I/O |
| Virtual threads | Thousands to millions | Lightweight, perfect for I/O |
jstack or jcmd to analyze thread states🔗 Blog: https://codewiz.info
🔗 LinkedIn: https://www.linkedin.com/in/code-wiz-740370302/
🔗 Medium: https://medium.com/@code.wizzard01
🔗 Github: https://github.com/CodeWizzard01

Get instant AI-powered summaries of YouTube videos and websites. Save time while enhancing your learning experience.