I need to bind a variable that changes from my thread to Property from JavaFx. In theory, this should allow me to bind my player to the controls from JavaFx. I even wrote a wrapper for Property, but I still have the feeling that something is wrong, maybe I reinvent the wheel. It’s impossible to set / get the Property value from a JavaFx stream, and Platform.runLater / ChangeEvent should be used for this, but in this case the following problems arise:

  • Echo. When old values ​​come through the listener again.
  • Out of sync. When in one place one value, and in another another. Occurs when parallel values ​​change.
  • Overwrite values. When we take a value from Property, we check it, we think that it has not changed and we write there a new one, and in fact we overwrite the changed value.

Wrapper:

package javafxapplication1; import java.util.Objects; import java.util.concurrent.CountDownLatch; import java.util.function.BiConsumer; import java.util.logging.Level; import java.util.logging.Logger; import javafx.application.Platform; import javafx.beans.property.Property; class FxPropertyAdapter<T> { private static final Logger LOG = Logger.getLogger(FxPropertyAdapter.class.getName()); private volatile T value; private final Property<T> property; private boolean listenerEnabled = true; private final CountDownLatch initializedLatch = new CountDownLatch(1); private volatile Runnable onInitializeHandler = null; private volatile BiConsumer<T, T> onChangeHandler = null; public FxPropertyAdapter(Property<T> property) { Objects.requireNonNull(property); this.property = property; Platform.runLater(() -> { value = property.getValue(); property.addListener((observable, oldValue, newValue) -> { if (listenerEnabled) { synchronized (this) { updateValue(newValue); } } }); synchronized (this) { initializedLatch.countDown(); if (onInitializeHandler != null) { try { onInitializeHandler.run(); } catch (Exception e) { LOG.log(Level.WARNING, "Error in initializeHandler.", e); } } } }); } public Runnable getOnInitializeHandler() { return onInitializeHandler; } public synchronized void setOnInitializeHandler(Runnable onInitializeHandler) { this.onInitializeHandler = onInitializeHandler; if (isInitialized() && this.onInitializeHandler != null) { try { this.onInitializeHandler.run(); } catch (Exception e) { LOG.log(Level.WARNING, "Error in onInitializeHandler.", e); } } } public BiConsumer<T, T> getOnChangeHandler() { return onChangeHandler; } public void setOnChangeHandler(BiConsumer<T, T> onChangeHandler) { this.onChangeHandler = onChangeHandler; } public Property<T> getProperty() { return property; } /** * Позволяет не упустить обновления, произведённые в JavaFx. Если JavaFx * успеет обновить значение, то мы увидим несоответствие, если запланирует * обновление, тогда то значение, которое мы сюда передали, будет затёрто * начатым обновлением из JavaFx. Но местный обработчик изменения все же * сработает. * * @param expect * @param update * @return */ public synchronized boolean compareAndSet(T expect, T update) { if (expect == getValue()) { setValue(update); return true; } else { return false; } } public T getValue() { requireInitialized(); return value; } /** * При установлении значения приоритет всегда на стороне JavaFx. Если * параллельно менять значения в JavaFx и в другом потоке, то из этого * потока значения не будут успевать попадать в постоянно меняющийся * Property. Однако, они будут попадать в onChangeHandler данного класса. * * С другой стороны, у нас есть доступ к самому Property и мы можем * установить ему значение через Platform.runLater. * * @param value */ public synchronized void setValue(T value) { requireInitialized(); if (this.value == value) { return; } T oldValue = this.value; updateValue(value); Platform.runLater(() -> { listenerEnabled = false; /** * Если JavaFx обновит значение до этого метода, то мы просто * обновим значение в обоих потоках. * * Если JavaFx начнёт обновлять значение до вызова метода, но не * успеет захватить доступ к объекту, то проверка спасает от разных * значений. В противном случае было бы запланировано изменение * Property без слушателя, а далее отработал бы ожидающий JavaFx * поток, который бы задал переменной старое значение, а это уже * рассинхронизация. * * Синхронизация нужная, чтобы текущий поток не затёр значение из * JavaFx вызовом updateValue(value), без синхронизации JavaFx может * пролезть между проверкой старого значения и обновлением значения, * в итоге JavaFx значение затрётся, что приведёт к разным * значениям. */ if (property.getValue().equals(oldValue)) { property.setValue(value); } listenerEnabled = true; }); } public boolean isInitialized() { return initializedLatch.getCount() == 0; } private void updateValue(T value) { T oldValue = this.value; this.value = value; if (isInitialized()) { try { onChangeHandler.accept(oldValue, value); } catch (Exception e) { LOG.log(Level.WARNING, "Error in onChangeHandler.", e); } } } private void requireInitialized() { if (!isInitialized()) { throw new RuntimeException("FxPropertyAdapter isn't initialized yet."); } } } 

