java线程暂停和继续,如何根据用户的请求使多个Java线程暂停和恢复?

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.

The problem:

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.

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

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.

startTimer() in FXMLDocumentController.java (used to start the countdown timer):

@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 (used to suspend/resume timer threads when user clicks the play/pause button in the UI):

public class TimerThread extends Thread implements Runnable {

public boolean paused = false;

public final Task 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 (used to create and update the minutes and seconds of the countdown timer):

public class CountdownTimer extends Task {

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 (used to update the progress bar as the countdown timer counts down):

public class ProgressBarUpdater extends Task {

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);

}

}

解决方案

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.

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 = 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 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;

}

}

}

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

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()).

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:

fx:controller="com.example.Controller" prefHeight="300" prefWidth="500">

onAction="#handleStartOrResumeTimer"/>

onAction="#handleStartOrResumeTimer"/>

onAction="#handleResetTimer"/>

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();

}

}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值