multithreading

Published 2026-07-29 09:17 Updated 2026-07-29 09:17 2863 words 15 min read ... Page views

This article systematically introduces the basic concepts of multithreading, thread creation and management, life cycle, thread safety and synchronization mechanisms, deadlock avoidance, thread collaboration models, and the use of thread pools. It focuses on the differences between threads and processes, concurrency and parallelism, explains in detail synchronization mechanisms such as synchronization and wait/notify, emphasizes thread safety, deadlock prevention and reasonable configuration of thread pools, points out that sleep should be avoided to implement scheduled tasks, recommends using ScheduledExecutorService, and recommends correctly waiting for tasks to complete through methods such as Future and awaitTermination.

multithreading

basic concepts

process

A process is a running program instance that has relatively independent memory space and system resources. The operating system is responsible for scheduling multiple processes to execute on the CPU.

thread

A thread is a unit of execution in a process. A process can contain multiple threads that share heap memory and some resources in the process, but each thread has its own thread-private data such as program counters and virtual machine stacks.

Multiple threads are usually executed alternately on a single-core CPU, but may actually execute in parallel on a multi-core CPU. The specific scheduling of threads is completed jointly by the operating system and the JVM, and the program cannot rely on a fixed execution order.

Concurrency and parallelism

  • Concurrency means that multiple tasks are advancing alternately over a period of time.
  • Parallel means that multiple tasks are executed by different processor cores at the same time.

create threads

Inheriting Thread class

Define Thread subclass and rewrite run(), and then call start() to start the thread.

public class MyThread extends Thread {
    public MyThread(String name) {
        super(name);
    }

    @Override
    public void run() {
        for (int i = 1; i <= 10; i++) {
            System.out.println(getName() + " 执行第 " + i + " 次任务");
        }
    }
}
public class Main {
    public static void main(String[] args) {
        Thread thread1 = new MyThread("A");
        Thread thread2 = new MyThread("B");
        Thread thread3 = new MyThread("C");

        thread1.start();
        thread2.start();
        thread3.start();
    }
}

A direct call to run() is just a normal method call and will not create a new execution thread. A Thread object can only successfully call start() once.

Implement Runnable interface

Separating tasks from thread objects is a more common approach. The task class implements Runnable, and then transfers the task object to Thread.

public class TicketTask implements Runnable {
    private final String stationName;

    public TicketTask(String stationName) {
        this.stationName = stationName;
    }

    @Override
    public void run() {
        for (int i = 1; i <= 10; i++) {
            System.out.println(
                    stationName + ",窗口 "
                            + Thread.currentThread().getName()
                            + " 执行第 " + i + " 次任务"
            );
        }
    }
}
public class Main {
    public static void main(String[] args) {
        Runnable task1 = new TicketTask("哈西站");
        Runnable task2 = new TicketTask("哈尔滨站");

        new Thread(task1, "A").start();
        new Thread(task1, "B").start();
        new Thread(task2, "C").start();
    }
}

The advantages of using Runnable are as follows:

  • Task classes can still inherit from other classes.
  • The same task object can be handed over to multiple threads for execution, making it easy to share task state.
  • Tasks are separated from the thread life cycle and are easier to hand over to the thread pool for management.

Sharing task objects also shares mutable fields within them, so thread safety must be considered.

thread life cycle

Java Thread.State defines six thread states:

  • NEW: The thread object has been created, but start() has not been called.
  • RUNNABLE: The thread is running, or it has run conditions and is waiting for CPU scheduling.
  • BLOCKED: Waiting to enter a synchronized monitor.
  • WAITING: Wait indefinitely for other threads to perform specific operations, such as wait() or join() without timeout.
  • TIMED_WAITING: Wait within the specified time, such as sleep(), wait() with timeout, or join().
  • TERMINATED: run() ended normally or ended due to no captured anomaly.

There is no separate RUNNING enumeration in Java state, and the executing thread also belongs to RUNNABLE.

image-001
image-001

Common Thread Methods

currentThread()

Returns the thread object that is currently executing code.

System.out.println(Thread.currentThread().getName());

sleep()

Let the current thread sleep for at least a specified time and enter TIMED_WAITING. The actual recovery time may be later than the specified time.

try {
    Thread.sleep(1000);
} catch (InterruptedException e) {
    Thread.currentThread().interrupt();
}

