首页 > Java > java教程 > 正文

掌握Java中通过用户输入优雅终止无限循环的并发编程实践

DDD
发布: 2025-10-29 18:00:07
原创
143人浏览过

掌握Java中通过用户输入优雅终止无限循环的并发编程实践

本教程详细探讨了在java中如何利用多线程和非阻塞输入机制,实现一个可由用户输入(如回车键)中断的无限循环,同时运行如加载动画等并发任务。文章解释了传统阻塞式输入方法的局限性,并提供了一个基于`volatile`标志和`inputstream.available()`的完整解决方案,确保动画流畅运行的同时,能及时响应用户中断指令,从而提高程序的交互性和用户体验。

在Java应用程序开发中,我们经常会遇到需要执行一个持续性任务(如加载动画、数据监听)直到用户发出停止指令的场景。一个常见的挑战是,如何既能保持任务的持续性,又能实时响应用户的输入,特别是当用户输入是用于中断任务时。本文将深入探讨这一问题,并提供一个基于多线程和非阻塞输入的高效解决方案。

传统阻塞式输入方法的局限性

考虑一个常见的需求:显示一个循环的加载动画(例如,三个点“...”不断闪烁),直到用户按下回车键才停止。初学者可能会尝试在一个无限循环中显示动画,并在循环内部或之后调用System.in.read()来等待用户输入。

public class BlockingLoopExample {

    public static void pause(long duration) {
        try {
            Thread.sleep(duration);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt(); // 重新设置中断标志
        }
    }

    public static void loading() {
        while (true) { // 尝试无限循环显示动画
            pause(500);
            for (int i = 0; i < 3; i++) {
                System.out.print(".");
                pause(500);
            }
            System.out.print("\b\b\b"); // 回退光标,清除点
        }
    }

    public static void main(String[] args) {
        System.out.println("Loading... Press Enter to stop.");
        loading(); // 动画开始
        // 理论上这里应该等待输入,但实际上永远不会执行到这里
        try {
            System.in.read(); // 阻塞等待输入
        } catch (Exception e) {
            e.printStackTrace();
        }
        System.out.println("\nStopped.");
    }
}
登录后复制

上述代码存在两个主要问题:

  1. 阻塞问题: loading()方法内部是一个无限循环while(true),这意味着程序将永远停留在loading()方法中,main方法中System.in.read()那一行代码永远不会被执行到。因此,用户无法通过输入来停止动画。
  2. 并发需求: 即便我们将System.in.read()放在loading()循环内部,System.in.read()本身是一个阻塞调用。这意味着一旦程序执行到System.in.read(),它就会暂停,直到用户输入数据。这会导致动画停止,无法实现动画与输入监听的并发进行。

为了解决这些问题,我们需要引入多线程编程和非阻塞输入的概念。

立即进入豆包AI人工智官网入口”;

立即学习豆包AI人工智能在线问答入口”;

解决方案:多线程与非阻塞输入

实现动画与输入监听并发进行并优雅终止无限循环的关键在于:

  1. 使用单独的线程处理动画: 将动画逻辑封装在一个独立的线程中运行。
  2. 使用单独的线程监听输入: 另一个线程专门负责监听用户的输入。
  3. 共享状态与volatile关键字: 两个线程需要通过一个共享的标志(例如一个boolean变量)来通信。当输入线程检测到用户输入时,它会修改这个标志,动画线程则定期检查这个标志以决定是否停止。为了确保不同线程对共享变量的可见性,这个标志必须声明为volatile。
  4. 非阻塞输入检查: 输入监听线程不应使用阻塞式的System.in.read()。相反,它应该使用InputStream.available()方法来检查输入缓冲区中是否有数据可用,而不会阻塞当前线程。

下面是基于这些原则的完整解决方案代码:

豆包AI编程
豆包AI编程

豆包推出的AI编程助手

豆包AI编程483
查看详情 豆包AI编程
import java.io.IOException;
import java.io.InputStream;

public class ConcurrentLoadingAnimation {

    // 使用 volatile 关键字确保 stopFlag 对所有线程的可见性
    private static volatile boolean stopFlag = false;

    /**
     * 模拟暂停一段时间的方法
     * @param duration 暂停时长(毫秒)
     */
    public static void pause(long duration) {
        try {
            Thread.sleep(duration);
        } catch (InterruptedException e) {
            // 当线程被中断时,设置中断标志并退出,以便外部可以捕获中断
            Thread.currentThread().interrupt();
            System.err.println("Loading thread interrupted.");
            stopFlag = true; // 强制停止动画
        }
    }

    /**
     * 运行加载动画的线程任务
     */
    static class LoadingTask implements Runnable {
        @Override
        public void run() {
            System.out.println("Loading... Press Enter to stop.");
            while (!stopFlag) { // 只要 stopFlag 为 false,就继续循环
                for (int i = 0; i < 3; i++) {
                    if (stopFlag) break; // 每次打印前检查是否需要停止
                    System.out.print(".");
                    pause(300); // 调整暂停时间以适应动画速度
                }
                if (stopFlag) break; // 循环结束后再次检查
                System.out.print("\b\b\b   \b\b\b"); // 回退光标,清除点并留空,再回退
                pause(300);
            }
            // 动画停止后,清除可能残留的点
            System.out.print("\r                     \r"); // 清除整行并回车
            System.out.println("Loading stopped.");
        }
    }

