スレッドとSwing

翻訳元:Threads and Swing By Hans Muller and Kathy Walrath

これは、1998年にアーカイブされたSwingでのマルチスレッディングに関する記事です。その1ヶ月後、私たちはこのテーマ:"マルチスレッドはSwingでどのように動くのか?"の理解をさらに深めるため別の記事"Using a Swing Worker Thread"を執筆しました。私たちはあなたに両方の記事を読むことを進めます。

Note:私たちは2000年の11月にこの記事を変更しました。スレッドにかかわる些細なバグを修正した新しいバージョンのSwingWorkerの使い方を例示しています。

SwingAPIは簡単に、柔軟で、パワフルに使えるように設計されました。特に、私たちは、プログラマが新しいSwingコンポーネントをスクラッチから、もしくは、私たちが提供しているコンポーネントを拡張することで、簡単に作成できるようにする事を望んでいました。


この理由から、私たちは、Swingコンポーネントを複数のスレッドからのアクセスをサポートするようにしていません。その代わり、私たちはコンポーネントに対して、シングルスレッド上で動くリクエストを簡単に送信出来るようにしました。


この記事は、スレッドとSwing コンポーネントについての情報です。
その目的は、あなたがスレッドセーフな方法SwingAPIを使うことを助ける為だけではなく、私たちが、なぜスレッドに対してそのようなアプローチを行ったのかを説明しています。

この記事の目次:

シングルスレッドルール

これがルールですw:

一度Swingコンポーネントが実体化されると、コンポーネントの内部の状態に影響もしくは、依存しているコードは"event-dispatching thread"上で実行される必要があります。

このルールを恐ろしく感じるかも知れませんが、大多数の単純なプログラムでは, スレッドを気にする必要はありません。Swingでのプログラム作成方法の詳細に入る前に、次の2つを定義しておきます:実体化event-dispatching threadです。

実体化 は、コンポーネントpaint()が呼び出される事を意味しています。Swingの最上位のウィンドウコンポーネントはそのコンポーネントが持つ下記のメソッドを実行することで実体化されます:setVisible(true), show(), もしくは (これは驚くかもしれませんが) pack()。Windowコンポーネントが実体化されると、それに含まれるコンポーネントは全て実体化されます。コンポーネントを実体化する別の方法は、既に実体化されたコンポーネントをコンテナに追加することです。後ほどコンポーネントの実態化を例示します。

event-dispatching thread は描画の実行とイベントのハンドリングを行うスレッドです。例として、paint() actionPerformed()メソッドは"event-dispatching thread"が自動的に実行します。 別の方法として、SwingUtilitiesのinvokeLater()を使うことで、コードを"evnet-dispatching"スレッドで実行することができます。


このルールの例外

コンポーネントが実体化されると、影響を与える全てのコードが、"event-dispatching"スレッドで実行されなければならないというルールにはいくつかの例外が存在します。
  • いくつかのメソッドはスレッドセーフである:SwingのAPIドキュメントにおいて、スレッドセーフなメソッドは以下のようにマーキングされています:

    このメソッドはスレッドセーフですが、ほとんどの Swing メソッドは違います。

  • アプリケーションのGUIは、よく"main"スレッド上で構築されて表示されます:下記のほとんどのコードは、コンポーネント(Swingや他の)の実体化さえ行わなければほとんど安全です:

    public class MyApplication {
    public static void main(String[] args) {
    JFrame f = new JFrame("Labels");
    // ここでこのフレームにコンポーネントを追加
    f.pack();
    f.show();
    // GUIが動いているのでなにもしてはいけない

    }
    }

    上の全てのコードは"main"スレッドで動作します。f.pack()JFrame配下のコンポーネント郡を実体化します。これは、技術的な意味でf.show()の呼び出しが安全ではなく、"event-dispatching thread"から呼び出されなければならないことを現しています。しかしながら、まだプログラムが可視のGUIを持たない限り、JFrameもしくはそのコンテンツが、f.show()の呼び出しが終了するまでにpaint()呼び出しを受信することはないといって良いでしょう。なぜなら、全てのGUIの動作が"main"スレッドから"event-dispatching"スレッドへ移管されるf.show()呼び出しの後にGUIのコードが無いからです。そして、前述したコードは慣例的にもスレッドセーフです。

  • アプレットGUIinit()メソッドによって構築、表示されます。:現在のブラウザではinit()start()メソッドが呼ばれた後でないとアプレットを描画してくれません。従って、動作中のアプレットオブジェクトに対してshow()もしくはsetVisible(true)を呼び出さない限り、アプレットinit() メソッドによるGUIの構築は安全です。

    蛇足ですが、アプレットでSwingコンポーネントを使うにはJAppletのサブクラスを実装しなければなりません。そして、JAppletに直接Swingコンポーネントを追加するよりも、JAppletの"content pane"に追加するべきです。全てのアプレットにとって、init()もしくはstart()メソッドで時間のかかる処理を実行すべきではありません。代わりに、スレッドを起動して行うべきです。

  • 以下のJComponentのメソッドはどのスレッドから呼び出されても安全です。:repaint(),revalidate(),invalidate().repaint()revalidate() メソッドはそれぞれpaint()validate()を呼び出すために、"event-dispatching thread"にリクエストをキューイングします。invalidate()メソッドはそのコンポーネントの全ての直系の祖先の"validation"が必要なことをただマーキングするだけです。

  • リスナーの一覧はどのスレッドからも変更できます: addListenerTypeListener()removeListenerTypeListener()メソッドの呼び出しは常に安全です。追加もしくは削除操作は"event dispatch"をこなっておいる間、効果を発揮しません。

