Fundamentals 81 min read

Master Java Concurrency: 60+ Interview Q&A on Threads, Locks & Thread Pools

This comprehensive guide explores Java concurrency fundamentals, covering thread creation, lifecycle, synchronization mechanisms, lock implementations, thread-local storage, memory model, atomic classes, common concurrency utilities, and detailed explanations of thread pools, their configurations, states, and best practices, accompanied by over sixty interview-style questions and code examples.

Su San Talks Tech
Su San Talks Tech
Su San Talks Tech
Master Java Concurrency: 60+ Interview Q&A on Threads, Locks & Thread Pools

Fundamentals

1. What is the difference between parallelism and concurrency?

From the operating system perspective, a thread is the smallest unit of CPU allocation.

Parallelism means that at the same moment two threads are executing, which requires two CPUs to execute the two threads.

Concurrency means that at the same moment only one thread is executing, but within a time interval both threads have executed. Concurrency relies on CPU thread switching; because the switching time is extremely short, users feel no difference.

Parallel and concurrent
Parallel and concurrent

Parallel and concurrency analogy: going to the cafeteria to get food. Parallelism is like multiple windows serving at the same time; concurrency is like a single window serving one plate after another.

Parallel and concurrent in cafeteria
Parallel and concurrent in cafeteria

2. What are processes and threads?

To talk about threads, we must first talk about processes.

Process: a process is a run of code on a set of data; it is the basic unit of resource allocation and scheduling by the system.

Thread: a thread is an execution path within a process. A process has at least one thread, and multiple threads in a process share the process's resources.

The operating system allocates resources to processes, but CPU resources are special: they are allocated to threads because the actual execution on the CPU is performed by threads. Therefore, a thread is considered the basic unit of CPU allocation.

In Java, when we start the main function, we actually start a JVM process, and the thread that runs main is the main thread of that process.

Process and thread relationship
Process and thread relationship

A process can have multiple threads; the threads share the process's heap and method area, but each thread has its own program counter and stack.

3. What are the ways to create a thread in Java?

Java provides three main ways to create a thread:

Extend the Thread class, override run(), and call start() to launch the thread.

public class ThreadTest {

    /**
     * Inherit Thread class
     */
    public static class MyThread extends Thread {
        @Override
        public void run() {
            System.out.println("This is child thread");
        }
    }

    public static void main(String[] args) {
        MyThread thread = new Thread();
        thread.start();
    }
}

Implement the Runnable interface and override run().

public class RunnableTask implements Runnable {
    public void run() {
        System.out.println("Runnable!");
    }

    public static void main(String[] args) {
        RunnableTask task = new RunnableTask();
        new Thread(task).start();
    }
}

Both of the above approaches have no return value. If we need to obtain a result from a thread, we can use the Callable interface.

Implement Callable, override call(). The result can be obtained via FutureTask.

public class CallerTask implements Callable<String> {
    public String call() throws Exception {
        return "Hello, I am running!";
    }

