JRuby+swingでinvokeLaterを使ってみる

SwingUtilities.invokeLaterというものを自分なりに理解してみました。

swingのスレッドについて

Swingのスレッドは、通常スレッドセーフではありません。また、Swingには次のようなスレッドポリシーがあります。従わないと最悪デッドロックが発生することもあるようです。

  • コンポーネントの「状態に依存する」あるいは「状態に影響を与える」 処理はすべてEDT(イベントディスパッチスレッド)上で動作しなければならない

簡単にいうと描画に関しては1つのスレッド(EDT)上で処理しなければならないということのようです。

サンプルコード

EDTを理解するに当って以下のプログラムを用意してみました。
1つはEDTを使わなかった場合の2つのスレッド実行、もう1つはinvokeLaterを使った場合の実行を示します。
以下ではボタンをクリックすると2つのスレッド上でCountクラスのrunが実行されて、合計で(期待値=)200となるプログラムです。

  • 「Count.new(label).run」でカウントした場合(正しく200までカウントされない)

※該当するコードをコメント箇所を修正してください。

  • 「SwingUtilities.invoke_later Count.new(label)」でカウントした場合(正しく200までカウントされている)
require 'java'

%w( JFrame JPanel JLabel JButton SwingUtilities ).each{|c| eval c+"=javax.swing."+c}

class Count
  def initialize model
    @model = model
  end
  def run
    100.times do
      n = @model.text.to_i
      @model.text = (n + 1).to_s
    end
  end
end

def run
  label = JLabel.new "0"
  button = JButton.new "Push"
  
  button.add_action_listener do |event|
    Thread.start do
#      Count.new(label).run
      SwingUtilities.invoke_later Count.new(label)
    end
    Thread.start do
#      Count.new(label).run
      SwingUtilities.invoke_later Count.new(label)
    end
  end
  
  panel = JPanel.new
  panel.add label
  panel.add button
  
  frame = JFrame.new "invokeLater sample"
  frame.default_close_operation = JFrame::EXIT_ON_CLOSE
  frame.content_pane.add panel
  frame.set_size 150,80
  frame.location_relative_to = nil
  frame.visible = true
end

SwingUtilities.invoke_later self


「Count.new(label).run」で正しく200までカウントされない原因は、説明は省きます。ここが重要なんだと思いますが。。


失敗したこと

ボタンクリックしたときの動作記述を以下のように書いた時、うまく動きませんでした。

    SwingUtilities.invoke_later do
      100.times do
        n = label.text.to_i
        label.text = (n + 1).to_s
      end
    end

これで実行してしまうと、なぜかフレームが200個?複製されてうまく動作しません。
原因は、よくわかりませんでした。。

感想

今回のサンプルコードでは、おそらく完全には理解できたとは言えないと思いますが、それでも僕にとってはSwingスレッド理解への手助けにはなったと感じています。


とりあえず、ここまで。