深入探讨Java线程:状态转换、创建方式与安全停止

举报
bug菌 发表于 2024/09/30 16:37:40 2024/09/30
【摘要】 咦咦咦,各位小可爱,我是你们的好伙伴——bug菌,今天又来给大家普及Java SE相关知识点了,别躲起来啊,听我讲干货还不快点赞,赞多了我就有动力讲得更嗨啦!所以呀,养成先点赞后阅读的好习惯,别被干货淹没了哦~🏆本文收录于「滚雪球学Java」专栏中,这个专栏专为有志于提升Java技能的你打造,覆盖Java编程的方方面面,助你从零基础到掌握Java开发的精髓。赶紧关注,收藏,学习吧!环境说明...

咦咦咦,各位小可爱,我是你们的好伙伴——bug菌,今天又来给大家普及Java SE相关知识点了,别躲起来啊,听我讲干货还不快点赞,赞多了我就有动力讲得更嗨啦!所以呀,养成先点赞后阅读的好习惯,别被干货淹没了哦~


🏆本文收录于「滚雪球学Java」专栏中,这个专栏专为有志于提升Java技能的你打造,覆盖Java编程的方方面面,助你从零基础到掌握Java开发的精髓。赶紧关注,收藏,学习吧!

环境说明:Windows 10 + IntelliJ IDEA 2021.3.2 + Jdk 1.8

前言

在现代软件开发中,线程是实现并发和提高程序性能的重要工具。Java语言通过内建的线程机制,为开发者提供了丰富的线程管理能力。然而,对于许多初学者来说,理解线程的生命周期、创建方式以及如何安全地停止线程,仍然是一个挑战。本文将围绕“线程状态转换、创建线程的几种方式、线程如何停止”进行深入探讨,并结合实际案例,使读者能够更好地掌握线程的相关知识。

线程的状态转换

Java中的线程在其生命周期中会经历多种状态,每种状态都有特定的含义和特点。线程的状态主要包括以下几种:

  1. 新建(New):线程对象被创建,但未调用start()方法,此时线程处于新建状态。
  2. 就绪(Runnable):线程调用start()方法后,进入就绪状态,等待CPU调度。值得注意的是,这一状态包括了两种可能的情况:线程在内存中就绪但并不一定立即执行。
  3. 运行(Running):当线程被CPU选中时,它处于运行状态,可以执行其任务。此时,线程获得了CPU的控制权。
  4. 阻塞(Blocked):线程因等待某个资源(如I/O操作)而无法继续执行,进入阻塞状态。例如,线程在等待从输入流读取数据时会进入此状态。
  5. 等待(Waiting):线程在等待其他线程的特定操作(如join()wait())时进入此状态。此时,线程不占用CPU资源,处于一种空闲状态。
  6. 超时等待(Timed Waiting):线程在等待某个条件,但设置了超时。超过时间仍未满足条件则返回就绪状态。例如,使用Thread.sleep(1000),线程会在此状态下等待1秒。
  7. 终止(Terminated):线程执行完毕或因异常而结束,进入终止状态。此时,线程的生命周期结束。

状态转换示意图

+---------------------+
|        New          |
+---------------------+
          |
          v
+---------------------+
|       Runnable      |
+---------------------+
          |
          v
+---------------------+
|       Running       |
+---------------------+
          |
  +-------+-------+
  |               |
  v               v
+---------------------+    +---------------------+
|       Blocked       |    |      Waiting        |
+---------------------+    +---------------------+
          |                          |
          v                          v
+---------------------+    +---------------------+
|      Terminated     |    |   Timed Waiting     |
+---------------------+    +---------------------+

创建线程的几种方式

Java提供了多种创建线程的方式,主要包括以下几种:

1. 继承Thread

通过继承Thread类来创建线程是最常见的方式。重写run()方法,定义线程的执行逻辑。此方式适用于较为简单的任务,不需要共享状态的场景。

示例:

class MyThread extends Thread {
    public void run() {
        System.out.println("线程 " + Thread.currentThread().getName() + " 正在执行。");
    }
}

