Chapter 01 – Multithreading Basics: Unraveling the World of Threads

Mark MaMark Ma
14 min read

In everyday development, have you ever encountered this scenario: your application needs to perform multiple tasks, and you’d like them to run simultaneously to improve performance and responsiveness? This is exactly where the core concept of multithreaded programming comes into play.

When writing Java applications, it’s common to face situations where multiple tasks need to be handled at the same time. These tasks may involve downloading data from the internet, performing CPU-intensive computations, responding to user interactions, or executing other operations concurrently. In such cases, multithreading can be a powerful tool—it enables us to make better use of computing resources while keeping applications running smoothly.

In this article, we’ll explore the basics of Java multithreading programming, starting with thread creation, usage, lifecycle, and the causes of thread-safety concerns, helping you better understand and apply threads in your applications.


1. Thread Creation and Startup

Threads are lightweight compared to processes, as they consume fewer resources by sharing the same memory space within a process. In Java’s threading model, multiple threads are allowed to execute different tasks within the same program. The existence of threads significantly improves a program’s performance and responsiveness.

In Java, threads can be created and managed using the java.lang.Thread class. The most common approach looks like this:

public class ThreadRunnableTest {
    public static void main(String[] args) {
        Thread thread = new Thread(new Task());
        thread.setName("Test");
        thread.start();
    }
    static class Task implements Runnable {
        @Override
        public void run() {
            System.out.println("Thread name: " + Thread.currentThread().getName());
        }
    }
}

The above example shows how to create a regular thread. Once the start() method is called, the main thread will launch a child thread to execute the task, while continuing its own execution sequentially. At this point, both the main thread and the child thread run concurrently. But can a child thread return a value?

Java also provides a threading mechanism that supports return values. A basic usage example looks like this:

public class ThreadCallableTest {
    public static void main(String[] args) {
        // Construct a task object with a return value by wrapping the actual task logic.
        FutureTask<String> stringFutureTask = new FutureTask<>(new TaskReturn());
        Thread thread = new Thread(stringFutureTask);
        thread.setName("Test");
        thread.start();
        try {
            System.out.println(stringFutureTask.get());
        } catch (InterruptedException | ExecutionException e) {
            e.printStackTrace();
        }
    }
    private static class TaskReturn implements Callable<String> {
        @Override
        public String call() throws Exception {
            return String.format("I was executed by thread [%s].", Thread.currentThread().getName());
        }
    }
}

In the above example, we created a thread task that returns a result. As you can see, when defining the task, we specify a generic type — this represents the type of the final result produced by the task. Unlike a standard Runnable, a Callable cannot be passed directly to a Thread. Therefore, we use a FutureTask to wrap the Callable object. The get() method of FutureTask allows us to retrieve the result of the asynchronous Callable execution.

With that, we’ve basically covered how to define a basic thread. As mentioned earlier, we referred to the main thread and child threads. To summarize briefly: when you click “Run” in your IDE, a thread is created to execute the main method. We call the thread running the main method the main thread, and any thread created from within main is referred to as a child thread.

2. Key Thread Parameters and APIs

Now that we’ve explored the basics of working with threads, let’s take a closer look at some of the important parameters and methods associated with thread management. In this section, we’ll focus on four key areas:

  • Thread Priority

  • Thread Name

  • Daemon Threads

  • Stopping Threads

2.1 Thread Priority

In Java, thread priority is used to indicate how a thread should be scheduled relative to other threads.

Thread priority is an integer value between 1 and 10, where 1 represents the lowest priority and 10 represents the highest priority. While thread priority can influence the decisions of the thread scheduler, it does not guarantee the execution order, since scheduling is ultimately controlled by the underlying operating system and the Java Virtual Machine (JVM).

