2016-04-13 3 views
2

Я стараюсь часто обновлять ячейки в JFX TableView (доказательство концептуального приложения). Я загружаю TableView через FXML и запускаю ExecutorService для изменения значения ячейки.JavaFX TableView с частыми обновлениями

Когда я запускаю приложение, я замечаю, что обновление работает для первых 3-4 миллионов элементов, а затем оно запускается. Если я замедляю обновления (см. MAGIC# 1), он работает (10 мс все еще слишком быстро, но работает задержка 100 мс). Поэтому я подумал, что это может быть проблема с потоками.

Но потом я узнал, что если я добавлю пустой ChangeListener (см. MAGIC# 2) в свойство, он отлично работает. Даже без необходимости MAGIC# 1.

Я что-то не так? Нужно ли мне обновлять ячейки по-другому?

Заранее благодарим за помощь!

Элементы в TableView:

public class Element { 
    public static final AtomicInteger x = new AtomicInteger(0); 
    private final StringProperty nameProperty = new SimpleStringProperty("INIT"); 

    public Element() { 
    // MAGIC#2 
    // this.nameProperty.addListener((observable, oldValue, newValue) -> {}); 
    } 

    public void tick() { 
    this.setName(String.valueOf(x.incrementAndGet())); 
    } 

    public String getName() ... 
    public void setName(String name)... 
    public StringProperty nameProperty() ... 
} 

Контроллер для FXML:

public class TablePerformanceController implements Initializable { 
    private final ObservableList<Element> data = FXCollections.observableArrayList(); 

    public Runnable changeValues =() -> { 
    while (true) { 
     if (Thread.currentThread().isInterrupted()) break; 
     data.get(0).tick(); 
     // MAGIC#1 
     // try { Thread.sleep(100); } catch (Exception e) {} 
    } 
    }; 

    private ExecutorService executor = null; 

    @FXML 
    public TableView<Element> table; 

    @Override 
    public void initialize(URL location, ResourceBundle resources) { 
    this.table.setEditable(true); 

    TableColumn<Element, String> nameCol = new TableColumn<>("Name"); 
    nameCol.setCellValueFactory(cell -> cell.getValue().nameProperty()); 
    this.table.getColumns().addAll(nameCol); 

    this.data.add(new Element()); 
    this.table.setItems(this.data); 

    this.executor = Executors.newSingleThreadExecutor(); 
    this.executor.submit(this.changeValues); 
    } 
} 

ответ

1

Вы нарушаете однопоточный правило JavaFX: обновления в пользовательском интерфейсе должны быть сделаны только из FX Тема приложения. Ваш метод tick() обновляет nameProperty(), и поскольку ячейка таблицы наблюдает за nameProperty(), tick() приводит к обновлению пользовательского интерфейса. Поскольку вы вызываете tick() из фонового потока, это обновление для пользовательского интерфейса происходит в фоновом потоке. Полученное поведение по существу не определено.

Кроме того, ваш код заканчивается слишком большим количеством запросов на обновление пользовательского интерфейса. Поэтому, даже если вы исправляете проблемы с потоками, вам необходимо каким-то образом отключить запросы, чтобы вы не заливали поток приложений FX слишком большим количеством запросов на обновление, что сделает его невосприимчивым.

Методика для этого адресована в Throttling javafx gui updates. Я буду повторять его здесь в контексте класса таблицы модели:

import java.util.concurrent.atomic.AtomicInteger; 
import java.util.concurrent.atomic.AtomicReference; 

import javafx.application.Platform; 
import javafx.beans.property.SimpleStringProperty; 
import javafx.beans.property.StringProperty; 

public class Element { 

    // Note that in the example we only actually reference this from a single background thread, 
    // in which case we could just make this a regular int. However, for general use this might 
    // need to be threadsafe. 
    private final AtomicInteger x = new AtomicInteger(0); 

    private final StringProperty nameProperty = new SimpleStringProperty("INIT"); 

    private final AtomicReference<String> name = new AtomicReference<>(); 


    /** This method is safe to call from any thread. */ 
    public void tick() { 
     if (name.getAndSet(Integer.toString(x.incrementAndGet())) == null) { 
      Platform.runLater(() -> nameProperty.set(name.getAndSet(null))); 
     } 
    } 

    public String getName() { 
     return nameProperty().get(); 
    } 

    public void setName(String name) { 
     nameProperty().set(name); 
    } 

    public StringProperty nameProperty() { 
     return nameProperty; 
    } 
} 

Основная идея здесь заключается в использовании AtomicReference<String в «тень» реальную собственность. Атомно обновляйте его и проверьте, является ли оно null, и если да, то заплатите обновление для реального свойства в приложении приложения FX. В обновлении выполните атомарное извлечение значения «shadow» и сбросьте его до нуля и установите реальное свойство в полученное значение. Это гарантирует, что новые запросы на обновление в потоке приложения FX будут выполняться так же часто, как поток приложений FX потребляет их, гарантируя, что поток приложения FX не будет затоплен. Конечно, если есть задержка между планированием обновления в потоке приложения FX и фактическим обновлением, когда обновление произойдет, оно все равно получит последнее значение, для которого было установлено значение «shadow».

Вот автономный тест, который в основном эквивалентен коду контроллера вы показали:

import java.util.concurrent.ExecutorService; 
import java.util.concurrent.Executors; 

import javafx.application.Application; 
import javafx.collections.FXCollections; 
import javafx.collections.ObservableList; 
import javafx.scene.Scene; 
import javafx.scene.control.TableColumn; 
import javafx.scene.control.TableView; 
import javafx.stage.Stage; 

public class FastTableUpdate extends Application { 

    private final ObservableList<Element> data = FXCollections.observableArrayList(); 

    public final Runnable changeValues =() -> { 
     while (true) { 
     if (Thread.currentThread().isInterrupted()) break; 
     data.get(0).tick(); 
     } 
    }; 

    private final ExecutorService executor = Executors.newSingleThreadExecutor(runnable -> { 
     Thread t = new Thread(runnable); 
     t.setDaemon(true); 
     return t ; 
    }); 



    @Override 
    public void start(Stage primaryStage) { 

     TableView<Element> table = new TableView<>(); 
     table.setEditable(true); 

     TableColumn<Element, String> nameCol = new TableColumn<>("Name"); 
     nameCol.setPrefWidth(200); 
     nameCol.setCellValueFactory(cell -> cell.getValue().nameProperty()); 
     table.getColumns().add(nameCol); 

     this.data.add(new Element()); 
     table.setItems(this.data); 

     this.executor.submit(this.changeValues);   

     Scene scene = new Scene(table, 600, 600); 
     primaryStage.setScene(scene); 
     primaryStage.show(); 
    } 

    public static void main(String[] args) { 
     launch(args); 
    } 
} 
+0

Большое спасибо. Это решает мою проблему :-) –