Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

To run work concurrently in Java, define the work as a Runnable or Callable, then execute it with a Thread or an executor. Direct threads explain the mechanism; ExecutorService is usually easier to manage in an application. Because threads share process memory, protect shared mutable state with synchronization or concurrent utilities. Virtual threads make very large numbers of mostly waiting tasks practical, but they do not make CPU-bound code run faster.

Task versus thread: the essential distinction

A task is the work your program wants done. A thread is an execution path that runs that work. Java’s Runnable interface describes work that returns no result, while Callable describes work that can return a value or throw an exception. A Thread supplies the execution mechanism.

Threads run inside a process and share resources such as memory and open files. Sharing makes communication convenient, but it also means that unsafely shared data can be changed or observed at the wrong time.

How do I create a thread in Java?

Define a Runnable and start a Thread

The simplest example separates the task from the thread that runs it:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
Runnable task = () -> {
    System.out.println("Work is running on " + Thread.currentThread().getName());
};

Thread thread = new Thread(task, "worker-1");
thread.start();

try {
    thread.join();
} catch (InterruptedException e) {
    Thread.currentThread().interrupt();
}

start() asks the Java runtime to schedule the thread for concurrent execution. Calling run() directly does not create concurrent execution; it is an ordinary method call on the current thread:

thread.run(); // runs synchronously on the current thread

Use direct Thread construction when you need to demonstrate or control one specific thread. For application code that submits many independent tasks, an executor usually gives you a clearer lifecycle and resource policy.

How do I run multiple threads in Java?

Start several explicit threads

You can create a thread for each task and wait for each one to finish:

List<Thread> threads = new ArrayList<>();

for (int i = 0; i < 4; i++) {
    int taskNumber = i;
    Thread t = new Thread(() ->
        System.out.println("Task " + taskNumber), "worker-" + i);
    threads.add(t);
    t.start();
}

for (Thread t : threads) {
    try {
        t.join();
    } catch (InterruptedException e) {
        Thread.currentThread().interrupt();
        break;
    }
}

This approach exposes every thread and makes basic experiments easy. In a larger program, creating and coordinating threads yourself can complicate limits, error handling, result collection, and shutdown.

What’s actually slowing this PC down?

Pick the symptom - the matching free tool is one click away.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Use an ExecutorService for task management

An executor separates task submission from thread management. An ExecutorService accepts Runnable or Callable tasks, can return Future handles, and provides an explicit shutdown lifecycle.

Run tasks with a fixed pool

ExecutorService executor = Executors.newFixedThreadPool(4);

try {
    for (int i = 0; i < 10; i++) {
        int taskNumber = i;
        executor.submit(() ->
            System.out.println("Task " + taskNumber + " on "
                + Thread.currentThread().getName()));
    }
} finally {
    executor.shutdown();
}

A fixed pool caps the number of worker threads. If all workers are busy, additional submitted tasks wait in the executor’s queue instead of creating unlimited workers. The size is a workload and resource decision, not a universal constant: CPU-heavy work and I/O-waiting work have different needs.

Collect a result with Callable and Future

ExecutorService executor = Executors.newFixedThreadPool(2);

try {
    Future<Integer> result = executor.submit(() -> 21 * 2);
    System.out.println(result.get());
} catch (InterruptedException e) {
    Thread.currentThread().interrupt();
} catch (ExecutionException e) {
    System.err.println("Task failed: " + e.getCause());
} finally {
    executor.shutdown();
}

Future.get() waits for completion and reports task failure through ExecutionException. If the waiting thread is interrupted, preserve that signal by restoring its interrupt status, as shown above. Always arrange executor shutdown when the work is complete; otherwise its workers can keep the application alive.

What goes wrong with shared mutable state?

Multiple threads can read and write the same object. Without a correctness strategy, operations that appear simple can interfere: one thread can overwrite another thread’s update, or a thread can observe a value without the required visibility and ordering guarantees. These are concurrency errors even when each individual line looks valid.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Protect a critical section with synchronized

final class Counter {
    private int value;

    public synchronized void increment() {
        value++;
    }

    public synchronized int get() {
        return value;
    }
}

synchronized provides mutual exclusion for the methods shown and establishes the memory-consistency guarantees needed when threads enter and leave the monitor. Other choices include concurrent collections and atomic classes when their operation model matches the problem. Prefer reducing shared mutable state when possible.

Synchronization has a cost

Locking is a safety mechanism, not a free performance feature. Contended threads may wait or be suspended, reducing throughput. Keep critical sections small, avoid holding a lock while performing slow external work, and choose a concurrent abstraction that expresses the required operation instead of adding broad locks around unrelated code.

Platform threads and virtual threads

Traditional Java threads are platform threads tied to operating-system threads. They are appropriate for many workloads, especially when the number of simultaneously active threads is bounded and work is CPU-intensive.

Virtual threads are scheduled by the Java runtime rather than being permanently mapped one-for-one to an operating-system thread. They are designed for a thread-per-task style when tasks spend substantial time blocked on I/O. Their benefit is potential scale and throughput for many waiting tasks, not lower latency for one task or faster execution of CPU-bound code. As Oracle’s Java SE documentation puts it: “Virtual threads are not faster threads; they do not run code any faster than platform threads.”

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Start one virtual thread

In current Java SE documentation, Thread.ofVirtual() creates a virtual-thread builder:

Runnable task = () -> {
    // Mostly waiting work, such as a blocking I/O operation
    System.out.println(Thread.currentThread());
};

Thread.ofVirtual().start(task);

Use one virtual thread per submitted task

try (ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor()) {
    for (int i = 0; i < 1_000; i++) {
        int taskNumber = i;
        executor.submit(() -> {
            // A separate virtual thread is created for each submitted task
            process(taskNumber);
            return null;
        });
    }
}

This executor is not a conventional fixed-size pool: it creates a new virtual thread for each submitted task. The runtime can support very large numbers of virtual threads, but application limits still come from memory, databases, remote services, file descriptors, and other dependencies. Virtual threads do not remove the need to coordinate shared state or to limit access to scarce external resources.

Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Which approach should you choose?

Approach Control and lifecycle Best fit Main caution
Direct Thread Explicit thread creation, start, join, and interruption handling Learning, one-off workers, or cases needing direct thread control Manual management becomes difficult as task count and failure handling grow
Fixed ExecutorService Task submission, bounded worker count, queued work, Future results, and shutdown Predictable concurrency for CPU work or workloads needing a cap Tasks can queue when all workers are busy; shutdown is your responsibility
Virtual-thread-per-task executor Task submission with a new runtime-scheduled virtual thread per task Many independent tasks that spend substantial time waiting on I/O It does not speed CPU-bound code or replace limits around shared services

Choose based on control, resource model, workload, and coordination requirements. A platform-thread pool is a resource bound; a virtual-thread executor is a scalable task style, not a promise of unlimited throughput.

Practical multithreading checklist

  • Define work as a Runnable or Callable before choosing how to execute it.
  • Use start(), not a direct run() call, when you need concurrent execution.
  • Prefer an executor when tasks, results, worker limits, or shutdown need to be managed centrally.
  • Identify every piece of shared mutable state and protect it with synchronization or a suitable concurrent utility.
  • Handle interruption by restoring the interrupt status when you cannot fully handle it.
  • Shut down every ExecutorService after submitting work.
  • Use virtual threads for scale in mostly blocking workloads, not as a substitute for parallel CPU capacity.
  • Remember that the Oracle concurrency tutorials were written for JDK 8; consult current Java SE documentation for later APIs and behavior.

Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.