When receiving an interrupt, sleep() throws InterruptedException and clears the interrupt flag. When processing cannot continue, the interrupt state should usually be restored rather than just printing exceptions.

Timing tasks should not be roughly realized through infinite loop lengthening time sleep(). ScheduledExecutorService is recommended.

ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();
scheduler.scheduleAtFixedRate(
        () -> System.out.println("执行备份任务"),
        0,
        24,
        TimeUnit.HOURS
);

If it is required to perform at local time every day, the first delay needs to be calculated based on the current time, taking into account time zone and daylight saving time.

yield()

Thread.yield() prompts the scheduler that the current thread is willing to give up execution opportunities, but the scheduler can ignore the prompt. It cannot be used to achieve reliable thread collaboration.

join()

After the current thread calls another thread’s join(), it will wait for the target thread to finish.

public class Main {
    public static void main(String[] args) {
        Runnable task = () -> System.out.println(
                Thread.currentThread().getName() + " 完成任务"
        );

        Thread thread1 = new Thread(task, "A");
        Thread thread2 = new Thread(task, "B");
        Thread thread3 = new Thread(task, "C");

        thread1.start();
        thread2.start();
        thread3.start();

        try {
            thread1.join();
            thread2.join();
            thread3.join();
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            return;
        }

        System.out.println("所有任务执行结束");
    }
}

daemon thread

Calling setDaemon(true) can set the thread as a guardian thread before it starts.

Thread worker = new Thread(() -> {
    while (!Thread.currentThread().isInterrupted()) {
        // 提供后台服务
    }
});
worker.setDaemon(true);
worker.start();

When only guard threads remain in the JVM, the JVM can exit directly. The guard thread does not specifically bind and guard an ordinary thread, nor can it be relied on to perform necessary save or cleanup work.

thread priority

setPriority() sets thread priority prompts from Thread.MIN_PRIORITY to Thread.MAX_PRIORITY. Different operating systems and JVMs treat priorities differently, and it cannot be used to ensure execution order or fairness.

Thread safety and race conditions

When multiple threads read and write shared variable data at the same time, if the operation lacks proper synchronization, the result may depend on an unpredictable execution order, which is called a race condition.

For example, money += 1000 is not an indivisible single step; it usually involves reading, calculating, and writing back.

synchronized keyword

synchronized uses an object monitor for mutual exclusion and visibility. Before entering the synchronization area, the thread must first obtain the corresponding monitor; the monitor is released when exiting the synchronization area.

Synchronization instance method

The synchronization instance method locks the current object, which is this.

public class Account {
    private int money = 20_000;

    public synchronized void deposit(int amount) {
        money += amount;
        System.out.println("存款后余额:" + money);
    }

    public synchronized void withdraw(int amount) {
        if (money < amount) {
            throw new IllegalStateException("余额不足");
        }
        money -= amount;
        System.out.println("取款后余额:" + money);
    }
}

Only synchronization codes that lock the same object are mutually exclusive. If two threads call methods on two different Account objects, they are not using the same lock.

synchronized code block

Synchronizing blocks of code narrows the scope of the lock and explicitly specifies the lock target.

public void deposit(int amount) {
    synchronized (this) {
        money += amount;
        System.out.println("存款后余额:" + money);
    }
}

Lock objects should be stable objects that can be accessed by all relevant threads and cannot be replaced at will.

synchronous static method

The synchronous static method locks the Class object corresponding to the current class.

public static synchronized void updateGlobalCount() {
    count++;
}

It uses the same lock as the following:

synchronized (Account.class) {
    count++;
}

The instance synchronization method and the static synchronization method use different monitors and do not block each other by default.

sleep() and lock

Threads calling sleep() in the synchronization area will not release the monitors that are already held, so unnecessary long waits or time-consuming IO should not be performed while the lock is held.

Performance of synchronized

Modern JVM optimizes locks in a variety of ways, and the specific implementation strategy changes with JDK versions. synchronized cannot be regarded as a fixed “heavyweight lock”, nor should we rely on a certain version of internal phases such as biased locks and lightweight locks to write business logic.

CAS is an atomic update idea based on comparison and exchange. Original subclasses such as AtomicInteger will use it or equivalent mechanisms to implement partial lock-free operations. CAS cannot replace all locks, and complex composite states still require an appropriate synchronization scheme.

deadlock

A deadlock is when multiple threads wait for each other for resources held by each other, causing all related threads to fail to continue execution.

