株式会社八角研究所 > 【連載】いまさらSwing - 第2回 EDTとスレッド

エンジニア募集中!

独立心旺盛で、新しい技術で新しいWebサービスを作りたいと思っているけれど、ひとりでやることに限界を感じているフリーのエンジニアの方。あなたの期待にこたえられる仲間と環境を、八角研究所なら提供できると思います。社員としてではない関わり方も、あるかもしれません。

2009年03月20日 13:54

いまさらSwing
第2回 EDTとスレッド * * * *

by tunakan

Tags: Java 気になる技術をメモりたい Swing

のんびり書いてる間にJava6update12が出ちゃいました。Swing関連では、AWTの連携強化とJavaFXのパフォーマンス改善のおこぼれがあるくらいでしょうか。

今回は、予告通りEDTについて書いてみたいと思います。

EDTとは、Event Disptach Threadの略で、名前の通りイベントを割り当てるためのスレッドです。EDTは、1つのスレッドなのですべての操作がいったんEDTに集められ、それを適切な処理に振り分けています。

たとえばボタンをクリックするとEDTに「ボタンが押されたよ~。」というメッセージが送信されます。EDTはメッセージを受け取り、ボタンをクリックするというアクションに関連づけられているアクションがあれば、そのアクションを呼び出します。

実際に見てみる

本当に一本のスレッドを使用しているのか簡単なプログラムを作成して見てみます。今回使用するAPIは、Thread#getCurrentThread()です。このAPIを使用すると現在実行中のスレッドに関する情報を取得することができます。

import java.awt.FlowLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;

import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JOptionPane;
import javax.swing.SwingUtilities;

public class ImasaraSwing2_1 {

private static void createAndShowGUI() {

final JFrame f = new JFrame("いまさらSwing2_1");
f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
f.setLayout(new FlowLayout());

for (int i = 0; i < 3; i++) {
final JButton button = new JButton("ボタン" + i);
button.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
final Thread t = Thread.currentThread(); // ここで現在実行中のスレッドを取得
JOptionPane.showMessageDialog(f, String.format(
"%s がクリックされました\n Id: %d ,Name: %s", button.getText(), t
.getId(), t.getName()));
}
});

f.add(button);
}
f.pack();
f.setVisible(true);
}

public static void main(String[] args) {
//ちなみにメインスレッドも表示してみる
final Thread t = Thread.currentThread();
System.out.printf("Id: %d ,Name: %s\n", t.getId(), t.getName());
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
createAndShowGUI();
}
});
}
}

上記コードを実行すると下記のウィンドウが表示されます。

2_1

ボタンを順にクリックして見ると、スレッドIDとスレッド名のメッセージダイアログが表示されます。IDは13、名前は”AWT-EventQueue-0”ですべて同じスレッドであることが分かります。このスレッドがEDTです。

ちなみに、今回は自力でスレッドを取得してEDTの存在を確認しましたが、EDT上で実行されていることを確認するためのユーティリティSwingUtilities#isEventDispatchThread()が用意されています。

2_2 2_3 2_4

EDT上ですべてのイベントが処理されることはわかりましたが、そのことにメリット・デメリットがあるのでしょうか?

1つのスレッドですべてのイベントが処理されるということは、どこかの処理で無限ループや時間のかかる処理を行うと他のイベントが処理できずに固まることを意味します。

試しに(ありがちですが)素数を計算するプログラムを作ってみます。(簡単のためエラトステネスのふるいは使わず、ベタに割り切れる数があるかチェックしています。)

import java.awt.BorderLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;

import javax.swing.DefaultListModel;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JList;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTextField;
import javax.swing.SwingUtilities;

public class ImasaraSwing2_2 {

private static void createAndShowGUI() {
final JFrame frame = new JFrame("いまさらSwing2_2");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setLayout(new BorderLayout());

// 入力パネル
final JPanel inputPanel = new JPanel();
inputPanel.setLayout(new BorderLayout());
final JTextField value = new JTextField();
final JButton calc = new JButton("素数を表示");
inputPanel.add(value, BorderLayout.CENTER);
inputPanel.add(calc, BorderLayout.EAST);
frame.add(inputPanel, BorderLayout.NORTH);

// 結果を表示するリスト
final DefaultListModel listModel = new DefaultListModel();
final JList result = new JList(listModel);
frame.add(new JScrollPane(result), BorderLayout.CENTER);

// ボタンを押した時の処理
calc.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
listModel.clear();
final int n = Integer.parseInt(value.getText());
if (n < 2) {
return;
}
listModel.addElement(2);
for (int i = 3; i <= n; i++) {
if (isPrimeNumber(i)) {
listModel.addElement(i);
}
}
}

// 単純に割り切れる数があるかループしてチェック
private boolean isPrimeNumber(final int n) {
for (int i = 2; i < n; i++) {
if (n % i == 0) {
return false;
}
}
return true;
}
});

