JUC

Li

Li Wei

March 12, 202613 min read

JUC

Preface

Learning videos: Mind maps:

Threads

Overview

Thread: A thread belongs to a process and is the basic CPU execution unit, the smallest unit of a program’s execution flow. A thread is an entity within a process and is the basic unit independently scheduled by the system. A thread itself does not own system resources; it only holds the minimal resources needed for execution and shares all resources of its process with other threads in the same process.

Purpose of threads: enable better concurrent execution of multiprograms, improve resource utilization and system throughput, and enhance the OS’s concurrency performance.

Concurrency vs. parallelism:

  • Parallelism: At a given moment, multiple instructions execute simultaneously on multiple CPUs.
  • Concurrency: Over a given interval, multiple instructions execute alternately on a single CPU.

Synchronous vs. asynchronous:

  • Synchronous: You must wait for a result before continuing.
  • Asynchronous: You can continue without waiting for a result.

Comparison with Processes

Process: A program is static; the running instance of a program is a process, which is the basic unit of resource allocation for the system.

Relationship: A process can contain multiple threads—that’s multithreading. For example, watching a video is a process; the video track, audio track, subtitles, ads, etc., are separate threads.

Thread vs. process:

  • Processes are largely independent, while threads exist inside a process and are a subset of it.

  • Processes own shared resources (e.g., memory space) that their internal threads share.

  • Inter‑process communication (IPC) is relatively complex:

    • Communication between processes on the same machine is called IPC (Inter‑process communication).

    • Semaphore: a counter used to control access to shared data by multiple processes, solving synchronization issues and preventing race conditions.

    • Shared memory: multiple processes can access the same memory region; semaphores are needed to synchronize that access.

    • Pipe communication: a pipe is a shared file that connects a reading process and a writing process for communication. Only one process can access the pipe at a time, so it supports half‑duplex communication.

      • Anonymous pipes: used for communication between parent‑child processes or sibling processes that have a familial relationship.
      • Named pipes: appear as files on disk and can enable communication between any two processes on the same machine, following FIFO semantics.
    • Message queues: a kernel‑resident linked list of messages identified by a queue ID, providing full‑duplex communication between processes. Compared with pipes:

      • Anonymous pipes exist as in‑memory files; named pipes exist on actual disk or a file system; message queues reside in the kernel and are only truly removed when the OS restarts or the queue is explicitly deleted.
      • A reading process can selectively receive messages by type, unlike FIFO which always receives in order.
  • Communication between processes on different computers requires a network and a common protocol such as HTTP.

  • Sockets: unlike other IPC mechanisms, sockets can be used for communication between different machines.

  • Thread communication is simpler because threads share the same process memory; for example, multiple threads can access the same shared variable.

  • Java communication mechanisms: volatile, wait/notify, join, InheritableThreadLocal, MappedByteBuffer.

  • Threads are lighter weight; context‑switching cost for threads is generally lower than for processes.

Thread Types

User threads

These are the worker threads that perform the business logic required by the program.

Daemon threads

A daemon thread is a special kind of thread in Java whose lifecycle depends on non‑daemon (user) threads. When all user threads finish, the JVM automatically exits and stops all daemon threads. The garbage‑collector thread is the most typical example.

Code example:

Creating Threads

Thread

Creating a thread with Thread: define a subclass, or use an anonymous inner class.

  • The start() method actually registers the thread with the CPU and triggers the execution of run().
  • A thread must be started with start(). Calling run() directly just executes the method in the current (usually main) thread.
  • It’s advisable to create child threads first and let the main thread do its work afterward; otherwise the main thread (main) will always finish first.

Thread constructors:

  • public Thread()
  • public Thread(String name)

Code example:

Pros and cons of extending Thread:

  • Pros: simple to code.
  • Cons: because the thread class already extends Thread, it cannot extend any other class, limiting functionality (single inheritance limitation).
Runnable

Creating a thread with Runnable: define a class that implements Runnable, or use an anonymous inner class.

Thread constructors for Runnable:

  • public Thread(Runnable target)
  • public Thread(Runnable target, String name)

Code example:

The Thread class itself implements Runnable; it holds a Runnable reference and, under the hood, its run() method invokes Runnable#run.

