MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Java多线程编程中的线程状态转换

2022-09-021.9k 阅读

Java 线程状态概述

在 Java 多线程编程中,线程并不是一成不变地执行任务,它会在不同的阶段处于不同的状态。Java 中的线程状态定义在 java.lang.Thread.State 枚举类中,总共包含六种状态:

  1. NEW:新创建的线程,尚未开始执行。当线程被创建但 start() 方法还未被调用时,线程处于此状态。
  2. RUNNABLE:可运行状态,包含了两种子状态,一种是正在运行的线程,另一种是在等待 CPU 调度,一旦获得 CPU 资源就可以运行的线程。当 start() 方法被调用后,线程就进入此状态。
  3. BLOCKED:阻塞状态,线程正在等待监视器锁(monitor lock),通常发生在当一个线程试图进入一个同步块(synchronized block)或者调用一个同步方法,而该同步块或方法已经被其他线程持有锁的情况下。
  4. WAITING:等待状态,线程无限期地等待另一个线程执行特定操作。例如,线程调用了 Object 类的 wait() 方法,或者 Thread 类的 join() 方法等,就会进入等待状态,直到其他线程通知(notify()notifyAll())或者目标线程执行完毕。
  5. TIMED_WAITING:有时限的等待状态,与 WAITING 类似,但有一个指定的等待时间。例如,调用 Thread.sleep(long millis)Object.wait(long timeout) 等方法时,线程进入此状态,等待时间结束后,线程会自动唤醒。
  6. TERMINATED:终止状态,线程执行完毕或者因为异常退出,此时线程生命结束。

线程状态转换示例代码

下面通过代码示例来演示线程在不同状态之间的转换。

