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

掌握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("bbb"); // 回退光标,清除点         }     }      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()方法来检查输入缓冲区中是否有数据可用,而不会阻塞当前线程。

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

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

豆包AI编程

豆包推出的AI编程助手

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

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

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("bbb   bbb"); // 回退光标,清除点并留空,再回退                 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(“bbb bbb”):这行代码用于清除屏幕上的三个点。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应用程序的关键。

暂无评论

发送评论 编辑评论


				
上一篇
下一篇
text=ZqhQzanResources