    public static void main(String[] args) {
        FutureTask<String> task = new FutureTask<>(new CallerTask());
        new Thread(task).start();
        try {
            String result = task.get();
            System.out.println(result);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
    }
}
Thread creation methods
Thread creation methods

4. Why does calling start() execute run() , and why not call run() directly?

The JVM executes start() by first creating a new thread; the newly created thread then executes the thread's run() method, achieving multithreading.

start method
start method

Calling run() directly runs the method in the main thread, which is sequential execution and does not achieve multithreading.

5. Common thread scheduling methods

Thread waiting and notification methods are defined in Object: wait(): The calling thread is blocked until one of the following occurs:

The thread's shared object receives notify() or notifyAll().

Another thread calls interrupt() on the waiting thread, causing an InterruptedException. wait(long timeout): Similar to wait() but with a timeout. If the thread is not awakened within the specified time, it returns automatically. wait(long timeout, int nanos): Internally calls wait(long timeout).

Notification methods: notify(): Wakes up one thread that is waiting on the object's monitor. notifyAll(): Wakes up all threads waiting on the object's monitor.

The Thread class also provides join() to wait for a thread to finish.

6. Thread sleep

sleep(long millis)

is a static method of Thread. When a thread calls sleep, it temporarily yields the CPU for the specified time while retaining any monitors it holds.

7. Thread states

Java threads have six states:

State

Description

NEW

Thread created but start() not yet called.

RUNNABLE

Running state (includes ready and running in the OS).

BLOCKED

Blocked on a lock.

WAITING

Waiting for a specific condition (e.g., wait()).

TIME_WAITING

Waiting with a timeout.

TERMINATED

Thread has completed execution.

Thread state diagram
Thread state diagram

8. What is a thread context switch?

Multithreading aims to fully utilize the CPU. In a concurrent scenario, a single CPU handles multiple threads by time-slicing. When a thread's time slice ends, the scheduler switches to another thread, which is a context switch.

Context switch timing
Context switch timing

9. Daemon threads

Java threads are divided into daemon threads and user threads. The JVM starts the main thread (a user thread) when the program begins. The JVM also starts many daemon threads internally, such as the garbage collector thread.

When the last user thread finishes, the JVM exits regardless of any remaining daemon threads.

10. Thread communication methods

Common communication mechanisms include:

volatile and synchronized : volatile ensures visibility of variable changes across threads; synchronized ensures both visibility and mutual exclusion.

wait/notify : Threads can use wait() and notify() / notifyAll() to coordinate state changes.

Piped streams : PipedInputStream, PipedOutputStream, PipedReader, and PipedWriter provide in-memory data transfer between threads.

Thread.join() : Allows one thread to wait for another to finish.

ThreadLocal : Provides thread‑local storage for data such as user context.

ThreadLocal

ThreadLocal is a thread‑local variable. Each thread accessing a ThreadLocal gets its own independent copy, preventing data races.

11. What is ThreadLocal?

ThreadLocal is a thread‑local variable. When a ThreadLocal variable is created, each thread that accesses it gets its own copy stored in the thread's local memory.

ThreadLocal diagram
ThreadLocal diagram
public static ThreadLocal<String> localVariable = new ThreadLocal<>();

Set value: localVariable.set("User123"); Get value:

String value = localVariable.get();

12. How is ThreadLocal implemented?

The Thread class contains a ThreadLocalMap field named threadLocals. Each entry in the map holds a weak reference to the ThreadLocal key and a strong reference to the value.

public class Thread implements Runnable {
    ThreadLocal.ThreadLocalMap threadLocals = null;
}

When set() is called, the current thread obtains its ThreadLocalMap, creates an Entry if necessary, and stores the value.

public void set(T value) {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
}

13. ThreadLocal memory leak

ThreadLocalMap uses weak references for keys. If a ThreadLocal object becomes unreachable, its key can be garbage‑collected while the value remains, causing a memory leak.

Solution: call remove() when the ThreadLocal is no longer needed.

ThreadLocal<String> localVariable = new ThreadLocal<>();
try {
    localVariable.set("User123");
    // ...
} finally {
    localVariable.remove();
}

Java Memory Model (JMM)

The Java Memory Model abstracts the relationship between threads and main memory. Shared variables reside in main memory; each thread has a private working memory that caches copies of variables.

JMM diagram
JMM diagram

Atomicity, Visibility, Ordering

Atomicity : An operation is indivisible; it either completes fully or not at all.

Visibility : When one thread modifies a variable, other threads can immediately see the change.

Ordering : Within a single thread, statements execute in program order, but the JVM may reorder instructions for performance as long as the as‑if‑serial semantics are preserved.

Instruction Reordering

Reordering can occur at three levels: compiler optimization, instruction‑level parallelism, and memory system reordering. The JMM defines happens‑before and as‑if‑serial rules to constrain reordering.

Instruction reordering levels
Instruction reordering levels

Volatile

volatile

guarantees visibility and ordering by inserting memory barriers before and after volatile reads/writes.

Volatile memory barrier
Volatile memory barrier

Synchronized

synchronized

provides mutual exclusion, visibility, ordering, and reentrancy. The JVM implements it using monitorenter/monitorexit bytecode instructions or the ACC_SYNCHRONIZED flag for synchronized methods.

monitorenter and monitorexit
monitorenter and monitorexit

Lock Upgrade

Lock states transition from no lock → biased lock → lightweight lock → heavyweight lock. The transition is generally irreversible.

Lock upgrade
Lock upgrade

ReentrantLock

ReentrantLock is an explicit lock built on top of AQS. It can be fair or non‑fair.

ReentrantLock lock = new ReentrantLock();
lock.lock();
try {
    // critical section
} finally {
    lock.unlock();
}

Fair vs Non‑fair

Fair locks grant access in FIFO order; non‑fair locks may acquire the lock immediately if it is free, improving throughput but risking starvation.

CAS (Compare‑And‑Swap)

CAS atomically compares a memory location with an expected value and, if they match, updates it to a new value. It solves many concurrency problems but suffers from ABA, livelock, and single‑variable limitation.

ABA Problem

Solution: use versioned references such as AtomicStampedReference or add a version counter.

Atomic Classes

Java provides atomic wrappers for primitive types, arrays, references, and fields (e.g., AtomicInteger, AtomicReferenceArray, AtomicIntegerFieldUpdater).

AtomicInteger Implementation

Uses Unsafe.getAndAddInt, which internally performs a CAS loop.

public final int getAndIncrement() {
    return unsafe.getAndAddInt(this, valueOffset, 1);
}

Deadlock

A deadlock occurs when two or more threads wait for each other’s resources, forming a circular wait. The four necessary conditions are mutual exclusion, hold‑and‑wait, no preemption, and circular wait.

Deadlock diagram
Deadlock diagram

To avoid deadlock, break at least one of the conditions, e.g., acquire all needed resources at once, use lock ordering, or employ timeout/retry strategies.

Concurrency Utilities

CountDownLatch

Allows one or more threads to wait until a set of operations complete.

CountDownLatch latch = new CountDownLatch(5);
new Thread(latch::countDown).start();
// ...
latch.await();

CyclicBarrier

Allows a group of threads to wait for each other at a common barrier point, reusable for multiple phases.

CyclicBarrier barrier = new CyclicBarrier(5);
barrier.await();

Semaphore

Controls the number of concurrent accesses to a resource.

Semaphore semaphore = new Semaphore(10);
semaphore.acquire();
// critical section
semaphore.release();

Exchanger

Provides a synchronization point where two threads can exchange data.

Exchanger<String> exchanger = new Exchanger<>();
String received = exchanger.exchange("data");

Thread Pools

A thread pool manages a set of reusable worker threads, reducing the overhead of thread creation and destruction, improving response time, and enabling task reuse.

Thread pool
Thread pool

Core Parameters

corePoolSize : Number of core threads that are always kept alive.

maximumPoolSize : Maximum number of threads (core + non‑core).

keepAliveTime : Time that excess idle threads wait before terminating.

unit : Time unit for keepAliveTime.

workQueue : Queue that holds pending tasks.

threadFactory : Factory to create new threads.

handler : Rejection policy when the pool cannot accept new tasks.

Rejection Policies

AbortPolicy

: Throws a RejectedExecutionException (default). CallerRunsPolicy: Executes the task in the calling thread. DiscardOldestPolicy: Discards the oldest task in the queue. DiscardPolicy: Silently discards the new task.

Work Queues

ArrayBlockingQueue

: Bounded FIFO queue. LinkedBlockingQueue: Optionally bounded FIFO queue (default unbounded). DelayQueue: Queue of delayed tasks. PriorityBlockingQueue: Unbounded priority queue. SynchronousQueue: No storage; each insert must wait for a corresponding remove.

execute vs submit

execute(Runnable)

: Submits a task without a return value. submit(Callable) or submit(Runnable): Returns a Future to obtain the result or check completion.

Shutdown

shutdown()

: No new tasks are accepted; existing tasks continue. shutdownNow(): Attempts to stop all executing tasks and returns a list of pending tasks.

Thread Pool States

The pool transitions through RUNNING, SHUTDOWN, STOP, TIDYING, and TERMINATED.

Thread pool state diagram
Thread pool state diagram

Common Thread Pool Types

newSingleThreadExecutor

: One core thread, unbounded queue. newFixedThreadPool: Fixed number of core threads, unbounded queue. newCachedThreadPool: No core threads, unlimited maximum, uses SynchronousQueue, idle threads terminate after 60 seconds. newScheduledThreadPool: Supports delayed and periodic tasks, uses DelayedWorkQueue.

Thread Pool Tuning

Choose pool size based on workload type:

CPU‑bound: CPU cores + 1.

IO‑bound: CPU cores * 2 or higher.

Monitor pool metrics, adjust parameters dynamically via configuration centers (e.g., Nacos, Apollo) or custom ThreadPoolExecutor subclasses.

Fork/Join Framework

The Fork/Join framework splits a large task into smaller subtasks, processes them in parallel, and combines the results. It uses work‑stealing queues to balance load among worker threads.

Fork/Join divide‑and‑conquer
Fork/Join divide‑and‑conquer

Example: Sum of 1 to N

public class CountTask extends RecursiveTask<Integer> {
    private static final int THRESHOLD = 16;
    private int start;
    private int end;