public class ThreadStateExample {
    public static void main(String[] args) {
        // 创建新线程
        Thread thread = new Thread(() -> {
            System.out.println("线程开始执行");
            try {
                // 模拟任务执行
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("线程执行结束");
        });

        // 打印初始状态,应该是 NEW
        System.out.println("线程初始状态: " + thread.getState());

        // 启动线程
        thread.start();

        // 打印启动后的状态,应该是 RUNNABLE
        System.out.println("线程启动后状态: " + thread.getState());

        try {
            // 主线程等待子线程执行一段时间
            Thread.sleep(1000);
            // 打印子线程状态,可能是 RUNNABLE 或者 TIMED_WAITING
            System.out.println("等待 1 秒后线程状态: " + thread.getState());

            // 主线程等待子线程执行完毕
            thread.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        // 打印最终状态,应该是 TERMINATED
        System.out.println("线程最终状态: " + thread.getState());
    }
}

在上述代码中,我们创建了一个新线程,在 main 方法中打印了线程不同阶段的状态。首先创建线程时,线程处于 NEW 状态,调用 start() 方法后,线程进入 RUNNABLE 状态。子线程在执行过程中调用 Thread.sleep(2000),这期间子线程处于 TIMED_WAITING 状态,主线程等待子线程执行完毕调用 thread.join(),当子线程执行结束后,其状态变为 TERMINATED

从 NEW 到 RUNNABLE 的转换

当我们使用 new 关键字创建一个 Thread 对象时,线程处于 NEW 状态。这个时候线程还没有开始执行任何代码,仅仅是一个对象实例。

Thread newThread = new Thread(() -> {
    System.out.println("新线程执行任务");
});

要将线程从 NEW 状态转换到 RUNNABLE 状态,需要调用 start() 方法。

newThread.start();

调用 start() 方法后,Java 虚拟机(JVM)会为线程分配必要的系统资源,如栈空间等,并将线程置于可运行队列中,等待 CPU 调度执行。一旦 CPU 调度到该线程,线程就开始执行 run() 方法中的代码。需要注意的是,不能对已经启动过的线程再次调用 start() 方法,否则会抛出 IllegalThreadStateException 异常。

RUNNABLE 状态的深入理解

RUNNABLE 状态涵盖了正在运行和准备运行两种情况。当线程处于 RUNNABLE 状态且获得了 CPU 时间片时,线程就开始执行 run() 方法中的代码逻辑。而当 CPU 时间片用完或者有更高优先级的线程需要执行时,当前线程会被暂停,重新回到可运行队列中等待下一次 CPU 调度,此时它依然处于 RUNNABLE 状态。

例如,假设有多个线程同时处于 RUNNABLE 状态:

class RunnableThread implements Runnable {
    @Override
    public void run() {
        for (int i = 0; i < 5; i++) {
            System.out.println(Thread.currentThread().getName() + " 正在执行: " + i);
        }
    }
}

public class MultipleRunnableThreads {
    public static void main(String[] args) {
        Thread thread1 = new Thread(new RunnableThread(), "线程 1");
        Thread thread2 = new Thread(new RunnableThread(), "线程 2");
        thread1.start();
        thread2.start();
    }
}

在上述代码中,thread1thread2 启动后都处于 RUNNABLE 状态,它们会竞争 CPU 资源来执行 run() 方法中的代码,输出结果可能会交替出现,这取决于 CPU 的调度策略。

从 RUNNABLE 到 BLOCKED 的转换

当线程试图进入一个同步块或者调用一个同步方法,而该同步块或方法已经被其他线程持有锁时,线程会从 RUNNABLE 状态转换到 BLOCKED 状态。以下是一个示例:

public class BlockedThreadExample {
    private static final Object lock = new Object();

    public static void main(String[] args) {
        Thread thread1 = new Thread(() -> {
            synchronized (lock) {
                System.out.println("线程 1 获得锁并执行");
                try {
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("线程 1 执行完毕释放锁");
            }
        });

        Thread thread2 = new Thread(() -> {
            synchronized (lock) {
                System.out.println("线程 2 获得锁并执行");
            }
        });

        thread1.start();
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        thread2.start();

        try {
            thread1.join();
            thread2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

在这个例子中,thread1 先启动并获得了 lock 对象的锁,然后执行 sleep(3000) 模拟长时间任务。在这期间,thread2 启动并试图获取 lock 对象的锁,但由于 thread1 持有锁,thread2 会进入 BLOCKED 状态,直到 thread1 执行完同步块释放锁,thread2 才能获得锁并进入 RUNNABLE 状态进而执行同步块中的代码。

从 RUNNABLE 到 WAITING 的转换

线程调用 Object 类的 wait() 方法或者 Thread 类的 join() 方法等会导致线程从 RUNNABLE 状态转换到 WAITING 状态。例如,当一个线程调用了共享对象的 wait() 方法:

public class WaitingThreadExample {
    private static final Object sharedObject = new Object();

    public static void main(String[] args) {
        Thread thread = new Thread(() -> {
            synchronized (sharedObject) {
                System.out.println("线程进入同步块");
                try {
                    sharedObject.wait();
                    System.out.println("线程被唤醒");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });

        thread.start();

        try {
            Thread.sleep(2000);
            synchronized (sharedObject) {
                System.out.println("主线程获得锁并唤醒等待线程");
                sharedObject.notify();
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        try {
            thread.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

在上述代码中,thread 进入同步块后调用 sharedObject.wait(),此时 thread 会释放 sharedObject 的锁并进入 WAITING 状态,直到主线程获取 sharedObject 的锁并调用 sharedObject.notify() 方法,thread 才会被唤醒,重新获得锁并继续执行后续代码。

从 RUNNABLE 到 TIMED_WAITING 的转换

调用 Thread.sleep(long millis)Object.wait(long timeout) 等带有时限参数的方法会使线程从 RUNNABLE 状态转换到 TIMED_WAITING 状态。以 Thread.sleep(long millis) 为例:

public class TimedWaitingThreadExample {
    public static void main(String[] args) {
        Thread thread = new Thread(() -> {
            System.out.println("线程开始执行");
            try {
                Thread.sleep(3000);
                System.out.println("线程睡眠结束");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });

        thread.start();

        try {
            Thread.sleep(1000);
            System.out.println("等待 1 秒后线程状态: " + thread.getState());
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        try {
            thread.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

在这个示例中,thread 启动后调用 Thread.sleep(3000),此时线程进入 TIMED_WAITING 状态,等待 3 秒后自动唤醒并继续执行后续代码。在主线程等待 1 秒后查看 thread 的状态,可以看到它处于 TIMED_WAITING 状态。

从 WAITING 和 TIMED_WAITING 回到 RUNNABLE 的转换

对于处于 WAITING 状态的线程,当其他线程调用了对应的 notify() 或者 notifyAll() 方法时,等待线程会被唤醒并进入 RUNNABLE 状态,等待获取对象锁(如果是在同步块中等待)。例如前面 WaitingThreadExample 中的 threadnotify() 唤醒后会进入 RUNNABLE 状态。

对于处于 TIMED_WAITING 状态的线程,当等待时间结束或者被其他线程提前唤醒(如果调用的是 Object.wait(long timeout) 且被 notify() 唤醒),线程会进入 RUNNABLE 状态。比如 TimedWaitingThreadExamplethread 在睡眠 3 秒结束后进入 RUNNABLE 状态继续执行。

从 RUNNABLE 到 TERMINATED 的转换

当线程的 run() 方法正常执行完毕或者因为未捕获的异常而退出时,线程会从 RUNNABLE 状态转换到 TERMINATED 状态。例如:

public class TerminatedThreadExample {
    public static void main(String[] args) {
        Thread thread = new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                System.out.println("线程执行: " + i);
            }
            System.out.println("线程正常结束");
        });

        thread.start();

        try {
            thread.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println("线程最终状态: " + thread.getState());
    }
}

在上述代码中,threadrun() 方法正常执行完毕,之后线程进入 TERMINATED 状态,通过 thread.getState() 可以获取到这个状态。如果 run() 方法中抛出未捕获的异常,同样会导致线程进入 TERMINATED 状态,例如:

public class TerminatedByExceptionExample {
    public static void main(String[] args) {
        Thread thread = new Thread(() -> {
            int result = 10 / 0; // 抛出异常
            System.out.println("这行代码不会执行");
        });

        thread.start();

        try {
            thread.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println("线程最终状态: " + thread.getState());
    }
}

在这个例子中,thread 在执行 int result = 10 / 0; 时抛出 ArithmeticException 异常,导致 run() 方法提前结束,线程进入 TERMINATED 状态。

状态转换中的异常处理

在多线程编程中,状态转换过程可能会涉及到异常处理。例如,当调用 Thread.sleep(long millis) 方法时,线程可能会被中断,此时会抛出 InterruptedException 异常。正确处理这些异常对于保证线程的健壮性非常重要。

public class InterruptedExceptionExample {
    public static void main(String[] args) {
        Thread thread = new Thread(() -> {
            try {
                System.out.println("线程开始睡眠");
                Thread.sleep(5000);
                System.out.println("线程睡眠结束");
            } catch (InterruptedException e) {
                System.out.println("线程被中断");
                Thread.currentThread().interrupt(); // 重新设置中断标志
            }
        });

        thread.start();

        try {
            Thread.sleep(2000);
            thread.interrupt();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        try {
            thread.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

在上述代码中,thread 睡眠 5 秒,但主线程在 2 秒后调用 thread.interrupt() 中断 threadthread 捕获到 InterruptedException 异常后,打印“线程被中断”,并重新设置中断标志。这样做是因为 InterruptedException 会清除中断标志,如果后续代码还需要根据中断标志进行处理,就需要重新设置。

线程状态转换与并发控制

理解线程状态转换对于并发控制至关重要。通过合理地控制线程的状态转换,可以实现高效的并发编程。例如,在生产者 - 消费者模型中,生产者线程和消费者线程通过共享队列进行数据传递。当共享队列已满时,生产者线程需要进入等待状态(WAITINGTIMED_WAITING),直到消费者线程从队列中取出数据后通知生产者线程,生产者线程才会被唤醒并继续生产数据。同样,当共享队列已空时,消费者线程也需要进入等待状态,直到生产者线程生产出新的数据。

import java.util.LinkedList;
import java.util.Queue;

class Producer implements Runnable {
    private final Queue<Integer> queue;
    private final int capacity;

    public Producer(Queue<Integer> queue, int capacity) {
        this.queue = queue;
        this.capacity = capacity;
    }

    @Override
    public void run() {
        int value = 0;
        while (true) {
            synchronized (queue) {
                while (queue.size() == capacity) {
                    try {
                        System.out.println("队列已满,生产者等待");
                        queue.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                queue.add(value++);
                System.out.println("生产者生产: " + (value - 1));
                queue.notify();
            }
        }
    }
}

class Consumer implements Runnable {
    private final Queue<Integer> queue;

    public Consumer(Queue<Integer> queue) {
        this.queue = queue;
    }

    @Override
    public void run() {
        while (true) {
            synchronized (queue) {
                while (queue.isEmpty()) {
                    try {
                        System.out.println("队列已空,消费者等待");
                        queue.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                int consumed = queue.poll();
                System.out.println("消费者消费: " + consumed);
                queue.notify();
            }
        }
    }
}

public class ProducerConsumerExample {
    public static void main(String[] args) {
        Queue<Integer> queue = new LinkedList<>();
        int capacity = 5;
        Thread producerThread = new Thread(new Producer(queue, capacity));
        Thread consumerThread = new Thread(new Consumer(queue));

        producerThread.start();
        consumerThread.start();
    }
}

在上述生产者 - 消费者模型代码中,通过 synchronized 关键字和 wait()notify() 方法来控制线程状态转换,从而实现了生产者和消费者之间的协同工作,保证了共享队列的正确使用和并发操作的正确性。

总结线程状态转换的注意事项

  1. 避免死锁:在涉及线程状态转换到 BLOCKED 状态时,要注意避免死锁情况。例如,多个线程相互等待对方释放锁,导致程序无法继续执行。可以通过合理设计锁的获取顺序、使用 tryLock() 方法等方式来预防死锁。
  2. 正确处理中断:在涉及 WAITINGTIMED_WAITING 状态的转换中,要正确处理 InterruptedException 异常。如前文所述,捕获异常后根据需要重新设置中断标志,以确保线程能够正确响应中断。
  3. 同步控制:在多线程环境下,状态转换往往与同步控制紧密相关。要确保在合适的位置使用 synchronized 关键字或者其他同步机制,保证线程安全。同时,也要注意同步块的粒度,避免过度同步导致性能下降。

通过深入理解 Java 多线程编程中的线程状态转换,开发者可以更好地编写高效、健壮的多线程程序,充分利用多核处理器的优势,提升程序的性能和响应能力。在实际开发中,根据具体的业务需求,合理地控制线程状态转换,是实现复杂并发场景的关键。