public class ThreadExample {
    public static void main(String[] args) {
        MyThread thread = new MyThread();
        thread.start(); // 启动线程
    }
}

2. 实现Runnable接口

通过实现Runnable接口,允许将线程的执行逻辑与其他对象的实现分离。这种方法更灵活,便于共享资源。

示例:

class MyRunnable implements Runnable {
    public void run() {
        System.out.println("线程 " + Thread.currentThread().getName() + " 正在执行。");
    }
}

public class RunnableExample {
    public static void main(String[] args) {
        Thread thread = new Thread(new MyRunnable());
        thread.start(); // 启动线程
    }
}

3. 使用Callable接口与Future

Callable接口与Runnable类似,但它可以返回结果并抛出异常。通过ExecutorService可创建线程池并管理线程,推荐用于复杂任务。

示例:

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

class MyCallable implements Callable<String> {
    public String call() {
        return "线程 " + Thread.currentThread().getName() + " 执行完成。";
    }
}

public class CallableExample {
    public static void main(String[] args) throws InterruptedException, ExecutionException {
        ExecutorService executor = Executors.newFixedThreadPool(1);
        Future<String> future = executor.submit(new MyCallable());
        System.out.println(future.get()); // 获取线程返回结果
        executor.shutdown();
    }
}

4. 使用ForkJoinPool

在处理大数据或复杂计算时,ForkJoinPool可以将任务拆分成多个子任务并行执行,适合高性能计算。

示例:

import java.util.concurrent.RecursiveTask;
import java.util.concurrent.ForkJoinPool;

class SumTask extends RecursiveTask<Integer> {
    private final int start;
    private final int end;

    public SumTask(int start, int end) {
        this.start = start;
        this.end = end;
    }

    @Override
    protected Integer compute() {
        if (end - start <= 10) {
            int sum = 0;
            for (int i = start; i < end; i++) {
                sum += i;
            }
            return sum;
        } else {
            int mid = (start + end) / 2;
            SumTask leftTask = new SumTask(start, mid);
            SumTask rightTask = new SumTask(mid, end);
            leftTask.fork(); // Fork the left task
            return rightTask.compute() + leftTask.join(); // Wait for the left task to complete
        }
    }
}

public class ForkJoinExample {
    public static void main(String[] args) {
        ForkJoinPool pool = new ForkJoinPool();
        SumTask task = new SumTask(1, 100);
        int result = pool.invoke(task);
        System.out.println("1到100的和为:" + result);
    }
}

线程的安全停止

安全停止线程是一项重要的任务。直接调用stop()方法是不推荐的,因为它可能导致资源不被释放或数据不一致。以下是一些推荐的安全停止线程的方法:

1. 使用标志位

通过设置一个标志位,让线程在执行过程中定期检查该标志位,从而决定是否停止执行。这种方式简单易用,适用于大部分场景。

示例:

class StoppableThread extends Thread {
    private volatile boolean running = true; // 使用volatile确保可见性

    public void run() {
        while (running) {
            System.out.println("线程 " + Thread.currentThread().getName() + " 正在执行。");
            try {
                Thread.sleep(1000); // 模拟工作
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt(); // 恢复中断状态
            }
        }
        System.out.println("线程 " + Thread.currentThread().getName() + " 已停止。");
    }

    public void stopThread() {
        running = false; // 设置标志位
    }
}

public class StopThreadExample {
    public static void main(String[] args) throws InterruptedException {
        StoppableThread thread = new StoppableThread();
        thread.start();
        Thread.sleep(3000); // 主线程等待3秒
        thread.stopThread(); // 安全停止线程
    }
}

代码解析

  在本次的代码演示中,我将会深入剖析每句代码,详细阐述其背后的设计思想和实现逻辑。通过这样的讲解方式,我希望能够引导同学们逐步构建起对代码的深刻理解。我会先从代码的结构开始,逐步拆解每个模块的功能和作用,并指出关键的代码段,并解释它们是如何协同运行的。通过这样的讲解和实践相结合的方式,我相信每位同学都能够对代码有更深入的理解,并能够早日将其掌握,应用到自己的学习和工作中。