frame.setSize(250, 250);
frame.setVisible(true);
}

public static void main(String[] args) {
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
createAndShowGUI();
}
});
}

}

上記コードを実行すると下記のような画面が表示されます。数字を入力して[素数計算]ボタンをクリックすると、その数字以下すべての素数が表示されます。10000程度までなら最近のPCの性能であれば一瞬で表示されると思います。

2_5

しかし、5万,10万と数字を大きくすると次第に遅くなることがわかります。私のPCで100万を入力してウィンドウを操作すると下記のようになってしまいました。

2_6

では、時間のかかる処理は、どうすればよいでしょうか?

EDT上で実行できないなら答えは一つしかありません。EDT以外のスレッドで実行するしかないです。

というわけで、さきほどのプログラムの計算部分をベタにスレッドにしてみます。

import java.awt.BorderLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;

import javax.swing.DefaultListModel;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JList;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTextField;
import javax.swing.SwingUtilities;

public class ImasaraSwing2_3 {

private static void createAndShowGUI() {
final JFrame frame = new JFrame("いまさらSwing2_3");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setLayout(new BorderLayout());

// 入力パネル
final JPanel inputPanel = new JPanel();
inputPanel.setLayout(new BorderLayout());
final JTextField value = new JTextField();
final JButton calc = new JButton("素数を表示");
inputPanel.add(value, BorderLayout.CENTER);
inputPanel.add(calc, BorderLayout.EAST);
frame.add(inputPanel, BorderLayout.NORTH);

// 結果を表示するリスト
final DefaultListModel listModel = new DefaultListModel();
final JList result = new JList(listModel);
frame.add(new JScrollPane(result), BorderLayout.CENTER);

// ボタンを押した時の処理
calc.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
listModel.clear();
final int n = Integer.parseInt(value.getText());
if (n < 2) {
return;
}
// 計算は、計算スレッドに任せる。
new CalcThread(n, listModel).start();
}
});

frame.setSize(250, 250);
frame.setVisible(true);
}
//計算を行って、結果をリストボックスに表示するスレッド
private static class CalcThread extends Thread {
private int n;
private DefaultListModel listModel;

public CalcThread(final int n, final DefaultListModel listModel) {
this.listModel = listModel;
this.n = n;
}

@Override
public void run() {
listModel.addElement(2);
for (int i = 3; i <= n; i++) {
if (isPrimeNumber(i)) {
listModel.addElement(i);
}
}
}

private boolean isPrimeNumber(final int n) {
for (int i = 2; i < n; i++) {
if (n % i == 0) {
return false;
}
}
return true;
}
}

public static void main(String[] args) {
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
createAndShowGUI();
}
});
}

}

今度は、うまく動くでしょうか?

実は、これでもうまく動かないのです。運が良ければうまく動いているように見えますが、たいていはリストボックスが更新されなかったり、意図しないタイミングでリストボックスが更新したりすると思います。

実は、Swingにはもう一つ決まり(というか制約)があって、基本的にコンポーネントの更新処理(今回の例ではリストボックスに値を表示)は、EDT上で行わなければならないのです。

再度登場SwingUtilities#invokeLater

ここで、第1回で”おまじない”としておいたSwingUtilities#invokeLaterが再度登場します。SwingUtilities#invokeLaterは、EDTに対して「後でEDT上で実行してね。はぁ~と。」とお願いするメソッドです。このメソッドを使用してリストボックスの更新処理を書き直してみます。

import java.awt.BorderLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;

import javax.swing.DefaultListModel;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JList;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTextField;
import javax.swing.SwingUtilities;

public class ImasaraSwing2_4 {

private static void createAndShowGUI() {
final JFrame frame = new JFrame("いまさらSwing2_4");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setLayout(new BorderLayout());

// 入力パネル
final JPanel inputPanel = new JPanel();
inputPanel.setLayout(new BorderLayout());
final JTextField value = new JTextField();
final JButton calc = new JButton("素数を表示");
inputPanel.add(value, BorderLayout.CENTER);
inputPanel.add(calc, BorderLayout.EAST);
frame.add(inputPanel, BorderLayout.NORTH);

// 結果を表示するリスト
final DefaultListModel listModel = new DefaultListModel();
final JList result = new JList(listModel);
frame.add(new JScrollPane(result), BorderLayout.CENTER);

// ボタンを押した時の処理
calc.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
listModel.clear();
final int n = Integer.parseInt(value.getText());
if (n < 2) {
return;
}
// 計算は、計算スレッドに任せる。
new CalcThread(n, listModel).start();
}
});

frame.setSize(250, 250);
frame.setVisible(true);
}

private static class CalcThread extends Thread {
private int n;
private DefaultListModel listModel;

public CalcThread(final int n, final DefaultListModel listModel) {
this.listModel = listModel;
this.n = n;
}

//EDT上で更新されるようにお願いする。
private void addList(final int n) {
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
listModel.addElement(n);
}
});
}