    /**
     * 监听用户输入的线程任务
     */
    static class InputMonitorTask implements Runnable {
        @Override
        public void run() {
            try (InputStream in = System.in) { // 使用 try-with-resources 确保 InputStream 关闭
                while (!stopFlag) { // 只要 stopFlag 为 false,就继续监听
                    if (in.available() > 0) { // 检查输入缓冲区是否有数据
                        // 读取所有可用的输入,直到遇到换行符或缓冲区清空
                        while (in.available() > 0) {
                            int charCode = in.read();
                            if (charCode == '\n' || charCode == '\r') { // 检测到回车键
                                stopFlag = true; // 设置停止标志
                                break;
                            }
                        }
                    }
                    pause(50); // 短暂暂停,避免CPU空转过高
                }
            } catch (IOException e) {
                System.err.println("Error reading from input: " + e.getMessage());
            }
        }
    }

    public static void main(String[] args) {
        // 创建并启动加载动画线程
        Thread loadingThread = new Thread(new LoadingTask(), "Loading-Animation-Thread");
        loadingThread.start();

        // 创建并启动输入监听线程
        Thread inputMonitorThread = new Thread(new InputMonitorTask(), "Input-Monitor-Thread");
        inputMonitorThread.start();

        // 主线程等待两个子线程完成
        try {
            loadingThread.join(); // 等待动画线程结束
            inputMonitorThread.join(); // 等待输入监听线程结束
        } catch (InterruptedException e) {
            System.err.println("Main thread interrupted.");
            Thread.currentThread().interrupt();
        }

        System.out.println("Program finished.");
    }
}
登录后复制

代码详解与注意事项

  1. volatile boolean stopFlag:

    • 这是一个共享变量,用于在InputMonitorTask和LoadingTask之间传递停止信号。
    • volatile关键字确保对stopFlag变量的修改能够立即被所有线程可见,避免了由于缓存不一致导致的同步问题。这是实现线程间通信的关键。
  2. LoadingTask (Runnable):

    • run()方法包含动画逻辑。
    • while (!stopFlag)循环是动画持续运行的条件。
    • 在每次打印点之前和循环结束之后,都会检查stopFlag。一旦stopFlag变为true,循环立即终止。
    • System.out.print("\b\b\b \b\b\b"):这行代码用于清除屏幕上的三个点。\b是退格符,它会将光标向前移动一个位置。连续使用\b可以清除字符。后面打印三个空格再用\b回退,是为了确保点被完全覆盖,且光标回到原始位置。
    • System.out.print("\r \r"): 在动画停止时,为了确保屏幕干净,使用回车符\r将光标移到行首,然后打印足够多的空格覆盖可能残留的动画字符,再用\r将光标移回行首。
  3. InputMonitorTask (Runnable):

    • try (InputStream in = System.in):使用Java 7引入的try-with-resources语句,确保System.in流在不再需要时能够被正确关闭(尽管System.in通常不建议关闭,但这里作为示例)。
    • in.available() > 0:这是实现非阻塞输入的关键。它返回输入流中可供读取的字节数。如果大于0,说明有数据输入,此时才尝试读取。
    • in.read():当available() > 0时,读取一个字节。
    • if (charCode == '\n' || charCode == '\r'):检测用户是否按下了回车键(在不同操作系统上,回车键可能产生\n或\r,或者两者都有)。一旦检测到,就将stopFlag设置为true,从而通知LoadingTask停止。
    • pause(50):在没有输入时,输入监听线程会短暂暂停,避免CPU空转,降低资源消耗。
  4. main方法:

    • 创建并启动LoadingTask和InputMonitorTask的两个Thread实例。
    • loadingThread.join()和inputMonitorThread.join():主线程会等待这两个子线程执行完毕。这意味着只有当stopFlag被设置为true,两个子线程都自然终止后,主线程才会继续执行并打印"Program finished."。

总结

通过上述多线程和非阻塞输入的方法,我们成功地解决了在Java中同时运行动画和监听用户输入的问题。这种模式在需要后台任务持续运行,同时需要用户随时介入终止的场景中非常有用。理解volatile关键字在线程间通信中的作用以及InputStream.available()的非阻塞特性,是构建响应式和高效并发Java应用程序的关键。

以上就是掌握Java中通过用户输入优雅终止无限循环的并发编程实践的详细内容,更多请关注php中文网其它相关文章!

编程速学教程(入门课程)
编程速学教程(入门课程)

编程怎么学习?编程怎么入门?编程在哪学?编程怎么学才快?不用担心,这里为大家提供了编程速学教程(入门课程),有需要的小伙伴保存下载就能学习啦!

下载
来源:php中文网
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn
最新问题
开源免费商场系统广告
热门教程
更多>
最新下载
更多>
网站特效
网站源码
网站素材
前端模板
关于我们 免责申明 意见反馈 讲师合作 广告合作 最新更新 English
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送
PHP中文网APP
随时随地碎片化学习
PHP中文网抖音号
发现有趣的

Copyright 2014-2025 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号