public class DeadLockTask implements Runnable {
    private static final Object LOCK_A = new Object();
    private static final Object LOCK_B = new Object();
    private final boolean firstA;

    public DeadLockTask(boolean firstA) {
        this.firstA = firstA;
    }

    @Override
    public void run() {
        if (firstA) {
            synchronized (LOCK_A) {
                synchronized (LOCK_B) {
                    System.out.println("A -> B");
                }
            }
        } else {
            synchronized (LOCK_B) {
                synchronized (LOCK_A) {
                    System.out.println("B -> A");
                }
            }
        }
    }
}

avoid deadlock

  • All threads acquire multiple locks in a uniform order.
  • Reduce lock nesting and holding time.
  • Do not perform external calls, network IO, or uncontrolled blocking operations while the lock is in place.
  • Use Lock.tryLock() to cooperate with a timeout, release the lock that has been held when the acquisition fails and try again.
  • Use thread dump, monitoring tools, and ThreadMXBean to detect deadlocks.

Ordinary synchronized lock waiting cannot be cancelled directly by interrupt, so “interrupting the thread after a deadlock is discovered” may not necessarily relieve the deadlock.

wait(), notify(), and notifyAll()

These three methods are defined in the Object class for thread collaboration based on the same object monitor.

usage rules

  • You must hold the monitor of the object before calling, otherwise IllegalMonitorStateException is thrown.
  • wait() puts the current thread into a waiting state and releases the object monitor.
  • notify() wakes up a thread in the monitor waiting set. The specific thread is uncertain.
  • notifyAll() wakes up all threads waiting on the monitor.
  • The awakened thread must also re-compete for the monitor before it can be returned from wait().

while should be used for conditional judgment because the thread may be awakened by mistake or the condition may change again before the lock is regained.

The difference between wait() and sleep()

Comparison Itemswait()sleep()
CategoryObjectThread
Doesrequire to have a monitor?Yes
Does the monitor releaseYesNo
Wake-up MethodNotification, interrupt, timeoutTimeout or interrupt

producers and consumers

The following demonstrates thread collaboration using capacity-limited resource counting.

public class Resource {
    private final int capacity = 20;
    private int count;

    public synchronized void produce() {
        while (count == capacity) {
            try {
                wait();
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                return;
            }
        }

        count++;
        System.out.println("生产一个,当前数量:" + count);
        notifyAll();
    }

    public synchronized void consume() {
        while (count == 0) {
            try {
                wait();
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                return;
            }
        }

        count--;
        System.out.println("消费一个,当前数量:" + count);
        notifyAll();
    }
}
Resource resource = new Resource();

Runnable producer = () -> {
    for (int i = 0; i < 100; i++) {
        resource.produce();
    }
};

Runnable consumer = () -> {
    for (int i = 0; i < 100; i++) {
        resource.consume();
    }
};

new Thread(producer, "生产者 1").start();
new Thread(producer, "生产者 2").start();
new Thread(consumer, "消费者 1").start();
new Thread(consumer, "消费者 2").start();

In real projects, producer-consumer models often prioritize BlockingQueue, which already encapsulates waiting, notification, and capacity control.

BlockingQueue<String> queue = new ArrayBlockingQueue<>(20);
queue.put("task");
String task = queue.take();

thread pool

Frequent creation and destruction of threads consumes system resources. The thread pool reuses worker threads, limits the number of concurrency, and uniformly manages task queues and denial policies.

A small number of threads with a well-defined life cycle can be created directly; server programs that need to continuously process a large number of short tasks often use thread pools.

ThreadPoolExecutor

core parameters

The main construction parameters of ThreadPoolExecutor are as follows:

  1. corePoolSize: Number of core threads.

  2. maximumPoolSize: The maximum number of threads allowed to be created.

  3. keepAliveTime: Lifetime of non-core idle threads.

  4. unit: Unit of survival time.

  5. workQueue: Save the queue of tasks to be executed after the core thread is busy.

  6. threadFactory: Create a factory for worker threads.

  7. handler: Deny policy when the number of threads reaches the upper limit and the queue is full.

Task submission order

When calling execute() to submit a task, the main process is as follows:

  1. When the current number of worker threads is less than core threads, create a core thread to perform the task.

  2. Try queuing tasks while all core threads are working.

  3. When the queue is full and the number of threads is less than the maximum number of threads, a non-core thread is created to perform the task.

  4. When the queue is full and the number of threads has reached the maximum, a reject policy is executed.

  5. Non-core threads can be reclaimed after being idle for longer than their lifetime. By default, core threads are not reclaimed due to idleness unless core thread timeout is enabled.