Example:

 package javafxapplication1; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.logging.Level; import java.util.logging.Logger; import javafx.animation.KeyFrame; import javafx.animation.KeyValue; import javafx.animation.Timeline; import javafx.application.Application; import javafx.beans.property.SimpleFloatProperty; import javafx.scene.Scene; import javafx.scene.control.ProgressBar; import javafx.scene.layout.StackPane; import javafx.stage.Stage; import javafx.util.Duration; public class JavaFXApplication extends Application { private static final Logger LOG = Logger.getLogger(JavaFXApplication.class.getName()); @Override public void start(Stage primaryStage) { ProgressBar pb = new ProgressBar(); StackPane root = new StackPane(); root.getChildren().add(pb); Scene scene = new Scene(root, 300, 250); primaryStage.setTitle("Test"); primaryStage.setScene(scene); primaryStage.show(); Thread t1 = new Thread(() -> { SimpleFloatProperty slp = new SimpleFloatProperty(); FxPropertyAdapter<Number> fpa = new FxPropertyAdapter<>(slp); fpa.setOnChangeHandler((from, to) -> { System.out.println(String.format("Value have been changed from %s to %s.", from, to)); }); CountDownLatch initializedLatch = new CountDownLatch(1); fpa.setOnInitializeHandler(() -> { pb.progressProperty().bindBidirectional(slp); initializedLatch.countDown(); }); try { initializedLatch.await(); } catch (InterruptedException ex) { LOG.log(Level.SEVERE, null, ex); } int sign = +1; for (float i = .0f; i >= 0; i += .01f * sign) { if (i > 1) { sign = -sign; } fpa.setValue(i); try { TimeUnit.MILLISECONDS.sleep(100 - (int) (i * 100)); } catch (InterruptedException ex) { LOG.log(Level.SEVERE, null, ex); return; } } Timeline timeline = new Timeline(new KeyFrame(Duration.millis(5000), new KeyValue(slp, 1))); timeline.setCycleCount(2); timeline.setAutoReverse(true); timeline.play(); }); t1.setDaemon(true); t1.start(); } public static void main(String[] args) { launch(args); } } 
  • Don't quite understand what you want to do. you describe how you are doing, but not what you want to receive. - Mikhail Vaysman
  • What I wanted, I got it, but there are doubts about the decision. A common problem, it’s strange that I didn’t find a ready run-in solution, maybe I missed it or there is some other approach in solving this task. Although perhaps this is paranoia. The logic is not trivial, this component will form the foundation of the code, I'm afraid to get deadlocks in an already large amount of code in the future. - Dima G.
  • I did not understand what you want to do. for this I can not say correctly whether you decide or not. - Mikhail Vaysman
  • And so? I want in Thread to read and update the value of the timestamp variable without synchronization delays, for example, do not wait for JavaFxThread. Changes made to Thread should be passed to the JavaFx slider. But at the same time, if the slider is shifted by the user, then the variable should change. - Dima G.
  • At the beginning of the cycle, I want to save the current value of the variable to the local variable localTimestamp, then the video is played back, then again we return to the beginning of the cycle, if localTimestamp == timestamp, then we replace the timestamp with a new value, if not, we start playback from another location specified in timestamp. - Dima G.

0