如何让多个 Java 线程根据用户的请求暂停和恢复?

How do I get multiple Java threads to pause and resume at a user#39;s request?(如何让多个 Java 线程根据用户的请求暂停和恢复?)

本文介绍了如何让多个 Java 线程根据用户的请求暂停和恢复?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我正在创建一个 20 分钟倒计时应用程序.我正在使用 JavaFX SceneBuilder 来执行此操作.计时器由两个标签组成(一个代表分钟,一个代表秒——每个都由一个 CountdownTimer 类对象组成)和一个进度条(计时器看起来像 this).这些组件中的每一个都是独立的,并同时在不同的线程上运行,以防止 UI 冻结.它有效.

I'm creating a 20-minute countdown timer application. I'm using JavaFX SceneBuilder to do this. The timer is composed of two labels (one for minutes, one for seconds--each composed of a CountdownTimer class object), and a progress bar (the timer looks like this). Each of these components are separate and running on separate threads concurrently to prevent the UI from freezing up. And it works.

问题:

我需要能够暂停和恢复的三个线程(minutesThreadsecondsThreadprogressBarUpdaterThread)是常规的 .java 类.当用户单击播放(开始)按钮时,单击会向 FXMLDocumentController(控制 UI 中的组件如何更新的类)方法 startTimer() 发出信号关于计时器的工作.

The three threads (minutesThread, secondsThread, progressBarUpdaterThread) I need to be able to pause and resume are regular .java classes. When the user clicks the play (start) button, the click signals the FXMLDocumentController (the class that controls how the components in the UI are updated) method startTimer() to do work regarding the timer.

目前FXMLDocumentControllerstartTimer()的唯一功能是:用户点击播放(开始)按钮-->计时器开始倒计时.

Right now the only functionality startTimer() in FXMLDocumentController has is: user clicks play (start) button --> timer begins counting down.

我希望用户能够使用同一个按钮暂停和恢复计时器.我尝试在 FXMLDocumentController 类和其他三个线程之间使用同步,但以多种不同的方式无济于事(诚然,我几乎没有并发编码的经验).我只是希望能够暂停和播放计时器!

I want the user to be able to pause and resume the timer with this same button. I've tried using synchronization across the FXMLDocumentController class and the other three threads to no avail in multiple different ways (admittedly, I have almost no experience coding for concurrency). I just want to be able to pause and play the timer!

谁能给我建议如何解决这个问题?提前致谢.

Can anyone offer me advice in how to go about this? Thanks in advance.

FXMLDocumentController.java 中的startTimer()(用于启动倒计时):

