Java 多线程揭秘:彻底掌握线程状态转换与控制方法

stateDiagram-v2 [*] --> NEW: 创建线程对象 NEW --> RUNNABLE: 调用start() RUNNABLE --> BLOCKED: 等待synchronized锁 BLOCKED --> RUNNABLE: 获得锁 RUNNABLE --> WAITING: 调用wait()/join()/park() WAITING --> RUNNABLE: 调用notify()/notifyAll()/unpark()或目标线程结束 RUNNABLE --> TIMED_WAITING: 调用sleep(time)/wait(time)/join(time) TIMED_WAITING --> RUNNABLE: 时间到期或被中断 RUNNABLE --> TERMINATED: 运行结束 TERMINATED --> [*]

引言

各位开发者好!在上一篇文章中,我们详细介绍了 Java 多线程的四种创建方式。今天,我们将深入探讨线程的生命周期和基础操作方法,这些知识对于理解多线程程序的行为和调试线程问题至关重要。

很多初学者在多线程编程中遇到的困惑,往往源于对线程状态转换和控制方法的理解不足。比如:为什么我的线程没有执行?为什么线程无法停止?如何优雅地结束一个线程?今天,我们就来一一解答这些问题。

一、线程的六大状态详解

Java 中的 Thread 类定义了线程的六种状态,这些状态定义在 Thread 类的内部枚举 State 中:

java 复制代码
public enum State {
    NEW,
    RUNNABLE,
    BLOCKED,
    WAITING,
    TIMED_WAITING,
    TERMINATED;
}

下面我们详细解析每一种状态及其转换过程:

1. NEW(新建)

当你创建一个 Thread 对象但还没有调用 start()方法时,线程处于 NEW 状态。

java 复制代码
Thread thread = new Thread(() -> {
    System.out.println("线程任务执行");
});
thread.setName("worker-thread"); // 给线程起一个有意义的名称
System.out.println("创建后线程状态:" + thread.getState()); // 输出: NEW

2. RUNNABLE(可运行)

调用 start()方法后,线程进入 RUNNABLE 状态。在 Java 中,RUNNABLE 状态包含了操作系统层面的"就绪"和"运行中"两个状态:

  • 就绪:线程已经准备好运行,但等待 CPU 分配时间片
  • 运行中:线程正在 CPU 上执行

重要说明:Java 中的 RUNNABLE 状态是一个复合状态,不区分线程是"就绪"还是"正在运行",这与操作系统的线程状态模型不同。即使线程获得了 CPU 时间片正在执行,在 Java API 看来它仍然是 RUNNABLE 状态,无法通过 Thread.getState()区分线程是否正在 CPU 上执行。

java 复制代码
thread.start();
System.out.println("启动后线程状态:" + thread.getState()); // 输出: RUNNABLE
graph TD A[操作系统线程状态] --> B[就绪Ready] A --> C[运行中Running] A --> D[阻塞Blocked] B --> J[Java RUNNABLE] C --> J D --> K[Java BLOCKED/WAITING/TIMED_WAITING] style J fill:#9cf,stroke:#333,stroke-width:2px style K fill:#f9a,stroke:#333,stroke-width:2px

3. BLOCKED(阻塞)

线程被阻塞,等待获取一个 synchronized 内置锁(也称 monitor 锁)。当线程尝试进入一个 synchronized 块/方法,但该锁被其他线程持有时,就会进入这个状态。

java 复制代码
Object lock = new Object();