NOTE: revalidate() と、より古いvalidate() の間には重要な違いがあります。キューイングされたrevalidate()のリクエストは、単一のvalidate()呼び出しにまとめられるかもしれません。これは、キューイングされたrepaint()のリクエストが、単一のpaint()にまとめられるのと似ています。

Event dispatching

ほとんどの初期化後のGUIは"event-dispatching"スレッド上で自然なきっかけで動きます。一旦GUIが表示されると、ほとんどのプログラムはボタン操作や、マウスのクリックのようなイベントをきっかけに動作し、それらは常に"event-dispatching"スレッドでコントロールされています。

しかしながら、いくつかのプログラムは、GUIが表示された後、"non-evnet-driven"なGUIの実行を必要とします。ここにいくつかの例を示します:

  • 使用可能になる前に長い初期化処理が必要なプログラム:このような種類のプログラムは初期化処理の間、なんらかのGUIを表示し、そのGUIを更新及び変化させるべきです。その初期化処理を"event-dispatching"スレッド上で動作させると、再描画処理と"event-dispatch"処理が停止してしまいます。それでも、GUIが初期化された後の更新/変更は"スレッドの安全性"の理由から"event-dispatching"スレッド上で起動されるべきなのです。
  • AWTイベント以外で、GUIを更新しなければならないプログラム:例として、別のマシン上で実行されているかも知れない他のプログラムからのリクエストを受け付けるサーバプログラムが上げられます。これらのリクエストは常に送られてくる可能性があり、結果としてサーバのメソッドのうちの1つが、知るよしもないスレッド上で実行されます。どうすれば、このようなメソッドがGUIを更新出来るのでしょうか?"event-dispaching"スレッド上でGUIの更新コードを実行するのです。

SwingUtilitiesクラスは、"event-dispatch"スレッド上で、あなたの書いたプログラムを実行できるようにする為に、下記の2つのメソッドを提供しています:

  • invokeLater():なんらかのコードを"event-dispatching"スレッドで実行するように要求します。このメソッドはコードの実行を待たずに、終了します。
  • invokeAndWait():コードが実行されるのを待つ事以外は、invokerLater()と同じように動作します。原則として、このメソッドの代わりにinvokeLater()を使うべきです。

このページはこのAPIを使った例を示しています。The Java TutorialBINGO exampleの、特に下記のクラスを見てください: CardWindow,ControlPane, Player, andOverallStatusPane.

invokeLater()メソッドを使う

あなたは、どのスレッドからでも、invokeLater()を呼ぶことができ、"event-dispatching"スレッドになんらかのコードの実行を依頼できます。あなたはそのコードを、invokeLater()の引数として定義したRunnable オブジェクトのrun()メソッドに配置しなければなりません。このinvokeLaterメソッドは"event-dispatching"スレッドがそのコードを実行するのを待たず、すぐに終了します。以下はinvokeLater()を使った例です:


Runnable doWorkRunnable = new Runnable() {
public void run() {
doWork();
}
};
SwingUtilities.invokeLater(doWorkRunnable);

invokeAndWait()メソッドを使う

