プロジェクト

全般

プロフィール

JavaFXとマルチスレッド

以前、JavaFXでブロックされるメソッドや処理に時間のかかるメソッドを呼ぶときは、バックグラウンドスレッドで実行することについて日記に記載しました。
http://d.hatena.ne.jp/torutk/20120408/p1

JavaFXのスレッド設計

JavaFXのシーングラフはスレッドセーフではないので、必ず「JavaFXアプリケーションスレッド」からアクセスするのが決まりです。
このようにデータが1つのスレッドからしかアクセスされないようにする設計は、スレッド拘束(thread confinement)と呼ばれます1

ただし、JavaFXアプリケーションスレッドで時間のかかる処理を行うと、ユーザーインタフェース(UI)が無応答になってしまいます。これを避けるには、バックグラウンドスレッドを作って時間のかかる処理を実行します。このバックグラウンドスレッドからはシーングラフにはアクセスできない決まりなので、スレッド間の通信が発生します。

1 書籍「Java並行処理プログラミング~その基盤と最新APIを究める」(ソフトバンク刊、Brian Goetzら著、2006年)

JavaFXのバックグラウンドスレッドAPI

JavaFXではバックグラウンドスレッドを提供する2つのAPI、ServiceとTaskがあります。

ServiceとTaskのクラス関係

               java.util.concurrent.
  Worker       FutureTask
  △ △           △
  : +---------+ |
  :            : |
 Service -----> Task

workerの状態遷移

     ●
     ↓      cancel
    READY ----------------+
     ↓      cancel       ↓
  SCHEDULED ---------> CANCELED
     ↓      cancel       ↑
   RUNNING ---------------+
     | +-----------+
     ↓             ↓
  SUCCEEDED       FAILED

TaskとService

Taskインスタンスはワンショット(使い捨て)
繰り返し使用したい場合はServiceを使います。

Taskの使用例

プログレスバーの更新をTaskから実施します。

Task task = new Task<Void>() {
    @Override
    public Void call() {
        static final int MAX = 1_000_000;
        for (int i = 1; i <= MAX; i++) {
            updateProgress(i, MAX);
        }
        return null;
    }
};

callメソッドはバックグラウンドスレッド上で実行するので、このメソッド内でシーングラフのオブジェクトにアクセスすると実行時例外が発生します。そこで、callメソッド内では自身のプロパティを変更する操作を行い、JavaFXのバインディングを介してシーングラフ側のオブジェクトに通知を行う手段が容易されています。Taskクラスでは、JavaFXアプリケーションスレッド側に働きかける次のメソッドが用意されています。

  • updateProgress
  • updateMessage
  • updateTitle

例えば、updateProgressを使うときは、働きかける先のProgressBarインスタンスのProgressPropertyとTaskインスタンスのProgressPropertyとをバインドします。

ProgressBar bar = new ProgressBar();
bar.progressProperty().bind(task.progressProperty());

Taskインスタンスをバックグラウンドスレッドで実行するには、自分でスレッドを生成しなければなりません。

Executor executor = Executors.newSingleThreadExecutor();
executor.execute(task);

Serviceの使用例

public class FirstLineSerivce extends Service {
    private StringProperty url = new StringProperty();
    public final void setUrl(String value) {
        url.set(value);
    }
    public final String getUrl() {
        return url.get();
    }
    public final StringProperty urlProperty() {
        return url;
    }
    protected Task createTask() {
        final String _url = getUrl();
        return new Task<InputStream>() {
            protected String call() {
                URL u = new URL(_url);
                BufferedReader in = new BufferedReader(
                        new InputSreamReader(u.openStream());
                String result = in.readLine();
                in.close();
                return result;
            }
        }
    }
}

Serviceを使うときは、サービス内で抽象メソッドcreateTaskを実装する必要があります。これは前述のTaskクラスを使うときの実装と同じです。

Serviceインスタンスをバックグラウンドスレッドで実行するには、Serviceクラスのメソッドrestartを呼びます。自身でスレッドを生成しなくてもよいのです。

ServiceまたはTaskの定義、生成、バインディング

Service(またはTask)を定義する場所についてですが、JavaFXのコントロールとのバインディング、バックグラウンドスレッド実行などでJavaFXと密接に関わりを持つので、JavaFXのコントローラクラス内にネストクラスで定義し、インスタンス化、バインディングの設定はJavaFXのコントローラクラスのinitializeメソッドに記述するのが一案です。

public class TcpExaminerController implements Initializable {
    :
    @FXML
    private Button acceptButton;
    @FXML
    private ProgressBar acceptProgressBar;
    @FXML
    private Text acceptStatusText;

    private AcceptService acceptService;

    static class AcceptService extends Service {
        :
    }
    :
    @Override
    public void initialize(URL url, ResourceBundle rb) {
        acceptService = new AcceptService(TcpServer.getInstance());
        acceptProgressBar.progressProperty().bind(acceptService.progressProperty());
        acceptProgressBar.visibleProperty().bind(acceptService.runningProperty());
        acceptButton.disableProperty().bind(acceptService.runningProperty());
        acceptStatusText.textProperty().bind(acceptService.messageProperty());
    }

クリップボードから画像を追加 (サイズの上限: 1 GB)