Pros and cons of the Runnable approach:

  • Cons: code is slightly more complex.
  • Pros:
    • The task class only implements Runnable, so it can still extend another class, avoiding the single‑inheritance limitation.
    • The same task object can be wrapped by multiple Thread objects.
    • Suitable for many threads sharing the same resource.
    • Decouples task code from thread code; the task can be reused by multiple threads.
    • Thread pools can accept Runnable or Callable task objects.
Callable

Implementing the Callable interface:

  • Define a task class that implements Callable<T> and declares the result type T.
  • Override the call() method, which can directly return a result.
  • Create an instance of the Callable task.
  • Wrap the Callable instance into a FutureTask.
  • Wrap the FutureTask into a Thread.
  • Call the thread’s start() method to launch it.

public FutureTask(Callable<V> callable): a FutureTask; after the thread finishes, it holds the thread’s result.

  • FutureTask implements Runnable because Thread can only execute Runnable objects, so we wrap the Callable in a FutureTask.
  • The thread‑pool section later examines the source code of FutureTask.

public V get(): synchronously wait for the task’s result; if a thread tries to obtain another thread’s result, it blocks until the result is ready—used for thread synchronization.

  • get() blocks the calling thread until the task completes.
  • After run() finishes, it stores the result in a member of FutureTask; get() can then retrieve that value.

Differences between Runnable and Callable:

  • Runnable.run() has no return value; Callable.call() returns a value that must be obtained via FutureTask.
  • Callable.call() may throw checked exceptions; Runnable.run() cannot propagate exceptions outward.

Code example:

Pros and cons:

  • Pros: like Runnable, but you can obtain the thread’s result.
  • Cons: more complex code.

Viewing Threads

Windows
  • Task Manager shows process and thread counts; you can also kill processes (Ctrl + Shift + Esc).
  • tasklist – list processes.
  • taskkill – terminate a process.
Linux
  • ps -fe – list all processes.
  • ps -fT -p <PID> – list all threads of a given process.
  • kill – terminate a process.
  • top – press uppercase H to toggle thread display.
  • top -H -p <PID> – show all threads of a specific process.
Java
  • jps – list all Java processes.
  • jstack <PID> – display thread states of a Java process.
  • jconsole – graphical tool to monitor thread activity in a Java process.

Thread Methods

API

Thread class API:

Method Description
public void start() Starts a new thread; the JVM calls the thread’s run method.
public void run() Invoked after the thread starts.
public void setName(String name) Sets the thread’s name.
public String getName() Retrieves the thread’s name. (Default names: Thread‑<index> for child threads, main for the main thread.)
public static Thread currentThread() Returns the currently executing thread object.
public static void sleep(long time) Sleeps the current thread for the specified milliseconds. Thread.sleep(0) yields the CPU immediately.
public static native void yield() Hints to the scheduler to give up the CPU.
public final int getPriority() Returns the thread’s priority.
public final void setPriority(int priority) Sets the thread’s priority (commonly 1, 5, 10).
public void interrupt() Interrupts the thread (exception‑handling mechanism).
public static boolean interrupted() Checks and clears the interrupt status of the current thread.
public boolean isInterrupted() Checks the interrupt status without clearing it.
public final void join() Waits for this thread to finish.
public final void join(long millis) Waits up to millis milliseconds (0 means wait forever).
public final native boolean isAlive() Returns true if the thread has not yet terminated.
public final void setDaemon(boolean on) Marks the thread as a daemon (true) or user thread (false).
run & start
  • run: the thread body containing the code to execute. When run returns, the thread ends. Calling run directly executes it in the current (main) thread; no new thread is created. It can be called repeatedly.
  • start: creates a new thread, puts it in the runnable state, and eventually causes run to execute in that new thread. Can be called only once.

Note: these are thread‑control resources.

Exceptions cannot be thrown out of run(); they must be caught locally because the superclass does not declare any checked exceptions, and exceptions cannot propagate across thread boundaries to main().

sleep & yield

sleep:

  • Invoking sleep moves the current thread from Running to Timed Waiting (blocked) state.
  • While sleeping, the thread does not release any object locks it holds.
  • Another thread can interrupt a sleeping thread; sleep then throws InterruptedException.
  • After waking, the thread may not run immediately—it must reacquire the CPU.
  • Prefer TimeUnit‑based sleep (e.g., TimeUnit.SECONDS.sleep(1)) for readability.

yield:

  • Hints to the scheduler to give up the CPU.
  • Actual behavior depends on the OS scheduler.
  • CPU time is relinquished, but locks are not released.
join

public final void join(): wait for this thread to finish.