    public CountTask(int start, int end) {
        this.start = start;
        this.end = end;
    }

    @Override
    protected Integer compute() {
        int sum = 0;
        boolean canCompute = (end - start) <= THRESHOLD;
        if (canCompute) {
            for (int i = start; i <= end; i++) {
                sum += i;
            }
        } else {
            int middle = (start + end) / 2;
            CountTask leftTask = new CountTask(start, middle);
            CountTask rightTask = new CountTask(middle + 1, end);
            leftTask.fork();
            rightTask.fork();
            int leftResult = leftTask.join();
            int rightResult = rightTask.join();
            sum = leftResult + rightResult;
        }
        return sum;
    }

    public static void main(String[] args) {
        ForkJoinPool forkJoinPool = new ForkJoinPool();
        CountTask task = new CountTask(1, 100);
        Future<Integer> result = forkJoinPool.submit(task);
        try {
            System.out.println(result.get());
        } catch (InterruptedException | ExecutionException e) {
            e.printStackTrace();
        }
    }
}

References

[1] 《Java并发编程的艺术》

[2] 《Java并发编程实战》

[3] 80+ Java multithreading interview questions (link)

[4] 《我想进大厂》

[5] Java concurrency mind map (link)

[6] 极客时间《Java并发编程实战》

[7] 《Java并发编程之美》

[8] ReentrantLock and AQS deep dive (link)

[9] 《深入理解Java虚拟机》

[10] Blocking queue implementation (link)

[11] Thread pool deep dive (link)

[12] Thread pool analysis (link)

[13] Real‑world thread pool case study (link)

[14] 《Java面经手册》

[15] Meituan thread pool practice (link)

[16] ThreadLocal source analysis (link)

[17] JMM explanation (link)

[18] 《王者并发课》

[19] Synchronized lock upgrade details (link)

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.

JavaconcurrencyThreadPoolThreadSynchronizationmultithreadingLock
Su San Talks Tech
Written by

Su San Talks Tech

Su San, former staff at several leading tech companies, is a top creator on Juejin and a premium creator on CSDN, and runs the free coding practice site www.susan.net.cn.

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.