invokeAndWait()は"event-dispatching"スレッドがそのコードを実行している間は終了しない事以外は、invokeLater()と全く同じです。可能であれば、invokeAndWait()の代わりに、invokeLater()を使うべきです。もしinvokeAndWait()を使えば、invokeAndWait()を呼び出したスレッドは、その呼び出しの実行中に、他のスレッドが必要とするかもしれないどのようなロックも受付無くなります。以下はinvokeAndWait()を使った例です:



void showHelloThereDialog()
throws Exception {
Runnable showModalDialog = new Runnable() {
public void run() {
JOptionPane.showMessageDialog(
myMainFrame, "Hello There");
}
};
SwingUtilities.invokeAndWait(showModalDialog);
}

同じように、2つのテキストフィールドの内容といったGUIの内部情報にアクセスする必要があるスレッドのコードは、下記のようになるでしょう:



void printTextField() throws Exception {
final String[] myStrings = new String[2];

Runnable getTextFieldText = new Runnable() {
public void run() {
myStrings[0] = textField0.getText();
myStrings[1] = textField1.getText();
}
};
SwingUtilities.invokeAndWait(getTextFieldText);
System.out.println(myStrings[0]
+ " " + myStrings[1]);
}

スレッドの作成

避けられるものであれば、スレッドを使うことを避けてください。スレッドは、プログラムのデバッグや、扱いを難しくします。一般的にも、慣例的なGUIの動作(コンポーネントの属性を更新するといった)において、スレッドは全く必要ありません。

しかしながら、時々スレッドが必要になります。ここにスレッドが使われている典型的な状況を示します:


  • "event-dispatching"スレッドをロックせずに、時間のかかるタスクを実行する為。
    例としては、高負荷な計算処理、なんらかの処理の結果、たくさんのクラスがロードされる
    (初期化等)事や、ネットワークやディスクのI/Oのブロッキングが発生する事などが含まれます。


  • 繰り返し処理(終了回数が事前に分かっている事が多い)の実行の為。


  • クライアントからのメッセージを待つ為。

下記2つのクラスが、スレッドの実装に役立つでしょう:

  • SwingWorker:時間のかかる処理を実行するために、バックグラウンドスレッドを作成します。

  • Timer:利用者が定義した時間間隔で、なんらかのコードを1回もしくはそれ以上実行するスレッドを作成します。

SwingWorker Classを使う

SwingWorkerクラスの実装はSwingWorker.java, です。JREのリリースには含まれていませんので、別にダウンロードする必要があります。

SwingWorkerは汚い仕事(バックグラウンドスレッドに実装されるような)を全てこなします。ほとんどのプログラムはバックグラウンドスレッドを必要としないかもしれませんが、バックグラウンドスレッドは、時間のかかる処理を行う場合に有効であり、プログラムの体感速度を改善します。

SwingWorkerクラスを使うには、最初にSwingWorkerのサブクラスを作成します。サブクラスでは、あなたの時間のかかる処理を実行するコードを実装する場所として、construct()メソッドを必ず実装しなければなりません。SwingWorkerのサブクラスをインスタンス化するとき、SwingWorkerはスレッドを作りますが、開始しません。construct()を呼び出すスレッドをスタートするには、SwingWorkerオブジェクトのstart()を実行します。construct()メソッドがオブジェクトを返す必要がある場合、SwingWorkerのget()メソッドを呼びます。ここにSwingWorkerを使う例を示します。:



...//main()メソッド内で:
final SwingWorker worker =
new SwingWorker() {
public Object construct() {
return new
expensiveDialogComponent();
}
};
worker.start();

...//イベントハンドラ内で:
JOptionPane.showMessageDialog
(f, worker.get());

このプログラムのmain()メソッドがstart()メソッドを実行した場合、SwingWorkerはExpensiveDialogComponentインスタンス化する新しいスレッドを開始します。そのmain()メソッドはウィンドウ、1つのボタンと1つのウィンドウを備えた、GUIを構築しています。

利用者がそのボタンをクリックすると、ExpensiveDialogComponentが作成されている間、プログラムはブロックされます。それからExpensiveDialogComponentを含む1つのモーダルダイアログを表示します。MyApplication.javaで、プログラムの全体を見ることが出来ます。

Timerクラスを使う

Timerクラスは1回かそれ以上の操作を実行するためにActionListenerと共に動作します。Timerを作成する場合、あなたは、「どんな間隔でその処理を実行するのか」と、Timerのアクションイベントに対応するリスナーオブジェクトを定義します。一度Timerを開始すると、その処理を実行するために、ActionListeneractionPerformed()が1度かそれ以上の回数実行されます。