@FXML
void startTimer(MouseEvent event) throws FileNotFoundException {
    // update click count so user can switch between pause and start
    startTimerButtonClickCount++;

    // create a pause button image to replace the start button image when the user pauses the timer
    Image pauseTimerButtonImage = new Image(new 
         FileInputStream("/Users/Home/NetBeansProjects/Take20/src/Images/pause2_black_18dp.png"));
    // setting imageview to be used when user clicks on start button to pause it
    ImageView pauseTimerButtonImageView = new ImageView(pauseTimerButtonImage);
    // setting the width and height of the pause image
    pauseTimerButtonImageView.setFitHeight(31);
    pauseTimerButtonImageView.setFitWidth(28);
    // preserving the pause image ratio after resize
    pauseTimerButtonImageView.setPreserveRatio(true);

    // create a start button image to replace the pause button image when the user unpauses the timer
    Image startTimerButtonImage = new Image(new 
          FileInputStream("/Users/Home/NetBeansProjects/
          Take20/src/Images/play_arrow2_black_18dp.png"));
    ImageView startTimerButtonImageView = new ImageView(startTimerButtonImage);
    startTimerButtonImageView.setFitHeight(31);
    startTimerButtonImageView.setFitWidth(28);
    startTimerButtonImageView.setPreserveRatio(true);

    // progressBar updater
    ProgressBarUpdater progressBarUpdater = new ProgressBarUpdater();
    TimerThread progressBarThread = new TimerThread(progressBarUpdater);
    // minutes timer
    CountdownTimer minutesTimer = new CountdownTimer(19);
    TimerThread minutesThread = new TimerThread(minutesTimer);
    // seconds timer
    CountdownTimer secondsTimer = new CountdownTimer(59);
    TimerThread secondsThread = new TimerThread(secondsTimer);

    // bind our components in order to update them
    progressBar.progressProperty().bind(progressBarUpdater.progressProperty());
    minutesTimerLabel.textProperty().bind(minutesTimer.messageProperty());
    secondsTimerLabel.textProperty().bind(secondsTimer.messageProperty());

    // start the threads in order to have them run parallel when the start button is clicked
    progressBarThread.start();
    minutesThread.start();
    secondsThread.start();

    // if the start button was clicked, then we set its graphic to the pause image
    // if the button click count is divisible by 2, we pause it, otherwise, we play it (and change 
    // the button images accordingly).
    if (startTimerButtonClickCount % 2 == 0) {
        startTimerButton.setGraphic(pauseTimerButtonImageView);
        progressBarThread.pauseThread();
        minutesThread.pauseThread();
        secondsThread.pauseThread();

        progressBarThread.run();
        minutesThread.run();
        secondsThread.run();
    } else {
        startTimerButton.setGraphic(startTimerButtonImageView);
        progressBarThread.resumeThread();
        minutesThread.resumeThread();
        secondsThread.resumeThread();

        progressBarThread.run();
        minutesThread.run();
        secondsThread.run();
    }
}

TimerThread(用于在用户单击 UI 中的播放/暂停按钮时暂停/恢复计时器线程):

public class TimerThread extends Thread implements Runnable {

public boolean paused = false;
public final Task<Integer> timerObject;
public final Thread thread;

public TimerThread(Task timerObject) {
    this.timerObject = timerObject;
    this.thread = new Thread(timerObject);
}

@Override
public void start() {
    this.thread.start();
    System.out.println("TimerThread started");
}

@Override
public void run() {
    System.out.println("TimerThread class run() called");
    try {
        synchronized (this.thread) {
            System.out.println("synchronized called");
            while (paused) {
                System.out.println("wait called");
                this.thread.wait();
                System.out.println("waiting...");
            }
        }
    } catch (Exception e) {
        System.out.println("exception caught in TimerThread");
    }

}

synchronized void pauseThread() {
    paused = true;
}

synchronized void resumeThread() {
    paused = false;
    notify();
}
}

CountdownTimer.java(用于创建和更新倒计时的分秒):

public class CountdownTimer extends Task<Integer> {

private int time;
private Timer timer;
private int timerDelay;
private int timerPeriod;
private int repetitions;

public CountdownTimer(int time) {
    this.time = time;
    this.timer = new Timer();
    this.repetitions = 1;
}

@Override
protected Integer call() throws Exception {
    // we will create a new thread for each time unit (minutes, seconds)
    // we start with whatever time is passed to the constructor
    // we have threads devoted to each case so both minutes and second cases can run parallel to each other.
    switch (time) {
        // for our minutes timer
        case 19:
            // first display should be 19 first since our starting timer time should be 19:59 
            updateMessage("19");

            // set delay and period to change every minute of the countdown
            // 60,000 milliseconds in one minute
            timerDelay = 60000;
            timerPeriod = 60000;
            System.out.println("Running minutesthread....");

            // use a timertask to loop through time at a fixed rate as set by timerDelay, until the timer reaches 0 and is cancelled
            timer.scheduleAtFixedRate(new TimerTask() {
                @Override
                public void run() {
                    //check if the flag is divisible by 2, then we sleep this thread 

                    // if time reaches 0, we want to update the minute label to 00
                    if (time == 0) {
                        updateMessage("0" + Integer.toString(time));
                        timer.cancel();
                        timer.purge();
                        // if the time is a single digit, append a 0 and reduce time by 1
                    } else if (time <= 10) {
                        --time;
                        updateMessage("0" + Integer.toString(time));
                        // otherwise, we we default to reducing time by 1, every minute
                    } else {
                        --time;
                        updateMessage(Integer.toString(time));
                    }
                }
            }, timerDelay, timerPeriod);
            // exit switch statement once we finish our work 
            break;

        // for our seconds timer
        case 59:
            // first display 59 first since our starting timer time should be 19:59 
            updateMessage("59");
            // use a counter to count repetitions so we can cancel the timer when it arrives at 0, after 20 repetitions

            // set delay and period to change every second of the countdown
            // 1000 milliseconds in one second
            timerDelay = 1000;
            timerPeriod = 1000;
            System.out.println("Running seconds thread....");

            // use a timertask to loop through time at a fixed rate as set by timerDelay, until the timer reaches 0 and is cancelled
            timer.scheduleAtFixedRate(new TimerTask() {
                @Override
                public void run() {
                    --time;
                    System.out.println("repititions: " + repetitions);
                    // Use a counter to count repetitions so we can cancel the timer when it arrives at 0, after 1200 repetitions
                    // We will reach 1200 repetitions at the same time as the time variable reaches 0, since the timer 
                    // loops/counts down every second (1000ms).
                    // 1200 seconds = 20 minutes * 60 seconds (1 minute)
                    repetitions++;

                    if (time == 0) {
                        if (repetitions == 1200) {
                            // reset repetitions if user decides to click play again
                            repetitions = 0;
                            timer.cancel();
                            System.out.println("repetitions ran");
                        }
                        updateMessage("0" + Integer.toString(time));
                        // reset timer to 60, so it will countdown again from 60 after reaching 0 (since we have to repeat the seconds timer multiple times,
                        // unlike the minutes timer, which only needs to run once
                        time = 60;
                        System.out.println("time == 00 ran");
                    } else if (time < 10 && time > 0) {
                        updateMessage("0" + Integer.toString(time));
                    } else {
                        updateMessage(Integer.toString(time));
                    }
                }
            }, timerDelay, timerPeriod);
            // exit switch statement once we finish our work
            break;
    }

    return null;
}
}

ProgressBarUpdater.java(用于在倒数计时器倒计时时更新进度条):

public class ProgressBarUpdater extends Task<Integer> {

private int progressBarPeriod;
private Timer timer;
private double time;

public ProgressBarUpdater() {
    this.timer = new Timer();
    this.time = 1200000;
}

@Override
protected Integer call() throws Exception {
    progressBarPeriod = 10;
    System.out.println("Running progressBar thread....");

    // using a timer task, we update our progressBar by reducing the filled progressBar every 9.68 milliseconds 
    // (instead of 10s to account for any delay in program runtime) to ensure that the progressBar ends at the same time our timer reaches 0. 
    // according to its max (1200000ms or 20 minutes)
    timer.scheduleAtFixedRate(new TimerTask() {
        @Override
        public void run() {
            time -= 9.68;
            updateProgress(time, 1200000);
            System.out.println("progressBarUpdater is running");
        }
    }, 0, progressBarPeriod);

    return null;
}

@Override
protected void updateProgress(double workDone, double maxTime) {
    super.updateProgress(workDone, maxTime);
}

}

推荐答案

正如我在评论中提到的,为此使用后台线程,更不用说 三个(!) 个后台线程,只会使这更难实施和推理.最好使用 动画 API 由 JavaFX 提供——它是异步的,但仍然在 JavaFX 应用程序线程上执行.正如其他人所提到的,您只需要一个值来表示剩余时间,另一个值表示持续时间.从那里您可以显示分钟、秒和进度.

As I mentioned in a comment, using a background thread for this, let alone three(!) background threads, will only make this harder to implement and reason about. It would be better to use the animation API provided by JavaFX—it's asynchronous but still executes on the JavaFX Application Thread. And as mentioned by others, you only need one value to represent the time remaining and another value representing the duration. From there you can display the minutes, seconds, and progress.

就个人而言,我会使用 AnimationTimer 因为它为您提供了当前帧的时间戳,您可以使用它来计算还剩多少时间.为了使事情更容易使用,我还将 AnimationTimer 包装在另一个类中,并让后一个类公开一个更适合倒数计时器的 API.例如:

Personally, I would use an AnimationTimer as it gives you the timestamp of the current frame which you can use to calculate how much time is left. To make things easier to use I would also wrap the AnimationTimer in another class and have that latter class expose an API more appropriate for countdown timers. For example:

package com.example;

import java.util.concurrent.TimeUnit;
import javafx.animation.AnimationTimer;
import javafx.beans.property.LongProperty;
import javafx.beans.property.ReadOnlyDoubleProperty;
import javafx.beans.property.ReadOnlyDoubleWrapper;
import javafx.beans.property.ReadOnlyLongProperty;
import javafx.beans.property.ReadOnlyLongWrapper;
import javafx.beans.property.ReadOnlyObjectProperty;
import javafx.beans.property.ReadOnlyObjectWrapper;
import javafx.beans.property.SimpleLongProperty;

public class CountdownTimer {

  private static long toMillis(long nanos) {
    return TimeUnit.NANOSECONDS.toMillis(nanos);
  }

  /* *********************************************************************
   *                                                                     *
   * Instance Fields                                                     *
   *                                                                     *
   ***********************************************************************/

  private final Timer timer = new Timer();
  private long cachedDuration;

  /* *********************************************************************
   *                                                                     *
   * Constructors                                                        *
   *                                                                     *
   ***********************************************************************/

  public CountdownTimer() {}

  public CountdownTimer(long duration) {
    setDuration(duration);
  }

  /* *********************************************************************
   *                                                                     *
   * Public API                                                          *
   *                                                                     *
   ***********************************************************************/

  public void start() {
    if (getStatus() == Status.READY || getStatus() == Status.PAUSED) {
      timer.start();
      setStatus(Status.RUNNING);
    }
  }

  public void pause() {
    if (getStatus() == Status.RUNNING) {
      timer.pause();
      setStatus(Status.PAUSED);
    }
  }

  public void stopAndReset() {
    timer.stopAndReset();
    setStatus(Status.READY);
  }

  /* *********************************************************************
   *                                                                     *
   * Properties                                                          *
   *                                                                     *
   ***********************************************************************/

  private final ReadOnlyObjectWrapper<Status> status = new ReadOnlyObjectWrapper<>(this, "status", Status.READY) {
    @Override protected void invalidated() {
      if (get() == Status.READY) {
        cachedDuration = Math.abs(getDuration());
        setTimeRemaining(cachedDuration);
      }
    }
  };
  private void setStatus(Status status) { this.status.set(status); }
  public final Status getStatus() { return status.get(); }
  public final ReadOnlyObjectProperty<Status> statusProperty() { return status.getReadOnlyProperty(); }

  private final LongProperty duration = new SimpleLongProperty(this, "duration") {
    @Override protected void invalidated() {
      if (getStatus() == Status.READY) {
        cachedDuration = Math.abs(get());
        setTimeRemaining(cachedDuration);
      }
    }
  };
  public final void setDuration(long duration) { this.duration.set(duration); }
  public final long getDuration() { return duration.get(); }
  public final LongProperty durationProperty() { return duration; }

  private final ReadOnlyLongWrapper timeRemaining = new ReadOnlyLongWrapper(this, "timeRemaining") {
    @Override protected void invalidated() {
      setProgress((double) (cachedDuration - get()) / (double) cachedDuration);
    }
  };
  private void setTimeRemaining(long timeRemaining) { this.timeRemaining.set(timeRemaining); }
  public final long getTimeRemaining() { return timeRemaining.get(); }
  public final ReadOnlyLongProperty timeRemainingProperty() { return timeRemaining.getReadOnlyProperty(); }

  private final ReadOnlyDoubleWrapper progress = new ReadOnlyDoubleWrapper(this, "progress");
  private void setProgress(double progress) { this.progress.set(progress); }
  public final double getProgress() { return progress.get(); }
  public final ReadOnlyDoubleProperty progressProperty() { return progress.getReadOnlyProperty(); }

  /* *********************************************************************
   *                                                                     *
   * Static Classes                                                      *
   *                                                                     *
   ***********************************************************************/

  public enum Status {
    READY,
    RUNNING,
    PAUSED,
    FINISHED
  }

  /* *********************************************************************
   *                                                                     *
   * Classes                                                             *
   *                                                                     *
   ***********************************************************************/

  private class Timer extends AnimationTimer {

    private long triggerTime = Long.MIN_VALUE;
    private long pauseTime = Long.MIN_VALUE;
    private boolean pausing;

    @Override
    public void handle(long now) {
      if (pausing) {
        pauseTime = toMillis(now);
        pausing = false;
        stop();
      } else {
        if (triggerTime == Long.MIN_VALUE) {
          triggerTime = toMillis(now) + cachedDuration;
        } else if (pauseTime != Long.MIN_VALUE) {
          triggerTime += toMillis(now) - pauseTime;
          pauseTime = Long.MIN_VALUE;
        }

        long timeRemaining = Math.max(0, triggerTime - toMillis(now));
        setTimeRemaining(timeRemaining);
        if (timeRemaining == 0) {
          setStatus(Status.FINISHED);
          stop();
        }
      }
    }

    @Override
    public void start() {
      pausing = false;
      super.start();
    }

    void pause() {
      if (triggerTime != Long.MIN_VALUE) {
        pausing = true;
      } else {
        stop();
      }
    }

    void stopAndReset() {
      stop();
      triggerTime = Long.MIN_VALUE;
      pauseTime = Long.MIN_VALUE;
      pausing = false;
    }
  }
}

警告:AnimationTimer 运行时,CountdownTimer 实例不能被垃圾回收.

Warning: While the AnimationTimer is running the CountdownTimer instance cannot be garbage collected.

此实现将持续时间和剩余时间值都解释为毫秒.此外,在启动计时器后更改持续时间直到计时器重置(即调用 stopAndReset())后才会生效.

This implementation interprets both the duration and time remaining values as milliseconds. Also, changing the duration after starting the timer has no effect until after the timer is reset (i.e. calling stopAndReset()).

这是在基于 FXML 的应用程序中使用上述 CountdownTimer 的示例.请注意,该示例使用不同的按钮来启动、暂停、恢复和重置计时器.这与您在问题中所描述的不同,但您应该能够根据您的需要进行修改.此外,该示例还提供了一种方法来切换是否显示当前秒的毫秒.

Here's an example of using the above CountdownTimer in an FXML-based application. Note that the example uses distinct buttons for starting, pausing, resuming, and resetting the timer. This is different than what you described in your question but you should be able to rework things to fit your needs. Also, the example provides a way to toggle whether or not the millisecond of the current second is shown.

App.fxml:

<?xml version="1.0" encoding="UTF-8"?>

<?import javafx.scene.control.Label?>
<?import javafx.scene.control.ProgressBar?>
<?import javafx.scene.control.ToolBar?>
<?import javafx.scene.layout.StackPane?>
<?import javafx.scene.layout.VBox?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.control.Separator?>
<?import javafx.scene.control.CheckBox?>
<?import com.example.CountdownTimer?>
<?import com.example.CountdownTimer.Status?>

<VBox xmlns="http://javafx.com/javafx/14.0.1" xmlns:fx="http://javafx.com/fxml/1"
      fx:controller="com.example.Controller" prefHeight="300" prefWidth="500">

  <fx:define>
    <!-- 90,000ms == 1m 30s -->
    <CountdownTimer fx:id="timer" duration="90000"/>
    <CountdownTimer.Status fx:id="READY" fx:value="READY"/>
    <CountdownTimer.Status fx:id="RUNNING" fx:value="RUNNING"/>
    <CountdownTimer.Status fx:id="PAUSED" fx:value="PAUSED"/>
  </fx:define>

  <ToolBar style="-fx-font: 10pt 'Monospaced';">
    <Button text="Start" disable="${timer.status != READY}" focusTraversable="false"
            onAction="#handleStartOrResumeTimer"/>
    <Button text="Resume" disable="${timer.status != PAUSED}" focusTraversable="false"
            onAction="#handleStartOrResumeTimer"/>
    <Button text="Pause" disable="${timer.status != RUNNING}" focusTraversable="false" onAction="#handlePauseTimer"/>
    <Button text="Reset" disable="${timer.status == READY || timer.status == RUNNING}" focusTraversable="false"
            onAction="#handleResetTimer"/>
    <Separator/>
    <CheckBox fx:id="showMillisBox" text="Show Millis" focusTraversable="false"/>
  </ToolBar>

  <ProgressBar progress="${timer.progress}" maxWidth="Infinity"/>

  <StackPane VBox.vgrow="ALWAYS">
    <Label fx:id="timerLabel" style="-fx-font: bold 48pt 'Monospaced';"/>
  </StackPane>
</VBox>

Controller.java:

Controller.java:

package com.example;

import java.time.Duration;
import javafx.beans.binding.Bindings;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.scene.control.CheckBox;
import javafx.scene.control.Label;
import javafx.scene.paint.Color;

public class Controller {

  @FXML private CountdownTimer timer;
  @FXML private CheckBox showMillisBox;
  @FXML private Label timerLabel;

  @FXML
  private void initialize() {
    timerLabel
        .textProperty()
        .bind(
            Bindings.createStringBinding(
                this::formatTimeRemaining,
                timer.timeRemainingProperty(),
                showMillisBox.selectedProperty()));
    timerLabel
        .textFillProperty()
        .bind(
            Bindings.when(timer.statusProperty().isEqualTo(CountdownTimer.Status.FINISHED))
                .then(Color.FIREBRICK)
                .otherwise(Color.FORESTGREEN));
  }

  private String formatTimeRemaining() {
    Duration d = Duration.ofMillis(timer.getTimeRemaining());
    if (showMillisBox.isSelected()) {
      return String.format("%02d:%02d:%03d", d.toMinutes(), d.toSecondsPart(), d.toMillisPart());
    }
    return String.format("%02d:%02d", d.toMinutes(), d.toSecondsPart());
  }

  @FXML
  private void handleStartOrResumeTimer(ActionEvent event) {
    event.consume();
    timer.start();
  }

  @FXML
  private void handlePauseTimer(ActionEvent event) {
    event.consume();
    timer.pause();
  }

  @FXML
  private void handleResetTimer(ActionEvent event) {
    event.consume();
    timer.stopAndReset();
  }
}

Main.java:

package com.example;

import java.io.IOException;
import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.stage.Stage;

public class Main extends Application {

  @Override
  public void start(Stage primaryStage) throws IOException {
    Parent root = FXMLLoader.load(getClass().getResource("/com/example/App.fxml"));
    primaryStage.setScene(new Scene(root));
    primaryStage.setTitle("Countdown Timer Example");
    primaryStage.show();
  }
}

这篇关于如何让多个 Java 线程根据用户的请求暂停和恢复?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持编程学习网!

本文标题为:如何让多个 Java 线程根据用户的请求暂停和恢复?

基础教程推荐