Fundamentals 21 min read

Java Multithreading and Concurrency Basics: Threads, Synchronization, Thread Pools, and Common Pitfalls

This article introduces the fundamentals of Java multithreading and concurrency, covering the differences between processes and threads, the relationship between Thread and Runnable, start vs run, thread creation methods, handling return values, thread states, sleep vs wait, notify vs notifyAll, yield, interrupt, and the use of thread pools with executors.

Selected Java Interview Questions
Selected Java Interview Questions
Selected Java Interview Questions
Java Multithreading and Concurrency Basics: Threads, Synchronization, Thread Pools, and Common Pitfalls

This article provides a beginner‑friendly overview of Java multithreading and concurrency.

Process vs. Thread – Early computers executed tasks serially; processes introduced independent memory spaces, while threads share a process’s memory for finer‑grained parallelism.

Java Process‑Thread Relationship – Running a Java program creates one JVM process containing at least one thread (the main thread). The JVM instance is shared among all threads.

Thread.start() vs. Thread.run() – start() creates a new OS thread and then invokes run(); calling run() directly executes the method in the current thread.

Thread vs. Runnable

Thread is a concrete class that implements Runnable. Implementing Runnable is preferred because Java only supports single inheritance.

Creating Threads

1. Extending Thread:

public class Main extends Thread {
    public static void main(String[] args) {
        Main main = new Main();
        main.start();
    }
    @Override
    public void run() {
        System.out.println("Thread created by extending Thread, name: " + Thread.currentThread().getName());
    }
}

2. Implementing Runnable:

public class Main {
    public static void main(String[] args) {
        SubThread subThread = new SubThread();
        Thread thread = new Thread(subThread);
        thread.start();
    }
}
class SubThread implements Runnable {
    @Override
    public void run() {
        System.out.println("Thread created by implementing Runnable, name: " + Thread.currentThread().getName());
    }
}

3. Anonymous inner class:

Thread thread = new Thread(new Runnable() {
    @Override
    public void run() {
        System.out.println("Thread created by anonymous class, name:" + Thread.currentThread().getName());
    }
});
thread.start();

Getting a Thread’s Return Value

Busy‑wait loop (not recommended): a static variable is set in run() and the main thread spins until it is non‑null. Thread.join() – blocks the calling thread until the target thread finishes. Callable + Future – the task returns a value that can be retrieved with future.get(). FutureTask – a concrete implementation of Future that can wrap a Callable.

Thread pool submission – ExecutorService.submit(Callable) returns a Future for the result.

Thread States – New, Runnable (Ready/Running), Waiting, Timed Waiting, Blocked, and Terminated.

Sleep vs. Wait – sleep() is a static method of Thread, does not release locks, and can be called anywhere; wait() is an instance method of Object, must be called inside a synchronized block, releases the monitor, and can be awakened by notify() or notifyAll().

notify vs. notifyAll – notify() wakes a single waiting thread (chosen arbitrarily); notifyAll() moves all waiting threads to the lock‑acquisition queue.

Thread.yield() – hints to the scheduler that the current thread is willing to give up its CPU slice; the scheduler may ignore the hint.

Thread.interrupt() – sets the interrupt flag; if the thread is blocked (e.g., in wait() or sleep()) it throws InterruptedException. Otherwise the flag can be checked via Thread.interrupted().

Thread Pools

Creating pools with Executors: newFixedThreadPool(int n) – fixed number of worker threads. newCachedThreadPool() – creates new threads as needed and reuses idle ones. newSingleThreadExecutor() – a single worker thread guaranteeing task order. newScheduledThreadPool(int core) / newSingleThreadScheduledExecutor() – for delayed or periodic tasks. newWorkStealingPool() – builds a ForkJoinPool that uses work‑stealing for parallelism.

Thread‑pool sizing guidelines:

CPU‑bound tasks: threads = number of CPU cores (+1).

I/O‑bound tasks: threads = cores * (1 + waitTime / workTime).

When a task is submitted, ThreadPoolExecutor decides whether to create a new thread, queue the task, or invoke a rejection handler (e.g., AbortPolicy, CallerRunsPolicy, DiscardPolicy, etc.).

Conclusion – The article covers essential Java concurrency concepts suitable for beginners and points to further resources for deeper study.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

JavaconcurrencyThreadPoolThreadCallable
Selected Java Interview Questions
Written by

Selected Java Interview Questions

A professional Java tech channel sharing common knowledge to help developers fill gaps. Follow us!

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.