@Override
public void run() {
addList(2);
for (int i = 2; i <= n; i++) {
if (isPrimeNumber(i)) {
addList(i);
}
}
}

private boolean isPrimeNumber(final int n) {
for (int i = 3; i < n; i++) {
if (n % i == 0) {
return false;
}
}
return true;
}
}

public static void main(String[] args) {
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
createAndShowGUI();
}
});
}

}

ここまでやって、ようやくまともに動作するようになります(実は、まだ問題がありますが・・・)。実際に大きな数字を入力して実行してみるとモリモリとリストボックスが大きくなっていくことがわかります。

でも、たいしたことやってない割に面倒くさすぎませんか?

このような処理を行うためにJava6からSwingWorkerというクラスが用意されました(Java5へのポーティングもあるようです)。このクラスを使用するともっとスッキリと書くことできるようになります。SwingWorkerを使用して書き直すと下記のようになります。詳しい説明は、JavaDocを参照してください。

import java.awt.BorderLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;

import javax.swing.DefaultListModel;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JList;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTextField;
import javax.swing.SwingUtilities;
import javax.swing.SwingWorker;

public class ImasaraSwing2_5 {

private static void createAndShowGUI() {
final JFrame frame = new JFrame("いまさらSwing2_5");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setLayout(new BorderLayout());

// 入力パネル
final JPanel inputPanel = new JPanel();
inputPanel.setLayout(new BorderLayout());
final JTextField value = new JTextField();
final JButton calc = new JButton("素数を表示");
inputPanel.add(value, BorderLayout.CENTER);
inputPanel.add(calc, BorderLayout.EAST);
frame.add(inputPanel, BorderLayout.NORTH);

// 結果を表示するリスト
final DefaultListModel listModel = new DefaultListModel();
final JList result = new JList(listModel);
frame.add(new JScrollPane(result), BorderLayout.CENTER);

// ボタンを押した時の処理
calc.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
listModel.clear();
final int n = Integer.parseInt(value.getText());
if (n < 2) {
return;
}
//SwingWorkerを使用して書き直す
new SwingWorker<Void, Integer>() {
@Override
protected Void doInBackground() throws Exception {
publish(2);
for (int i = 2; i <= n; i++) {
if (isPrimeNumber(i)) {
publish(i); //publishするとEDT上でprocessが実行される。
}
}
return null;
}

private boolean isPrimeNumber(final int n) {
for (int i = 3; i < n; i++) {
if (n % i == 0) {
return false;
}
}
return true;
}
//publishされた値は、ここで処理される。このメソッドはEDT上で実行される。
protected void process(java.util.List<Integer> chunks) {
for (int i: chunks) {
listModel.addElement(i);
}
};

}.execute();
}
});

frame.setSize(250, 250);
frame.setVisible(true);
}

public static void main(String[] args) {
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
createAndShowGUI();
}
});
}

}

2_7

いい忘れたこと

コンポーネントの更新処理は、EDT上で行う必要があるということで、リストボックスの更新処理をEDT上で実行するというサンプルを用いました。実は、これには例外があってJTextFieldやJTextAreaなどのJTextComponentを継承したクラスのsetTextメソッドは、EDT以外のスレッドからも更新する事が可能です。

ちなみJTextComponent#setTextのJavaDocには以下のように書かれています。(それにしても、この日本語では何を言ってるのか分かりませんね。。。)

このメソッドはスレッドに対して安全ですが、ほとんどの Swing メソッドは違います。詳細は、「How to Use Threads」を参照してください。

今回のポイント

  • ボタンクリックやウィンドウ操作などのイベントはEDT上で処理される。
  • EDT上で時間のかかる処理をしない 。時間のかかる処理は別のスレッドで実行。
  • コンポーネントの更新は、必ずEDT上で行う(ただし、JTextComponent#setTextのような例外もある)

次回予告

次回は、EDTと並んで理解の難しい(ような気のする)レイアウトについて書きます。

参考にしたページ

この記事の執筆者

九州男児 tunakan 28歳 入社2年目

内臓が全般的に弱いです。データベースは苦手なのに変なデーターベースに当たることが多い気がします。

この人の会社をみる この人関連のイベントをさがす この人と一緒にはたらく

この日記にコメントする

(メールアドレスは公開されません。メールで返答が欲しい場合などに入力してください)

このエントリへのトラックバックURL

コメント

コメントはありません

トラックバック

トラックバックはありません

メンバー紹介

tunakan

tunakan

内臓が全般的に弱いです。データベースは苦手なのに変なデーターベースに当たることが多い気がします。

はち子さん

はち子

広報を担当している、はち子です。本当は広報じゃなくて事務デスが、広報って言うほうがかっこいいし! 社内でゆいいつ、...

sugimaru

sugimaru

すぎまるです。22時のシンデレラです。