Note:そのTimerActionListenerに定義されたactionPerformed()メソッドは、"event-dispatching"スレッドで実行されます。これは、絶対に、このメソッド内で、invokeLater()を実行してはいけないという事を意味しています。

ここで、Timerを使ったアニメーションループの実装を例示します:



public class AnimatorApplicationTimer
extends JFrame implements
ActionListener {
...//where instance variables
...//are declared:
Timer timer;

public AnimatorApplicationTimer(...) {
...
// Set up a timer that calls this
// object's action handler.
timer = new Timer(delay, this);
timer.setInitialDelay(0);
timer.setCoalesce(true);
...
}

public void startAnimation() {
if (frozen) {
// Do nothing. The user has
// requested that we stop
// changing the image.
} else {
//Start (or restart) animating!
timer.start();
}
}

public void stopAnimation() {
//Stop the animating thread.
timer.stop();
}

public void actionPerformed
(ActionEvent e) {
//Advance the animation frame.
frameNumber++;

//Display it.
repaint();
}
...
}

なぜSwingをこのような方法で実装したのか?

ユーザーインターフェースのコードをシングルスレッドで実行するいくつかのメリット:

  • コンポーネントの開発者はスレッドプログラミングを深く理解する必要がありません:ViewPointやTrestleのようなツールキットは, 全てのコンポーネントがマルチスレッドアクセスに対応しなければならず、スレッドプログラミングのエキスパートではない一部の開発者にとって、コンポーネントを拡張しにくくします。ごく最近開発されたSubArctic や IFC のようなツールキットは、Swingに似た設計になっているものが多く見受けられます。
  • イベントの振り分け順序が予想可能です:invokeLater()を使ってキューに登録されたrunnableオブジェクトは、キーボードやマウスのイベント、タイマーイベント、描画リクエストと同じキューから振り分けられます。マルチスレッドアクセスをサポートしたコンポーネントのツールキット上では、数あるコンポーネントの変更はスレッドスケジューラの気まぐれで、並びかえられます。
  • より少ないオーバーヘッド: ツールキットはクリティカルセクションを慎重にロックしようとして、ロックの管理に相当量の時間と、メモリ空間を使います。ツールキットが、クライアントコード(例:パブリッククラスのpublicもしくはprotectedなメソッド)を呼び出すときは常に、クライアントコードが必要なロックを獲得する事ができるように、ツールキットはその状態を保存し、全てのロックを開放しなくてはいけません。そのメソッドからコントロールを取り戻す時、ツールキットはロックを再取得し、保存した状態に戻さねばなりません。ほとんどのアプリケーションは、GUIへの多重アクセスを必要としないのにもかかわらず、そのコストを負担しています。

    ここに、JavaのツールキットであるSubArcticの著作者が書いた、ツールキットで、マルチスレッドアクセスをサポートする事の問題に関する説明があります:


私たちの基本的な信条は、「マルチスレッドアプリケーションの設計と構築には、どれだけ注意を払ってもしすぎることはなく、特にそれがGUIコンポーネントを保持するものであればなおさらである。」です。スレッドの使用は、その見かけとかなり違ったものになります。それらは、しばしば、一つの処理にフォーカスした単純な問題領域において、ものすごくシンプルな設計でプログラミングする事が可能であるかのように見えます。実際にそれらは、いくつかのケースで、設計とコーディングを単純にしています。逆に、それはほとんど全てのケースでデバッグやテストやメンテナンスを困難にし、時には不可能にします。そして、ほとんどのプログラマの訓練、経験、慣習や、私たちが持っているツールも、このような不確定要素に対処できるように設計されていません。例として、タイミング依存のバグは徹底的な検証(これは常に難しい)がほぼ不可能なります。これは1つのプログラムがたくさんの違う種類のマシンやOS上で動作可能で、割り込み型マルチタスキングや、そうではないスケジューリングの両方の影響下で動くJavaにとってはなおさらです。

このような特有の難しさが理由で、私たちは、絶対的な必要性が無い場合に、スレッドを使用するかどうかを2回以上考える事を強く勧めます。しかし、スレッドが必要となる(もしくは、他のソフトウェアパッケージに押し付けられる)ケースがあるので、subArcticはスレッドセーフメカニズムを提供しています。このセクションはこのメカニズムと、独立したスレッドから安全にGUIの階層を操作する為に、このメカニズムの使用方法を説明しています。

彼らが支持しているスレッドセーフメカニズムはSwingUtilitiesクラスが提供しているinvokeLater()invokeAndWait()ととても良く似ています。