Overview
To accelerate program execution, tasks can be split into independent fragments and executed concurrently across multiple processors. Concurrency becomes valuable when leveraging multi-core systems—such as web servers assigning a thread per HTTP request to distribute load across CPUs.
On a single-core system, concurrency introduces overhead due to context switching between threads. However, when tasks block—typically waiting for I/O or external resources—concurrrent design allows other threads to proceed, maintaining overall progress. Without blocking operations, concurrency on a single CPU offers no benefit.
Processes vs. Threads
A process is an isolated program running in its own memory space. Operating systems schedule CPU time among processes, ensuring isolation. In contrast, threads within the same process share memory and I/O resources. This sharing necessitates careful coordination to prevent race conditions when accessing shared state.
Tasks and Thread Execution
In Java, concurrent work is encapsulated in tasks, which implement either Runnable (no return value) or Callable (returns a result). Each task runs under the control of a thread, which represents a single sequence of execution within a process.
Java uses a preemptive scheduling model: the JVM periodically interrupts threads to switch contexts, giving each a fair slice of CPU time. On multi-core systems, true parallelism is possible; otherwise, threads interleave rapidly, creating the illusion of simultaneous execution.
Defining and Launching Tasks
Implementing Runnable defines a task:
public class DataProcessor implements Runnable {
@Override
public void run() {
// Business logic here
}
}
To execute it:
Thread worker = new Thread(new DataProcessor());
worker.start(); // Non-blocking; main thread continues
Note: Thread object are not immediately eligible for garbage collection—even without explicit references—because they self-register with the JVM until their run() method completes.
Using Executors for Thread Management
The Executor framework decouples task submission from thread lifecycle management. It is the preferred way to run asynchronous tasks.
-
Cached Thread Pool: Creates new threads as needed but reuses idle ones.
ExecutorService pool = Executors.newCachedThreadPool(); for (int i = 0; i < 5; i++) { pool.execute(new DataProcessor()); } pool.shutdown(); -
Fixed Thread Pool: Limits concurrency by reusing a fixed number of threads.
ExecutorService pool = Executors.newFixedThreadPool(4); -
Single Thread Executor: Ensures sequential execution using one background thread.
ExecutorService pool = Executors.newSingleThreadExecutor();
All thread pools reuse existing threads when possible to reduce creation overhead.
Returning Values from Tasks
Use Callable<V> when a task must return a result:
public class ResultTask implements Callable<String> {
@Override
public String call() throws Exception {
return "Computed value";
}
}
Submit via ExecutorService.submit(), which returns a Future:
ExecutorService exec = Executors.newCachedThreadPool();
List<Future<String>> results = new ArrayList<>();
for (int i = 0; i < 3; i++) {
results.add(exec.submit(new ResultTask()));
}
for (Future<String> f : results) {
try {
System.out.println(f.get()); // Blocks until result is ready
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
}
exec.shutdown();
Pausing Thread Execution
Thread.sleep() temporarily suspends execution, allowing other threads to run:
public class DelayedTask implements Runnable {
@Override
public void run() {
try {
Thread.sleep(200); // Sleep for 200ms
} catch (InterruptedException e) {
Thread.currentThread().interrupt(); // Restore interrupt status
}
}
}
Prefer TimeUnit.SECONDS.sleep(1) over Thread.sleep(1000) for readability.
Thread Priorities
Priority hints influence—but do not guarantee—scheduling preference:
public class PriorityTask implements Runnable {
private final int level;
public PriorityTask(int level) { this.level = level; }
@Override
public void run() {
Thread.currentThread().setPriority(level);
// Perform work
}
}
// Usage
ExecutorService exec = Executors.newCachedThreadPool();
exec.execute(new PriorityTask(Thread.MAX_PRIORITY));
exec.execute(new PriorityTask(Thread.MIN_PRIORITY));
For portability, only use MIN_PRIORITY, NORM_PRIORITY, and MAX_PRIORITY.
Yielding CPU Time
Thread.yield() suggests that the scheduler switch to another thread of equal or higher priority. This is merely advisory and may be ignored.
Daemon Threads
Daemon threads perform background services and terminate automatically when all non-daemon threads finish. The main thread is non-daemon by default.
Set daemon status before starting:
Thread background = new Thread(() -> {
while (true) {
try {
TimeUnit.SECONDS.sleep(10);
System.out.println("Daemon alive");
} catch (InterruptedException e) {
return;
}
}
});
background.setDaemon(true);
background.start();
Customize thread properties (like daemon status) via ThreadFactory:
public class DaemonFactory implements ThreadFactory {
@Override
public Thread newThread(Runnable r) {
Thread t = new Thread(r);
t.setDaemon(true);
return t;
}
}
// Use with executor
ExecutorService exec = Executors.newCachedThreadPool(new DaemonFactory());
Important: finally blocks in daemon threads may not execute if the JVM exits before completion.
Extending Thread (Not Recommended)
While possible to subclass Thread, composition (implementing Runnable) is preferred for flexibility and separation of concerns.
Waiting for Thread Completion with join()
Calling t.join() blocks the current thread until t finishes:
Thread worker = new Thread(() -> {
try { Thread.sleep(1000); }
catch (InterruptedException e) { /* handle */ }
});
worker.start();
worker.join(); // Waits up to 1 second (or indefinitely without timeout)
A timeout can be specified: t.join(500). The thread can also be interrupted via t.interrupt(), which sets an internal flag and causes sleep() or join() to throw InterruptedException.
Handling Uncaught Exceptions
Exceptions thrown in run() propagate to the console and cannot be caught by the launching thread. To handle them:
-
Set a per-thread handler:
Thread t = new Thread(new FaultyTask()); t.setUncaughtExceptionHandler((thread, ex) -> System.err.println("Error in " + thread + ": " + ex)); t.start(); -
Or define a global default handler:
Thread.setDefaultUncaughtExceptionHandler((t, e) -> System.err.println("Global error: " + e));
When using ExecutorService, supply a custom ThreadFactory that configures exception handlers on created threads.