这段Java代码定义了一个名为StoppableThread的类,它继承自Thread类,用于创建一个可以被外部控制停止的线程。另外,还有一个名为StopThreadExample的类,它演示了如何使用StoppableThread。以下是代码的逐行解读:

StoppableThread 类
  1. class StoppableThread extends Thread 定义了一个继承自Thread的类StoppableThread

  2. private volatile boolean running = true; 定义了一个running成员变量,用来控制线程的执行。使用volatile关键字确保多线程环境下的可见性和有序性。

run 方法
  1. public void run() 重写了Thread类的run方法。

  2. while (running) 使用一个while循环,只要running标志为true,线程就继续执行。

  3. System.out.println("线程 " + Thread.currentThread().getName() + " 正在执行。"); 打印当前线程的名称和执行状态。

  4. try { Thread.sleep(1000);} catch (InterruptedException e) 尝试使当前线程暂停1秒钟,模拟工作。如果线程在睡眠期间被中断,会抛出InterruptedException

  5. Thread.currentThread().interrupt(); 如果线程在睡眠期间被中断,捕获异常后调用interrupt()方法恢复中断状态。

stopThread 方法
  1. public void stopThread() 定义了一个公共方法stopThread,用于外部控制线程停止。

  2. running = false;running标志设置为false,这样while循环会退出,线程将停止执行。

StopThreadExample 类
  1. public class StopThreadExample 定义了一个公共类StopThreadExample
main 方法
  1. public static void main(String[] args) 是程序的入口点。

  2. StoppableThread thread = new StoppableThread(); 创建了StoppableThread的一个实例。

  3. thread.start(); 启动线程。

  4. Thread.sleep(3000); 主线程休眠3秒钟,给子线程执行留出时间。

  5. thread.stopThread(); 调用stopThread方法,通过改变running标志的值来通知线程停止执行。

小结

这个StopThreadExample类演示了如何创建一个可以被外部控制停止的线程。通过定义一个running标志并提供一个公共方法stopThread来改变这个标志的值,可以安全地停止线程。这种方式比使用Thread.stop()方法更安全,因为Thread.stop()方法已经被废弃,它以非安全的方式终止线程,可能会导致资源泄露或者其他不可预知的问题。

在实际应用中,应该尽量避免使用Thread.stop()方法,而是使用类似上述代码中的方法来控制线程的停止。

2. 使用interrupt()方法

可以通过interrupt()方法请求线程中断。如果线程正在阻塞或休眠,抛出InterruptedException,可以在捕获异常后进行清理和退出。这种方式是Java多线程编程中非常重要的技巧。

代码示例

class InterruptibleThread extends Thread {
    public void run() {
        try {
            while (!isInterrupted()) {
                System.out.println("线程 " + Thread.currentThread().getName() + " 正在执行。");
                Thread.sleep(1000); // 模拟工作
            }
        } catch (InterruptedException e) {
            System.out.println("线程 " + Thread.currentThread().getName() + " 已被中断。");
            // 执行必要的清理工作
        }
    }
}

public class InterruptThreadExample {
    public static void main(String[] args) throws InterruptedException {
        InterruptibleThread thread = new InterruptibleThread();
        thread.start();
        Thread.sleep(3000); // 主线程等待3秒
        thread.interrupt(); // 请求中断
    }
}

代码解析

  在本次的代码演示中,我将会深入剖析每句代码,详细阐述其背后的设计思想和实现逻辑。通过这样的讲解方式,我希望能够引导同学们逐步构建起对代码的深刻理解。我会先从代码的结构开始,逐步拆解每个模块的功能和作用,并指出关键的代码段,并解释它们是如何协同运行的。通过这样的讲解和实践相结合的方式,我相信每位同学都能够对代码有更深入的理解,并能够早日将其掌握,应用到自己的学习和工作中。

这段Java代码展示了如何使用ExecutorService来管理线程,以及如何优雅地关闭线程池。以下是代码的逐行解读:

导入必要的类
  1. import java.util.concurrent.ExecutorService;
  2. import java.util.concurrent.Executors;

这两行导入了ExecutorServiceExecutors类,它们都是Java并发包中的一部分。

ExecutorServiceExample 类
  1. public class ExecutorServiceExample 定义了一个公共类ExecutorServiceExample
main 方法
  1. public static void main(String[] args) throws InterruptedException 是程序的入口点,声明了可能抛出的InterruptedException异常。
创建线程池
  1. ExecutorService executor = Executors.newFixedThreadPool(2); 创建了一个固定大小的线程池,池中线程数为2。
定义任务
  1. Runnable task = () -> 定义了一个Runnable任务,使用lambda表达式。

  2. try { 开始一个try块,用于捕获可能发生的异常。

  3. while (!Thread.currentThread().isInterrupted()) { 使用一个while循环,只要当前线程没有被中断,就继续执行。

  4. System.out.println("线程 " + Thread.currentThread().getName() + " 正在执行。"); 打印当前线程的名称和执行状态。

  5. Thread.sleep(1000); 调用Thread.sleep方法,使当前线程暂停1秒钟,模拟工作。

  6. } catch (InterruptedException e) { 捕获InterruptedException异常。

  7. Thread.currentThread().interrupt(); 恢复中断状态。当一个线程在阻塞状态(如sleep)下被中断时,它会清除中断状态并抛出InterruptedException。这里通过调用interrupt()方法来恢复中断状态,这样调用者就知道线程被中断过了。

提交任务
  1. executor.submit(task); 将定义的任务提交给线程池执行。
主线程等待
  1. Thread.sleep(3000); 主线程休眠3秒钟,给任务执行留出时间。
关闭线程池
  1. executor.shutdown(); 调用shutdown方法优雅地关闭线程池。这个方法不会立即关闭线程池,而是等待所有已提交的任务执行完成后再关闭。
小结

这个ExecutorServiceExample类演示了如何使用ExecutorService来创建线程池、提交任务,并优雅地关闭线程池。在任务执行过程中,通过捕获InterruptedException并恢复中断状态,可以正确处理线程的中断。此外,通过在主线程中等待一段时间,可以确保任务有足够的时间执行,然后再关闭线程池。

这种模式在实际应用中非常有用,特别是在需要并发执行多个任务时。通过使用线程池,可以有效地管理线程资源,提高程序的性能和响应能力。

3. 使用ExecutorService的shutdown方法

当使用线程池时,可以调用shutdown()shutdownNow()方法安全地停止线程池中的线程。这是一种优雅的关闭线程池的方式,可以确保线程资源被正确释放。

代码示例:

import java.util.concurrent

.ExecutorService;
import java.util.concurrent.Executors;

public class ExecutorServiceExample {
    public static void main(String[] args) throws InterruptedException {
        ExecutorService executor = Executors.newFixedThreadPool(2);
        Runnable task = () -> {
            try {
                while (!Thread.currentThread().isInterrupted()) {
                    System.out.println("线程 " + Thread.currentThread().getName() + " 正在执行。");
                    Thread.sleep(1000); // 模拟工作
                }
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt(); // 恢复中断状态
            }
        };

        executor.submit(task);
        Thread.sleep(3000); // 主线程等待3秒
        executor.shutdown(); // 优雅地关闭线程池
    }
}

代码解析

  在本次的代码演示中,我将会深入剖析每句代码,详细阐述其背后的设计思想和实现逻辑。通过这样的讲解方式,我希望能够引导同学们逐步构建起对代码的深刻理解。我会先从代码的结构开始,逐步拆解每个模块的功能和作用,并指出关键的代码段,并解释它们是如何协同运行的。通过这样的讲解和实践相结合的方式,我相信每位同学都能够对代码有更深入的理解,并能够早日将其掌握,应用到自己的学习和工作中。

这段Java代码展示了如何使用ExecutorService来管理线程,以及如何优雅地关闭线程池。以下是代码的逐行解读:

导入必要的类
  1. import java.util.concurrent.ExecutorService;
  2. import java.util.concurrent.Executors;

这两行导入了ExecutorServiceExecutors类,它们都是Java并发包中的一部分。

ExecutorServiceExample 类
  1. public class ExecutorServiceExample 定义了一个公共类ExecutorServiceExample
main 方法
  1. public static void main(String[] args) throws InterruptedException 是程序的入口点,声明了可能抛出的InterruptedException异常。
创建线程池
  1. ExecutorService executor = Executors.newFixedThreadPool(2); 创建了一个固定大小的线程池,池中线程数为2。
定义任务
  1. Runnable task = () -> 定义了一个Runnable任务,使用lambda表达式。

  2. try { 开始一个try块,用于捕获可能发生的异常。

  3. while (!Thread.currentThread().isInterrupted()) { 使用一个while循环,只要当前线程没有被中断,就继续执行。

  4. System.out.println("线程 " + Thread.currentThread().getName() + " 正在执行。"); 打印当前线程的名称和执行状态。

  5. Thread.sleep(1000); 调用Thread.sleep方法,使当前线程暂停1秒钟,模拟工作。

  6. } catch (InterruptedException e) { 捕获InterruptedException异常。

  7. Thread.currentThread().interrupt(); 恢复中断状态。当一个线程在阻塞状态(如sleep)下被中断时,它会清除中断状态并抛出InterruptedException。这里通过调用interrupt()方法来恢复中断状态,这样调用者就知道线程被中断过了。

提交任务
  1. executor.submit(task); 将定义的任务提交给线程池执行。
主线程等待
  1. Thread.sleep(3000); 主线程休眠3秒钟,给任务执行留出时间。
关闭线程池
  1. executor.shutdown(); 调用shutdown方法优雅地关闭线程池。这个方法不会立即关闭线程池,而是等待所有已提交的任务执行完成后再关闭。
小结

这个ExecutorServiceExample类演示了如何使用ExecutorService来创建线程池、提交任务,并优雅地关闭线程池。在任务执行过程中,通过捕获InterruptedException并恢复中断状态,可以正确处理线程的中断。此外,通过在主线程中等待一段时间,可以确保任务有足够的时间执行,然后再关闭线程池。

这种模式在实际应用中非常有用,特别是在需要并发执行多个任务时。通过使用线程池,可以有效地管理线程资源,提高程序的性能和响应能力。

结论

在Java中,线程的管理是开发中一个不可忽视的重要方面。通过理解线程的状态转换、灵活选择线程的创建方式以及安全地停止线程,我们可以提高应用程序的性能和可靠性。希望本文能为读者提供有价值的参考,帮助你在Java多线程编程的道路上越走越远。

☀️建议/推荐你

无论你是计算机专业的学生,还是对编程有兴趣的小伙伴,都建议直接毫无顾忌的学习此专栏「滚雪球学Java」,bug菌郑重承诺,凡是学习此专栏的同学,均能获取到所需的知识和技能,全网最快速入门Java编程,就像滚雪球一样,越滚越大,指数级提升。

码字不易,如果这篇文章对你有所帮助,帮忙给bug菌来个一键三连(关注、点赞、收藏) ,您的支持就是我坚持写作分享知识点传播技术的最大动力。
  同时也推荐大家关注我的硬核公众号:「猿圈奇妙屋」 ;以第一手学习bug菌的首发干货,不仅能学习更多技术硬货,还可白嫖最新BAT大厂面试真题、4000G Pdf技术书籍、万份简历/PPT模板、技术文章Markdown文档等海量资料,你想要的我都有!

📣关于我

我是bug菌,CSDN | 掘金 | infoQ | 51CTO 等社区博客专家,历届博客之星Top30,掘金年度人气作者Top40,51CTO年度博主Top12,掘金等平台签约作者,华为云 | 阿里云| 腾讯云等社区优质创作者,全网粉丝合计30w+ ;硬核微信公众号「猿圈奇妙屋」,欢迎你的加入!免费白嫖最新BAT互联网公司面试题、4000G pdf电子书籍、简历模板等海量资料。


–End

【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。