Mechanism: the caller repeatedly checks the thread’s alive status; t1.join() is essentially equivalent to:

  • The join method is synchronized; internally it calls wait(), which releases the current thread’s object lock, not any external lock.
  • When a thread calls t1.join(), t1 continues to hold the CPU until it terminates.

Thread synchronization:

  • join provides synchronization because it blocks until another thread ends before proceeding.
  • It requires shared external variables, which may violate encapsulation.
  • It forces a thread to finish, so it doesn’t work well with thread pools.
  • Future provides a similar synchronous pattern: get() blocks until the result is ready.
interrupt

Interrupt a thread public void **interrupt** (): triggers the thread’s interrupt handling mechanism.

public static boolean **interrupted** (): checks whether the current thread is interrupted; if so, returns true and clears the interrupt flag. Consecutive calls will then return false.

public boolean **isInterrupted** (): checks the interrupt status without clearing the flag.

Interrupting a thread causes a context switch; the OS saves the thread’s state, and when the thread is scheduled again it resumes from the interruption point (interrupt does not stop execution).

  • sleep, wait, and join all move the thread to a blocked state; interrupting them clears the interrupt status (sets it to false).
  • Interrupting a normally running thread does not clear the interrupt status (remains true).

Interrupting park (a low‑level blocking call similar to sleep): interrupting a parked thread does not clear the interrupt status (remains true). If the interrupt flag is already true, park returns immediately. You can clear the flag using Thread.interrupted().

Termination patterns – Two‑Phase Termination

Goal: gracefully stop thread T2 from within thread T1, giving T2 a chance to clean up.

Wrong approaches:

  • Calling stop() on a thread: this kills the thread abruptly; if the thread holds a lock on a shared resource, the lock is never released, causing other threads to deadlock forever.
  • Calling System.exit(int): intended to stop a single thread, but actually terminates the entire JVM.

Correct approach (Two‑Phase Termination):

  1. Phase 1 – Notify: set a flag or call interrupt() to signal that the thread should stop. The thread can decide, based on its business logic, whether to exit immediately.
  2. Phase 2 – Cleanup: if the thread does not exit right away, it performs necessary cleanup (release resources, reset state) before terminating. This is typically done inside the run method.

Diagram (omitted).

Because a thread can be interrupted at any time, you must handle interruption at every possible blocking point.

Daemon

public final void setDaemon(boolean on): if true, marks the thread as a daemon.

Call this before the thread is started.

  • User thread: a regular thread created by the application.
  • Daemon thread: serves user threads; when all user threads finish, the JVM forces daemon threads to stop even if they haven’t completed. A daemon process is detached from the terminal and runs in the background, preventing its output from appearing on the terminal.

Note: If only daemon threads remain, the JVM exits because there are no user threads to keep it alive.

Common daemon threads:

  • The garbage‑collector thread.
  • In Tomcat, the Acceptor and Poller threads are daemons, so Tomcat does not wait for them to finish processing current requests after receiving a shutdown command.
Deprecated methods (not recommended)

These methods are obsolete and can break synchronized blocks, leading to deadlocks:

  • stop(): brutally terminates a thread; if the thread holds a JUC lock, the lock may never be released, causing other threads to wait forever.
  • suspend() / resume(): if a suspended thread holds a lock, no other thread can acquire that lock until the thread is resumed. If the resuming thread tries to access the same resource before calling resume(), a deadlock occurs.

Thread States

Operating‑system‑style states: created, ready, running, blocked, terminated.

In Java, a thread’s full lifecycle is represented by the Thread.State enum java.lang.Thread.State, which defines six states:

State When it occurs
NEW Thread object created but start() not yet called.
RUNNABLE (called RUNNABLE in Java, sometimes “ready”) The thread is eligible to run; it may be actually running or waiting for CPU time.
BLOCKED The thread is trying to acquire an object lock that another thread holds.
WAITING The thread is waiting indefinitely for another thread to perform a specific action (e.g., Object.wait() without timeout).
TIMED_WAITING The thread is waiting for a specified time (e.g., sleep, wait with timeout, join with timeout).
TERMINATED The thread’s run method has completed.

(The rest of the original content continues with detailed explanations of each state, transitions, and examples.)


Originally written by Li Wei (李唯_) and published in Chinese on 后端技术栈全书 (Full-Stack Backend Engineering). Translated and adapted for DriftSeas with permission.

Keep reading

More related articles from DriftSeas.