Thread thread1 = new Thread(() -> {
    synchronized (lock) {
        try {
            Thread.sleep(3000); // 持有锁3秒
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
});
thread1.setName("lock-holder");

Thread thread2 = new Thread(() -> {
    synchronized (lock) {
        System.out.println("线程2获取到了锁");
    }
});
thread2.setName("lock-waiter");

thread1.start(); // 先启动线程1
Thread.sleep(100); // 确保线程1先获取到锁
thread2.start(); // 再启动线程2
Thread.sleep(100); // 给线程2一点时间尝试获取锁

System.out.println("线程2状态:" + thread2.getState()); // 输出: BLOCKED

4. WAITING(等待)

线程进入无限期等待状态,需要其他线程执行特定操作后才能继续。 主要由以下方法导致:

  • Object.wait()
  • Thread.join()
  • LockSupport.park()

特别说明:Object.wait()方法会释放持有的 monitor 锁(也就是 synchronized 锁),而 LockSupport.park()不会释放任何锁,这是一个重要区别。

性能考虑:在高并发环境中,过多线程进入 WAITING 状态可能导致系统资源浪费。建议使用带超时参数的 wait(timeout),避免由于通知丢失导致线程永久等待。

java 复制代码
Object lock = new Object();

Thread waitingThread = new Thread(() -> {
    synchronized (lock) {
        try {
            lock.wait(); // 进入WAITING状态,同时释放锁
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
});
waitingThread.setName("waiting-thread");

waitingThread.start();
Thread.sleep(100); // 确保waitingThread进入等待状态
System.out.println("等待中的线程状态:" + waitingThread.getState()); // 输出: WAITING

5. TIMED_WAITING(计时等待)

与 WAITING 类似,但有超时时间。以下方法会导致这个状态:

  • Thread.sleep(long)
  • Object.wait(long)
  • Thread.join(long)
  • LockSupport.parkNanos()
  • LockSupport.parkUntil()
java 复制代码
Thread sleepingThread = new Thread(() -> {
    try {
        Thread.sleep(5000); // 休眠5秒,进入TIMED_WAITING状态
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
});
sleepingThread.setName("sleeping-thread");

sleepingThread.start();
Thread.sleep(100); // 确保sleepingThread进入休眠状态
System.out.println("休眠中的线程状态:" + sleepingThread.getState()); // 输出: TIMED_WAITING

6. TERMINATED(终止)

线程执行完毕或因异常结束。

java 复制代码
Thread terminatedThread = new Thread(() -> {
    // 执行一些简短的任务
    System.out.println("任务执行完毕");
});
terminatedThread.setName("terminated-thread");

terminatedThread.start();
Thread.sleep(100); // 确保线程有足够时间完成任务
System.out.println("任务完成后线程状态:" + terminatedThread.getState()); // 输出: TERMINATED

三种等待状态的对比

状态 触发条件 是否释放锁 恢复条件 典型使用场景
BLOCKED 等待进入 synchronized 同步块/方法 不持有锁 获得锁 多线程竞争共享资源
WAITING Object.wait() Thread.join() LockSupport.park() wait()释放锁 join/park 不涉及锁释放 notify/notifyAll 目标线程结束 unpark 线程协作,等待条件满足
TIMED_WAITING Thread.sleep(time) Object.wait(time) Thread.join(time) sleep 不释放锁 wait 释放锁 join 不涉及锁释放 时间到期或上述对应条件 超时等待,避免无限阻塞
graph TD A[运行中的线程] -->|尝试获取被占用的synchronized锁| B[BLOCKED] A -->|"调用wait()"| C[WAITING] A -->|"调用sleep(time)"| D[TIMED_WAITING] B -->|获得锁| A C -->|"其他线程调用notify()"| A D -->|时间到期| A style A fill:#9cf,stroke:#333 style B fill:#f9a,stroke:#333 style C fill:#ffc,stroke:#333 style D fill:#cfc,stroke:#333

二、start()与 run()的本质区别

初学者常犯的一个错误是直接调用线程的 run()方法,而不是 start()方法。这两者有本质区别:

调用 run()

java 复制代码
Thread thread = new Thread(() -> {
    System.out.println("当前线程: " + Thread.currentThread().getName());
});
thread.run(); // 直接调用run方法

输出:当前线程: main

调用 start()

java 复制代码
Thread thread = new Thread(() -> {
    System.out.println("当前线程: " + Thread.currentThread().getName());
});
thread.start(); // 调用start方法

输出:当前线程: Thread-0

区别分析

  • run(): 普通方法调用,在当前线程(通常是 main 线程)执行线程体,没有创建新线程
  • start(): 启动新线程,在新线程中执行 run()方法,实现了多线程并发执行

底层原理:start()方法会调用 native 方法 start0(),该方法会在 JVM 层面创建一个新的操作系统线程,并设置线程状态,最终导致 run()方法在新线程中执行。

graph TD A[Thread对象创建] --> B{调用方法?} B -->|"thread.run()"| C[在当前线程中执行run方法] B -->|"thread.start()"| D[创建新的操作系统线程] D --> E[在新线程中执行run方法] C --> F[无并发效果] E --> G[实现多线程并发] style C fill:#f9a,stroke:#333 style E fill:#9cf,stroke:#333 style F fill:#f9a,stroke:#333 style G fill:#9cf,stroke:#333

常见错误案例

java 复制代码
// 错误用法:重复调用start()
Thread thread = new Thread(() -> System.out.println("任务执行"));
thread.start();
thread.start(); // 抛出IllegalThreadStateException

// 错误理解:以为run()会启动线程
Thread thread2 = new Thread(() -> {
    for(int i=0; i<1000; i++) {
        System.out.println(i);
    }
});
thread2.run(); // 在主线程中顺序执行,没有并发效果

三、线程控制方法详解

Java 提供了几个重要的线程控制方法,下面我们来详细解析:

1. sleep() - 线程休眠

使当前线程暂停执行指定的时间,进入 TIMED_WAITING 状态,但不会释放锁。

java 复制代码
public static void sleepDemo() {
    Object lock = new Object();

    Thread thread = new Thread(() -> {
        synchronized (lock) {
            System.out.println("线程获取到锁");
            try {
                System.out.println("线程开始休眠5秒");
                Thread.sleep(5000);
                System.out.println("线程休眠结束");
            } catch (InterruptedException e) {
                System.out.println("线程被中断");
            }
        }
    });
    thread.setName("sleeping-thread");

    thread.start();

    // 给点时间让线程启动并获取锁
    try {
        Thread.sleep(1000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }

    // 尝试获取锁,会被阻塞直到上面的线程释放锁
    synchronized (lock) {
        System.out.println("主线程获取到锁");
    }
}

要点

  • sleep 是 Thread 类的静态方法,会暂停当前正在执行的线程
  • 不会释放锁资源
  • 可以被 interrupt()方法中断,抛出 InterruptedException
  • sleep 结束后线程会自动回到 RUNNABLE 状态
  • 长时间的 sleep()会占用线程资源而不执行工作,在线程池环境中可能导致性能下降
  • 建议配合合理的超时机制使用,避免无限期阻塞

2. yield() - 线程让步

提示调度器当前线程愿意放弃 CPU 使用权,但调度器可以忽略这个提示。

java 复制代码
public static void yieldDemo() {
    Thread thread1 = new Thread(() -> {
        for (int i = 0; i < 100; i++) {
            System.out.println("线程1: " + i);
            if (i % 10 == 0) {
                System.out.println("线程1让步");
                Thread.yield();
            }
        }
    });
    thread1.setName("yield-thread");

    Thread thread2 = new Thread(() -> {
        for (int i = 0; i < 100; i++) {
            System.out.println("线程2: " + i);
        }
    });
    thread2.setName("normal-thread");

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

要点

  • yield 是 Thread 类的静态方法
  • 只是提示调度器,没有强制性
  • 从运行状态到就绪状态的转变(仍然是 RUNNABLE)
  • 实际效果取决于操作系统的实现,不可靠

yield()在现代系统中的实际效果

尽管 yield()方法的理论目的是让出 CPU 时间片,但在现代操作系统和 JVM 实现中,它的实际效果往往不可预测:

  1. 大多数现代 CPU 调度器已经非常智能,能够有效分配时间片
  2. 不同 JVM 实现和操作系统对 yield()的处理方式不同
  3. 在某些系统上,yield()可能完全没有效果
  4. 在其他系统上,yield()可能导致当前线程被过度惩罚,长时间无法获得 CPU

在实际开发中

  • 避免使用 yield()来解决线程协作问题
  • 如需控制线程执行顺序,应使用显式同步机制(如 CountDownLatch、CyclicBarrier 等)
  • 如需控制执行时间分配,考虑使用线程优先级或更高级的调度框架

3. join() - 线程等待

让当前线程等待另一个线程执行完毕后再继续执行。

java 复制代码
public static void joinDemo() {
    Thread worker = new Thread(() -> {
        System.out.println("工作线程开始执行...");
        try {
            // 模拟耗时操作
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("工作线程执行完毕");
    });
    worker.setName("worker-thread");

    System.out.println("主线程启动工作线程");
    worker.start();

    System.out.println("主线程等待工作线程完成");
    try {
        worker.join(); // 主线程在这里等待worker线程执行完毕
    } catch (InterruptedException e) {
        e.printStackTrace();
    }

    System.out.println("主线程继续执行");
}

join()方法的重载版本

  • join(): 等待线程终止
  • join(long millis): 等待指定的毫秒数
  • join(long millis, int nanos): 等待指定的毫秒数加纳秒数

join()方法的内部实现原理

java 复制代码
// join()方法的内部实现原理(简化版)
public final synchronized void join(long millis) throws InterruptedException {
    if (millis > 0) {
        if (isAlive()) {
            // 当前线程在目标线程对象上等待
            wait(millis);
        }
    } else if (millis == 0) {
        while (isAlive()) {
            // 无限期等待
            wait(0);
        }
    }
    // 注意:当目标线程终止时,JVM会调用notifyAll()唤醒所有等待线程
}

深入理解:当线程 A 调用线程 B 的 join()方法时,线程 A 会在线程 B 对象的监视器上等待。当线程 B 执行完毕(无论正常结束还是异常结束),JVM 会调用线程 B 对象的 notifyAll()方法,从而唤醒在其上等待的线程 A。这种设计确保了线程 A 能够在线程 B 结束后继续执行。

性能考虑

  • 无限期的 join()可能导致调用线程长时间等待,降低系统吞吐量
  • 在线程池环境中,线程长时间阻塞会降低线程池效率
  • 推荐使用 join(timeout)设置合理的等待超时时间
sequenceDiagram participant 主线程 participant 工作线程对象 participant 工作线程 主线程->>工作线程对象: 创建 主线程->>工作线程: start() 工作线程->>工作线程: 执行run()方法 主线程->>工作线程对象: join() 工作线程对象->>主线程: wait() Note over 主线程: 主线程进入WAITING状态 工作线程->>工作线程: 完成任务 Note over 工作线程: 进入TERMINATED状态 工作线程->>工作线程对象: JVM调用notifyAll() 工作线程对象->>主线程: 唤醒 Note over 主线程: 继续执行

4. interrupt() - 线程中断

这是一种协作式的线程中断机制,用于通知线程应该停止或中断当前工作。

java 复制代码
public static void interruptDemo() {
    Thread sleepingThread = new Thread(() -> {
        try {
            System.out.println("线程开始休眠10秒");
            Thread.sleep(10000);
            System.out.println("休眠完成"); // 如果被中断,这行不会执行
        } catch (InterruptedException e) {
            System.out.println("线程被中断: " + e.getMessage());
        } finally {
            System.out.println("线程结束");
        }
    });
    sleepingThread.setName("interrupted-thread");

    sleepingThread.start();

    // 主线程休眠2秒后中断sleepingThread
    try {
        Thread.sleep(2000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }

    System.out.println("主线程发出中断请求");
    sleepingThread.interrupt();
}

中断机制相关方法

  • interrupt(): 请求中断线程
  • isInterrupted(): 检查线程是否被中断(不清除中断状态)
  • Thread.interrupted(): 静态方法,检查当前线程是否被中断(清除中断状态)

非阻塞场景的中断处理

在非阻塞状态下,interrupt()方法只会设置线程的中断标志,不会导致线程抛出 InterruptedException。此时,线程需要主动检查中断状态并作出响应:

java 复制代码
public static void nonBlockingInterruptDemo() {
    Thread worker = new Thread(() -> {
        // 给线程起一个有意义的名称
        Thread.currentThread().setName("worker-thread");

        // 执行计算密集型任务,定期检查中断状态
        long sum = 0;
        System.out.println(Thread.currentThread().getName() + " 开始计算");

        while (!Thread.currentThread().isInterrupted()) {
            // 执行非阻塞计算
            for (int i = 0; i < 1_000_000; i++) {
                sum += i;
            }

            System.out.println("计算结果: " + sum);
            sum = 0;
        }

        System.out.println(Thread.currentThread().getName() + " 检测到中断信号,优雅退出");
    });

    worker.start();

    try {
        Thread.sleep(100); // 让worker线程有时间开始工作
    } catch (InterruptedException e) {
        e.printStackTrace();
    }

    System.out.println("主线程发送中断请求");
    worker.interrupt();
}

自定义阻塞方法的中断处理

当使用显式锁(如 ReentrantLock)或自定义阻塞机制时,需要特别注意中断处理:

java 复制代码
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.locks.Condition;

public static void customBlockingInterruptDemo() {
    Lock lock = new ReentrantLock();
    Condition condition = lock.newCondition();

    Thread waitingThread = new Thread(() -> {
        try {
            // 可中断的锁获取
            lock.lockInterruptibly();
            try {
                System.out.println("线程获取到锁,等待条件");
                // 等待条件,可被中断
                condition.await();
                System.out.println("条件满足,继续执行");
            } finally {
                lock.unlock();
            }
        } catch (InterruptedException e) {
            System.out.println("线程在等待锁或条件时被中断: " + e.getMessage());
        }
    });
    waitingThread.setName("custom-waiting-thread");

    waitingThread.start();

    try {
        Thread.sleep(1000); // 确保等待线程已经进入等待状态
    } catch (InterruptedException e) {
        e.printStackTrace();
    }

    System.out.println("主线程发送中断请求");
    waitingThread.interrupt();
}

中断机制的核心概念

  • interrupt()方法是一种协作式 而非强制式的线程中断机制
  • 线程可以选择如何响应中断请求,甚至可以完全忽略它
  • 良好的设计应确保线程能够及时检查并响应中断请求
flowchart TD A[线程正常执行] -->|"调用interrupt()"| B{线程状态?} B -->|阻塞状态\nsleep/wait/join| C[抛出InterruptedException\n中断标志被清除] B -->|运行状态| D[设置中断标志位] C -->|捕获异常| E{是否需要退出?} D -->|"周期性检查\nisInterrupted()"| F{标志为true?} E -->|是| G[重设中断标志\n线程退出] E -->|否| H[继续执行] F -->|是| I[执行清理\n线程退出] F -->|否| J[继续执行] style C fill:#f9a,stroke:#333 style D fill:#9cf,stroke:#333 style G fill:#cfc,stroke:#333 style I fill:#cfc,stroke:#333

isInterrupted() vs Thread.interrupted():

java 复制代码
public static void interruptFlags() {
    Thread thread = new Thread(() -> {
        while (!Thread.currentThread().isInterrupted()) {
            System.out.println("线程工作中...");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                // sleep()方法被中断会清除中断标志
                // 需要重新设置中断标志位以便外层循环检测
                Thread.currentThread().interrupt();
                System.out.println("中断发生,退出循环");
                break;
            }
        }
    });

    thread.start();

    // 主线程休眠3秒后中断工作线程
    try {
        Thread.sleep(3000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }

    thread.interrupt();
}

正确处理 InterruptedException 的模式

java 复制代码
public void run() {
    try {
        while (!Thread.currentThread().isInterrupted()) {
            // 执行任务

            // 执行可能被中断的阻塞操作
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                // 重新设置中断标志并退出循环
                Thread.currentThread().interrupt();
                break;
            }
        }
    } finally {
        // 执行清理工作
        System.out.println("线程结束,执行清理");
    }
}

四、守护线程(Daemon Thread)

守护线程是一种特殊的线程,它在后台为其他非守护线程提供服务。当所有非守护线程结束时,无论守护线程是否完成工作,JVM 都会退出。

守护线程的特点

  1. 当 JVM 中只剩下守护线程时,JVM 会退出
  2. 必须在线程启动前设置守护状态
  3. 典型应用:垃圾回收器、监控线程、心跳线程等

守护线程示例

java 复制代码
public static void daemonDemo() {
    Thread daemonThread = new Thread(() -> {
        while (true) {
            try {
                System.out.println("守护线程工作中...");
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                break;
            }
        }
    });
    daemonThread.setName("daemon-thread");

    // 设置为守护线程(必须在start之前)
    daemonThread.setDaemon(true);
    daemonThread.start();

    // 主线程工作3秒
    try {
        Thread.sleep(3000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }

    System.out.println("主线程结束,程序将退出");
    // 程序退出,守护线程也会终止
}

为何守护线程的 finally 块不保证执行?

当 JVM 准备退出时,如果只剩下守护线程,JVM 会强制终止这些线程而不等待它们完成当前工作。这意味着:

java 复制代码
public static void daemonFinalizationDemo() {
    Thread daemon = new Thread(() -> {
        try {
            System.out.println("守护线程开始执行");
            Thread.sleep(5000); // 模拟长时间操作
            System.out.println("守护线程完成工作"); // 可能不会执行
        } catch (InterruptedException e) {
            System.out.println("守护线程被中断");
        } finally {
            System.out.println("守护线程的finally块"); // 可能不会执行
            // 关键资源清理如文件关闭、连接释放可能不会发生
        }
    });

    daemon.setDaemon(true);
    daemon.start();

    // 主线程很快结束
    System.out.println("主线程结束,JVM准备退出");
}

守护线程应用场景

  1. 日志收集线程:周期性地将日志刷新到磁盘
  2. 缓存清理线程:后台清理过期缓存
  3. 心跳检测线程:定期发送心跳包
  4. 服务监控线程:监控服务健康状态

守护线程注意事项

  1. 守护线程创建的线程也是守护线程
  2. 守护线程不应该用于执行 I/O 操作,因为它们可能在操作完成前被强制终止
  3. finally 块在守护线程中不保证一定会执行
  4. 不要将连接池或事务性操作放在守护线程中

实践建议:不要在守护线程中执行以下操作:

  • 文件或数据库操作(可能导致数据损坏)
  • 网络连接管理(可能导致连接泄漏)
  • 事务处理(可能导致事务不完整)
graph TD A[JVM启动] --> B[创建用户线程] A --> C[创建守护线程] B --> D[用户线程运行] C --> E[守护线程运行] D --> F[用户线程结束] E --> G[守护线程继续] F -->|所有用户线程结束| H[JVM准备退出] G --> G H --> I[强制终止所有守护线程] I --> J[JVM退出] style B fill:#9cf,stroke:#333 style C fill:#cfc,stroke:#333 style D fill:#9cf,stroke:#333 style E fill:#cfc,stroke:#333 style F fill:#9cf,stroke:#333 style G fill:#cfc,stroke:#333 style I fill:#f9a,stroke:#333

五、线程常见问题及解决方案

1. 线程执行不成功

问题描述:创建了线程对象,但没有执行任务

常见原因

  • 调用 run()而不是 start()
  • 线程对象被垃圾回收
  • 主线程结束太快

解决方案

java 复制代码
// 正确启动线程
Thread thread = new Thread(() -> {
    System.out.println("任务执行");
});
thread.start(); // 不是thread.run()

// 保持对线程的引用
// 如果需要,可以使用join等待线程完成

2. 线程无法停止

问题描述:尝试停止一个正在运行的线程

常见错误:使用已废弃的 stop()、suspend()方法

为什么不应使用 stop()方法?

Thread.stop()方法在 Java 1.2 版本就被标记为废弃,主要原因包括:

  1. 不安全的资源释放:stop()会立即终止线程,不给线程任何机会执行清理工作,可能导致:
  • 文件句柄未关闭
  • 数据库连接未释放
  • 网络套接字未关闭
  • 临时文件未删除
  1. 数据不一致:强制终止可能使对象处于不一致状态
java 复制代码
// 假设有转账操作
synchronized void transfer(Account from, Account to, int amount) {
    from.debit(amount); // 假如在这行之后被stop()
    to.credit(amount);  // 这行永远不会执行
    // 结果:钱从from账户扣除了,但没有加到to账户
}
  1. 锁状态不确定:线程被 stop()时会释放所有锁,但可能导致受保护数据的不一致状态暴露给其他线程

推荐解决方案:使用中断机制或状态标志位

java 复制代码
// 通过标志位控制线程结束
class StoppableTask implements Runnable {
    private volatile boolean stopRequested = false;

    public void requestStop() {
        stopRequested = true;
    }

    @Override
    public void run() {
        while (!stopRequested) {
            // 执行任务
            System.out.println("线程工作中...");
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                // 响应中断
                Thread.currentThread().interrupt(); // 重设中断标志
                break;
            }
        }
        System.out.println("线程正常退出");
    }
}

// 使用示例
public static void stoppableThreadDemo() {
    StoppableTask task = new StoppableTask();
    Thread thread = new Thread(task);
    thread.start();

    // 主线程工作3秒
    try {
        Thread.sleep(3000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }

    // 请求线程停止
    task.requestStop();
}

3. 死锁问题

问题描述:两个或多个线程互相等待对方持有的锁,形成环路等待

死锁示例

java 复制代码
public static void deadlockDemo() {
    Object resource1 = new Object();
    Object resource2 = new Object();

    Thread thread1 = new Thread(() -> {
        synchronized (resource1) {
            System.out.println("线程1获取资源1");
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            System.out.println("线程1等待资源2");
            synchronized (resource2) {
                System.out.println("线程1获取资源2");
            }
        }
    });
    thread1.setName("deadlock-thread-1");

    Thread thread2 = new Thread(() -> {
        synchronized (resource2) {
            System.out.println("线程2获取资源2");
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            System.out.println("线程2等待资源1");
            synchronized (resource1) {
                System.out.println("线程2获取资源1");
            }
        }
    });
    thread2.setName("deadlock-thread-2");

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

其他常见并发问题

除了死锁外,多线程编程中还存在以下并发问题:

线程饥饿(Starvation)

当线程长时间无法获取所需资源而无法执行时,就会发生线程饥饿:

java 复制代码
public static void starvationDemo() {
    Object lock = new Object();

    // 创建一个高优先级线程,它会长时间占用锁
    Thread highPriorityThread = new Thread(() -> {
        synchronized (lock) {
            System.out.println("高优先级线程获取到锁");
            while (true) {
                // 持续占用锁不释放
            }
        }
    });
    highPriorityThread.setPriority(Thread.MAX_PRIORITY);
    highPriorityThread.setName("high-priority");

    // 创建低优先级线程,它很难获取到锁
    Thread lowPriorityThread = new Thread(() -> {
        synchronized (lock) {
            //
            System.out.println("低优先级线程获取到锁");
        }
    });
    lowPriorityThread.setPriority(Thread.MIN_PRIORITY);
    lowPriorityThread.setName("low-priority");

    highPriorityThread.start();
    lowPriorityThread.start();
}

避免饥饿的策略

  • 使用公平锁(如 ReentrantLock(true))
  • 避免长时间持有锁
  • 适当调整线程优先级

注意:在现代 JVM 中,线程优先级的效果可能不如预期。优先级调整是一种建议而非强制执行的机制,不同操作系统实现差异很大。推荐优先通过合理的锁使用策略(减少锁持有时间、使用公平锁等)来避免线程饥饿,而不是过度依赖优先级调整。

活锁(Livelock)

当线程不断相互礼让,都无法向前推进时,就会发生活锁:

java 复制代码
public static void simpleLivelockDemo() {
    final AtomicBoolean firstThreadGiveWay = new AtomicBoolean(true);
    final AtomicBoolean secondThreadGiveWay = new AtomicBoolean(true);

    Thread thread1 = new Thread(() -> {
        while (true) {
            // 第一个线程检查"对方是否在让路"
            if (secondThreadGiveWay.get()) {
                // 如果对方在让路,自己也让路
                System.out.println("线程1说:对方在让路,我也让路");
                firstThreadGiveWay.set(true);
            } else {
                // 如果对方不让路,自己也不让了,开始工作
                System.out.println("线程1说:对方不让路了,我开始工作");
                break;
            }

            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                return;
            }
        }
    });

    Thread thread2 = new Thread(() -> {
        while (true) {
            // 第二个线程检查"对方是否在让路"
            if (firstThreadGiveWay.get()) {
                // 如果对方在让路,自己也让路
                System.out.println("线程2说:对方在让路,我也让路");
                secondThreadGiveWay.set(true);
            } else {
                // 如果对方不让路,自己也不让了,开始工作
                System.out.println("线程2说:对方不让路了,我开始工作");
                break;
            }

            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                return;
            }
        }
    });

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

    // 这个活锁将永远持续下去,因为两个线程都在互相礼让
    // 在实际应用中,可以通过随机等待或优先级解决
}

避免活锁的策略

  • 引入随机因素(如随机等待时间)
  • 使用优先级或资源排序
  • 设置超时机制
graph TD A[正常执行] --> B{并发问题类型} B -->|线程无法前进| C{问题具体类型} C -->|互相持有对方需要的锁| D[死锁Deadlock] C -->|互相礼让导致无法前进| E[活锁Livelock] C -->|低优先级线程长期得不到执行| F[饥饿Starvation] D --> G[环路等待线程完全阻塞] E --> H[线程持续运行但无法完成任务] F --> I[部分线程几乎不被调度] style D fill:#f9a,stroke:#333 style E fill:#9cf,stroke:#333 style F fill:#fcf,stroke:#333

4. 线程异常处理

线程中未捕获的异常不会被传递到主线程,需要特殊处理:

java 复制代码
public static void exceptionHandlerDemo() {
    Thread thread = new Thread(() -> {
        System.out.println("线程开始执行");
        throw new RuntimeException("线程执行异常");
    });
    thread.setName("exception-thread");

    // 设置未捕获异常处理器
    thread.setUncaughtExceptionHandler((t, e) -> {
        System.out.println("捕获到线程 " + t.getName() + " 的异常:" + e.getMessage());
        // 可以记录日志、发送告警等
    });

    thread.start();
}

六、总结

类别 方法/状态 特点 注意事项
线程状态 NEW 线程已创建未启动 使用 start()方法启动
RUNNABLE 包含就绪和运行中 线程获得 CPU 时间片才真正运行
BLOCKED 等待获取 synchronized 锁 避免长时间阻塞
WAITING 无限期等待其他线程操作 可能导致程序挂起
TIMED_WAITING 有超时时间的等待 设置合理的超时时间
TERMINATED 线程执行完毕 可以用 isAlive()检查
线程控制 start() 启动新线程 不能重复调用
run() 普通方法调用,不创建新线程 不要直接调用 run()方法
sleep() 线程休眠,不释放锁 处理 InterruptedException
yield() 线程让步,提示调度器 效果不可靠,依赖系统实现
join() 等待其他线程完成 可能导致当前线程阻塞
interrupt() 中断线程,协作式机制 结合 isInterrupted()使用
线程类型 用户线程 默认线程类型 JVM 等待用户线程结束
守护线程 为其他线程服务的后台线程 不执行重要任务,finally 块不保证执行
并发问题 死锁 线程互相等待对方持有的锁 按固定顺序获取锁,使用 tryLock()
活锁 线程不断相互礼让 引入随机因素,设置优先级
饥饿 线程长期无法获取资源 使用公平锁,避免长时间持有锁

通过本文,我们深入了解了 Java 线程的生命周期和基础操作方法。掌握这些知识对于编写高质量的多线程程序至关重要。在实际开发中,请记住:理解线程状态转换、合理使用线程控制方法、妥善处理线程异常、选择适当的线程通信机制,这些都是构建稳定多线程应用的基石。

在下一篇文章中,我们将探讨线程安全问题与基本解决方案,敬请期待!


感谢您耐心阅读到这里!如果觉得本文对您有帮助,欢迎点赞 👍、收藏 ⭐、分享给需要的朋友,您的支持是我持续输出技术干货的最大动力!

如果想获取更多 Java 技术深度解析,欢迎点击头像关注我,后续会每日更新高质量技术文章,陪您一起进阶成长~

相关推荐
Hanson Huang1 小时前
【数据结构】堆排序详细图解
java·数据结构·排序算法·堆排序
慕容静漪1 小时前
如何本地安装Python Flask并结合内网穿透实现远程开发
开发语言·后端·golang
ErizJ1 小时前
Golang|锁相关
开发语言·后端·golang
路在脚下@1 小时前
Redis实现分布式定时任务
java·redis
xrkhy1 小时前
idea的快捷键使用以及相关设置
java·ide·intellij-idea
巨龙之路2 小时前
Lua中的元表
java·开发语言·lua
烛阴2 小时前
手把手教你搭建 Express 日志系统,告别线上事故!
javascript·后端·express
良许Linux2 小时前
请问做嵌入式开发C语言应该学到什么水平?
后端
Pitayafruit2 小时前
SpringBoot整合Flowable【08】- 前后端如何交互
spring boot·后端·workflow
花花鱼2 小时前
itext7 html2pdf 将html文本转为pdf
java·pdf