What does thread priority affect?

  • Execution Order: Threads with higher priority may have a better chance of obtaining CPU resources before lower-priority threads. However, this behavior is not deterministic.

  • Resource Allocation: On multi-core processors, higher-priority threads might be scheduled more frequently, thereby getting more CPU time.

  • Application Requirements: You can assign priority to threads based on the needs of your application—for example, to ensure certain time-sensitive tasks are handled first.

To set a thread’s priority, use the setPriority() method from the Thread class. For example:

Thread thread = new Thread(() -> {
    // Task logic here
});
thread.setPriority(Thread.MAX_PRIORITY); // Sets priority to 10

Note: Thread priority may not always behave as expected, as it heavily relies on support from the underlying operating system. Moreover, in many real-world multithreaded scenarios, over-relying on thread priority can lead to unpredictable results. Therefore, thread priority should be used with caution.

When designing multithreaded applications, it is often better to rely on other synchronization mechanisms—such as locks, condition variables, or thread pools—to coordinate thread behavior more reliably. For now, just be aware of this concept.

Common Issues with Thread Priority

Here are some potential problems that may arise when using thread priority:

  • Priority Inversion:

    This occurs when a low-priority thread holds a lock or resource that a higher-priority thread needs. If the low-priority thread doesn’t release the resource quickly, the high-priority thread will be blocked, waiting for it. In essence, a lower-priority thread may delay the execution of a higher-priority thread by monopolizing critical resources.

  • Starvation:

    Because high-priority threads have a greater chance of acquiring CPU time, lower-priority threads may never get scheduled, especially in resource-constrained environments. This can lead to starvation—where a thread is indefinitely prevented from executing.

  • Platform Differences:

    Thread priority behavior is not standardized across different operating systems or JVM implementations. As a result, the same code may exhibit different behavior on different platforms.

  • Priority Saturation:

    In environments with a large number of threads, even high-priority threads may face intense competition for CPU resources. As the system becomes saturated, thread scheduling becomes more complex and unpredictable.

2.2 Thread Naming

Multithreaded programming is not only challenging to grasp during development, but also becomes especially difficult to debug when things go wrong. Developers with multithreading experience understand that concurrency bugs are often non-deterministic—they don’t always happen and typically surface only under specific conditions or when concurrency reaches a certain scale.

In traditional systems—regardless of the framework you’re using—it’s common to create a large number of threads. If these threads lack meaningful identifiers, such as task-specific names, it becomes significantly harder to troubleshoot issues. This is where thread naming becomes critically important.

Whether you’re diagnosing performance bottlenecks, tracking down deadlocks, or analyzing thread dumps, having descriptive thread names can provide valuable insight into what each thread is doing.

So, how do you set and retrieve a thread’s name in Java?

thread.setName("测试线程");
Thread.currentThread().getName()

The thread name often “works wonders” when debugging issues like system slowdowns or deadlocks caused by certain conditions. With the help of JVM tools, we can easily monitor the presence and behavior of threads, and a meaningful thread name greatly improves the clarity and effectiveness of such monitoring.

2.3 Daemon Threads

In Java, a daemon thread is a special kind of thread designed to provide services or support to other threads. Unlike regular user threads, the lifecycle of a daemon thread ends automatically when all user threads have finished executing. This means that if only daemon threads remain, the Java Virtual Machine (JVM) will shut down.

Daemon threads are typically used for background tasks such as garbage collection, monitoring, logging, or scheduling. These threads run quietly in the background and do not interfere with the normal flow of the application. Once all user threads have completed their tasks and exited, the JVM will terminate automatically—regardless of whether the daemon threads have completed their work.

Let’s consider the following code snippet as an example:

public class ThreadRunnableTest {
    public static void main(String[] args) {
        Thread thread = new Thread(new Task());
        thread.setName("Test");
        thread.start();
    }
    private static class Task implements Runnable {
        @Override
        public void run() {
            try {
                TimeUnit.SECONDS.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("Thread name: " + Thread.currentThread().getName());
        }
    }
}

As you can see, the code is almost identical to the previous example, except for the addition of a 10-second sleep. In this case, when the program runs, the JVM will wait for the child thread to finish sleeping before exiting, allowing the main thread to complete normally. A thread like this is known as a worker thread (or user thread).

To convert a worker thread into a daemon thread, we use the following line in the code:

thread.setDaemon(true);

This tells the JVM that the thread should run in the background and should not prevent the JVM from exiting once all user threads have finished.

After running the code, once the main thread finishes, the program will terminate immediately — there will be no output, no exception — it simply stops.

This is the key difference between daemon threads and worker (user) threads:

  • Daemon Thread: As soon as the main thread finishes, all daemon threads are forcibly terminated — regardless of whether they’ve completed their tasks.

  • Worker Thread: The JVM will wait for all worker threads to finish before it shuts down the program.

In real-world development, we must be very cautious when using daemon threads. Because a daemon thread’s lifecycle is tightly coupled with the main thread, if the main thread ends too early, daemon threads might not have enough time to complete critical tasks — such as closing JDBC connections or releasing file resources — which could lead to subtle and confusing bugs.

So it’s important to carefully choose what tasks to assign to daemon threads, and ensure those tasks are safe to terminate abruptly when the main thread exits.

2.4 Stopping a Thread in Java

Stopping a thread is one of the most notoriously tricky problems in multithreaded programming. Even today, there is no “perfect” way to stop a thread in Java — only safe and unsafe practices.

The Wrong Way: thread.stop()

The method Thread.stop() can forcibly terminate a thread immediately, without throwing any exceptions or giving the thread a chance to clean up its resources.

Although this may sound convenient, it is strongly discouraged for the following reasons:

  • It may leave shared resources (like locks, files, DB connections) in an inconsistent state.

  • It breaks the atomicity of critical sections.

  • It has been deprecated by the JDK for a long time.

In short: don’t use stop().

The Safer Way: thread.interrupt()

Java provides a cooperative approach to thread termination using Thread.interrupt():

  • It doesn’t forcibly kill the thread.

  • It sets an internal “interrupted” flag on the thread.

  • If the thread is in a blocking state (like sleep(), wait(), or join()), it will throw an InterruptedException.

  • If the thread is not blocking, it won’t stop unless the code checks the interrupt status manually.

public class StopThreadTest {
    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(new Task());
        thread.setName("Test");
        thread.start();
        TimeUnit.SECONDS.sleep(1);

        thread.interrupt();
    }
    private static class Task implements Runnable {
        @Override
        public void run() {
            while (!Thread.currentThread().isInterrupted()) {
                System.out.println("Running");
            }
        }
    }
}

There are various perspectives and methods for stopping threads. Based on my practical experience in production, the most common and reliable way is to use the interrupt mechanism to send a stop signal, allowing the thread to terminate gracefully.

In an interview, I also encountered a method that involves placing checkpoints within the thread’s logic to periodically check whether an interrupt signal has been received, thereby controlling the thread’s termination. However, this approach is often just a temporary workaround and doesn’t fundamentally solve the problem.

In real-world development, it’s important to choose a thread-stopping strategy that fits your specific conditions and requirements. Each method has its own applicable scenarios, so the key is to ensure that threads can be stopped in a controlled and maintainable manner.

3. Thread Lifecycle and States

The thread lifecycle and states refer to the various stages and conditions a thread goes through from creation to termination. In Java, the lifecycle of a thread primarily includes the following states:

  1. New: When a thread object is created, the thread is in the New state. At this point, the thread object exists but has not yet started.

  2. Runnable: In the Runnable state, the thread is ready to run but has not yet acquired CPU time to execute. The thread may be waiting in the ready queue for CPU resources.

  3. Running: When the thread obtains CPU time and begins execution, it is in the Running state. The thread executes its task code during this time.

  4. Blocked: A thread enters the Blocked state when it is prevented from executing, usually because it is waiting for a certain condition to be met, such as waiting for I/O operations to complete or for a lock to be released.

  5. Waiting: In the Waiting state, the thread is waiting indefinitely until another thread notifies it to continue. Threads enter this state by calling methods like wait(), join(), or similar.

  6. Timed Waiting: Similar to Waiting, the thread enters the Timed Waiting state to wait for a specific amount of time until the time expires or another thread notifies it. This state can be triggered by methods such as sleep() or wait() with a timeout.

  7. Terminated: A thread in the Terminated state has completed its lifecycle and can no longer execute. This happens either when the thread finishes its task normally or terminates due to an exception.

