読者です 読者をやめる 読者になる 読者になる

プログラムdeタマゴ

nodamushiの著作物は、文章、画像、プログラムにかかわらず全てUnlicenseです

気をつけろ、javax.swing.TimerはEDTで動くぞ!

どーもアプリケーションの応答が思わぬところで悪くなる。原因を探っていたら、なんとjavax.swing.Timerが原因だった。
javax.swing.Timerはjava.lang.Timerに比べてシンプルで使いやすいので、よく使っていたのだが、なんと、javax.swing.TimerはEDT(Event Dispatch Thread)上で動いていた!

うん、全く知らんかった!



おかげで、

  1. 重たいアニメーション処理はTimerで別スレッドでやって、repaint呼び出そー(描画速度の精度はその程度で良かったので。)
  2. あれ?マウスイベント飛び飛びでしか受け付けないんだけど(´・д・`)
  3. マウスイベント別スレッドに投げてから処理する?
  4. 直んねぇぞ、クルァ!( ゚д゚)
  5. あれ、別スレッドに投げてもEDTが重いっておかしくね?(´・д・`)

ここまできてようやく気がつくという体たらくを披露してしまった。


ちなみに、JavaDocにはその該当部分はこう書いてある

ただし、タイマーは、カーソルの点滅やツールヒントの表示などと同じスレッドを使用します。




EDT上で動作するって書いておいてくれないかな。

続きはついでにjavax.swing.Timerのソースを読んできたんでまとめてみるよ。

Timerのアクションの実行

Timerのアクションは以下のパッケージ関数postで実行される。
どうみてもEDTに投げてます。本当にありがとうございました。

synchronized void post() {
    if (notify == false || !coalesce) {
        notify = true;
        SwingUtilities.invokeLater(doPostEvent);
    }
}

doPostEventはDoPostEventというRunnableを実装した内部クラスで、Timerに登録されているリスナーにイベントを通知する。



Timerはリスト構造で管理されてる

最初にTimer.javaを読んだときにフィールドにあったこれ何かな〜と思ってた。
名前的にリスト構造というのは分かるけど、Timer内では一度も触られることのないフィールド変数。

Tiemer nextTimer;

結論から言うと、これはタイマーを起動する順番を表すリストになっています。おもしろいですね。まさかこんなリスト構造で保持してるとは思っていませんでした。というのも私はTimerが、一つのスレッドで動いているって事すら知らなかったんです。(これはJavaDocに明示されています。ちゃんとJavaDoc読め。)なので、こんな発想はなかったので、かなりへ〜っと思ってしまいました。


javax.swing.TimerQueueというパッケージプライベートクラスがあり、このクラスの唯一のインスタンスにTimerを登録することで、Timerの実行タイミングを管理しています。
実際にそのシーンがここです。

//timer 追加するタイマー
//expirationTime 実行する時間
synchronized void addTimer(Timer timer, long expirationTime) {
    Timer previousTimer;
    Timer nextTimer;
    if (timer.running) {
        return;
    }

    previousTimer = null;
    nextTimer = firstTimer;

//timerを突っ込む場所を探す
    while (nextTimer != null) {
        if (nextTimer.expirationTime > expirationTime) break;

        previousTimer = nextTimer;
        nextTimer = nextTimer.nextTimer;
    }

    if (previousTimer == null) {
        firstTimer = timer;
    }
    else {
        previousTimer.nextTimer = timer;//ここと
    }

    timer.expirationTime = expirationTime;
    timer.nextTimer = nextTimer;//ここでリスト作成してるべ
    timer.running = true;
    notify();
}

Timerを呼び出す

ついでに実際にタイマーを呼び出してる場面です。これはTimerQueueの関数です。
この関数の返値はスリープする時間です。
ここまでわかってたら誰が書いてもこんな感じになると思います。
ただ、繰り返し処理はここで再登録しているようです。

synchronized long postExpiredTimers() {
    Timer   timer;
    long    currentTime;
    long    timeToWait;

    do {
        timer = firstTimer;//リストの先頭のタイマー
        if (timer == null) return 0;

        currentTime = System.currentTimeMillis();
        timeToWait = timer.expirationTime - currentTime;

        if (timeToWait <= 0) {
            try {
                timer.post();  // have timer post an event
            }
            catch (SecurityException e) {
            }

            removeTimer(timer);

            //繰り返すなら再度登録
            if (timer.isRepeats()) {
                addTimer(timer, currentTime + timer.getDelay());
            }

            try {
                wait(1);
            }catch (InterruptedException e) {}
        }
    } while (timeToWait <= 0);

    return timeToWait;
}

こうやってみると、そしてTimerが一つのスレッド上で動くように作られていることからみると、このリスト構造は当たり前の選択肢ですね。