refusal strategies

  • AbortPolicy: Throw RejectedExecutionException.
  • CallerRunsPolicy: The calling thread that submitted the task executes the task to provide backpressure.
  • DiscardOldestPolicy: Discard the longest waiting task in the queue and try to submit the current task.
  • DiscardPolicy: Discard the current task directly.

Dismissing tasks may cause loss of business data, so policies must be selected based on the importance of the task and monitoring information must be recorded.

Example of custom thread pool

public class TestTask implements Runnable {
    private final int number;

    public TestTask(int number) {
        this.number = number;
    }

    @Override
    public void run() {
        try {
            Thread.sleep(1000);
            System.out.println(
                    Thread.currentThread().getName()
                            + " 执行任务 " + number
            );
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }
}
ThreadPoolExecutor executor = new ThreadPoolExecutor(
        2,
        5,
        30,
        TimeUnit.SECONDS,
        new ArrayBlockingQueue<>(2),
        Executors.defaultThreadFactory(),
        new ThreadPoolExecutor.CallerRunsPolicy()
);

for (int i = 0; i < 10; i++) {
    executor.execute(new TestTask(i));
}

executor.shutdown();

shutdown() no longer accepts new tasks, but will continue to perform tasks that have been submitted. shutdownNow() will attempt to interrupt executing tasks and return queued tasks that have not yet started, but there is no guarantee that the tasks will stop immediately.

Common thread pools provided by Executors

Executors provides convenient factory methods, but some methods use unbounded queues or approximately unbounded thread numbers. The server program should understand its internal parameters and explicitly configure the thread pool based on the load.

FixedThreadPool

Create a fixed number of worker threads, usually using unbounded queues to hold excess tasks.

ExecutorService executor = Executors.newFixedThreadPool(5);
for (int i = 0; i < 10; i++) {
    executor.execute(new TestTask(i));
}
executor.shutdown();

When task submission speed exceeds processing speed for a long time, unbounded queues may continue to grow and consume a large amount of memory.

CachedThreadPool

Threads are created and reused based on the amount of tasks, and idle threads are usually retained for approximately 60 seconds. The maximum number of threads is very large, which is suitable for tasks with short execution times and controllable submissions, and is not suitable for external requests with no concurrency upper limit.

ExecutorService executor = Executors.newCachedThreadPool();
executor.execute(new TestTask(1));
executor.shutdown();

ScheduledThreadPool

Used for deferred tasks and periodic tasks.

ScheduledExecutorService executor = Executors.newScheduledThreadPool(2);
executor.schedule(
        new TestTask(1),
        5,
        TimeUnit.SECONDS
);
executor.shutdown();

Periodic tasks can use scheduleAtFixedRate() or scheduleWithFixedDelay(), which have different ways of handling task execution time.

SingleThreadExecutor

Always use a single worker thread to execute tasks in queue order. When a worker thread ends abnormally, the thread pool creates a replacement thread to continue executing subsequent tasks.

ExecutorService executor = Executors.newSingleThreadExecutor();
for (int i = 0; i < 5; i++) {
    executor.execute(new TestTask(i));
}
executor.shutdown();

It is suitable for scenarios that require tasks to be executed serially, but unbounded queues can also have a backlog of tasks.

WorkStealingPool

newWorkStealingPool() is based on ForkJoinPool. Worker threads can steal tasks in other queues, making it suitable for computing tasks that can be split and are relatively independent of each other.

ExecutorService executor = Executors.newWorkStealingPool(5);
List<Future<?>> futures = new ArrayList<>();

for (int i = 0; i < 10; i++) {
    futures.add(executor.submit(new TestTask(i)));
}

for (Future<?> future : futures) {
    try {
        future.get();
    } catch (InterruptedException e) {
        Thread.currentThread().interrupt();
        break;
    } catch (ExecutionException e) {
        e.getCause().printStackTrace();
    }
}

executor.shutdown();

The order of task execution is not fixed. Instead of letting the main thread wait for a long time for sleep() to complete the thread pool task, use Future, awaitTermination(), or other explicit synchronization methods.

If you enjoyed this, leave a comment~

... Page views
© 2026 跨越星轨的客 @Hoshiumi
Powered by theme astro-koharu · Inspired by Shoka