Threads can transition between these states; for example, a thread in the New state moves to Runnable, then to Running. A Running thread may enter Blocked, Waiting, or Timed Waiting states before ultimately terminating.

Understanding the thread lifecycle and states is essential for effective multithreaded programming, as it helps manage thread behavior, synchronization, and scheduling. Java’s Thread class and related tools allow monitoring and managing thread states to ensure threads run as expected. You can refer to java.lang.Thread.State for the official definitions of thread states.

4. Race Conditions and Critical Sections

In concurrent programming, one of the most common issues we hear about is thread safety.

So, what exactly is a concurrency safety problem? How do such problems arise?

Let me tell you a story:

In a small town bakery, there was a baker named Ming. One day, he invented a new pastry called the “Race Cake.” This cake was special — customers had to taste it immediately to appreciate its deliciousness. Because the cake was so popular, the bakery became very busy with many customers every day.

To speed up production, Ming hired two assistants — Hong and Lu — to help make the Race Cakes. Each assistant was responsible for half of the production process.

The Race Cake making process involved these steps:

  1. Prepare the cake batter mixture.

  2. Bake the cake.

  3. Add cream and decorations.

The problem happened in the third step: adding cream and decorations. Hong and Lu often finished the first two steps at roughly the same time and then rushed to do the third step simultaneously. This led to trouble.

One day, both assistants were preparing a cake for a customer. But when they reached the final step, both tried to add cream and decorations at the same time. Because of this conflict, the final cake ended up a mess.

This story highlights an important point: race conditions typically occur when multiple parties try to operate on something that requires a specific order, simultaneously. The key factors to notice are: multiple actors, simultaneous access, and order sensitivity.

In this example, Hong and Lu are like two threads, and the cake represents the critical section. When multiple threads access a critical section concurrently without proper coordination, race conditions occur, potentially corrupting the data.

So, how do we solve this problem? The story continues:

Ming realized the issue and decided to fix the race condition. He introduced a simple rule: only one assistant can work on the last step at a time, while the other must wait. This simple rule ensured that race conditions no longer occurred, and the Race Cake remained as delicious as ever.

This rule guarantees that the critical section is accessed correctly and sequentially under race conditions. In concurrent programming, this rule is what we commonly call a lock!

5. Summary

Threads are a critical element for maximizing server resource utilization, making a deep understanding and mastery of threading an essential skill in modern Java development.

In this chapter, we focused on the basic usage of threads, common parameters, and frequently used methods. Through case studies, we also discussed concurrency safety issues that can arise from multithreading and the root causes of these problems. This in-depth exploration lays a solid foundation for further learning.

While threads can significantly improve server resource efficiency, improper use or poor control can lead to serious system issues.

In the next section, we will delve into how to use threads rationally, efficiently, and safely to ensure system stability and performance. This will provide you with a more comprehensive perspective, enabling you to leverage threads more precisely, fully exploit their advantages, and avoid potential pitfalls.


👉 Enjoyed this chapter?

The full guide “Java Concurrency for Interviews: Mastering JUC from the Ground Up” includes practical chapters on synchronized, thread pools, AQS, CAS, and real-world concurrency patterns.

Support me on Ko-fi and get the full PDF version with all 12 chapters!

0
Subscribe to my newsletter

Read articles from Mark Ma directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Mark Ma
Mark Ma