Статьи

Многопоточность в Java Swing с SwingWorker

Если вы пишете настольную программу или программу Java Web Start на Java с использованием Swing, вам может потребоваться запустить некоторые вещи в фоновом режиме, создав собственные потоки.

Ничто не мешает вам использовать стандартные методы многопоточности в Swing, и применяются обычные соображения. Если у вас есть несколько потоков, обращающихся к одним и тем же переменным, вам необходимо использовать синхронизированные методы или блоки кода (или поточно-ориентированные классы, такие как AtomicInteger или ArrayBlockingQueue).

Однако для неосторожных есть ловушка. Как и в большинстве API-интерфейсов пользователя, вы не можете обновить пользовательский интерфейс из созданных вами потоков. Ну, как знает каждый студент Java, вы часто можете , но не должны. Если вы сделаете это, иногда ваша программа будет работать, а иногда — нет.

Вы можете обойти эту проблему, используя специализированный класс SwingWorker. В этой статье я покажу вам, как вы можете заставить ваши программы работать, даже если вы используете класс Thread, а затем мы продолжим рассмотрение решения SwingWorker.

Для демонстрации я создал небольшую программу Swing.

Как видите, он состоит из двух меток и кнопки запуска. В данный момент нажатие кнопки запуска вызывает метод-обработчик, который ничего не делает. Вот код Java:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
import java.awt.Font;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.List;
import java.util.concurrent.ExecutionException;
 
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.SwingUtilities;
import javax.swing.SwingWorker;
 
public class MainFrame extends JFrame {
 
 private JLabel countLabel1 = new JLabel('0');
 private JLabel statusLabel = new JLabel('Task not completed.');
 private JButton startButton = new JButton('Start');
 
 public MainFrame(String title) {
  super(title);
 
  setLayout(new GridBagLayout());
 
  countLabel1.setFont(new Font('serif', Font.BOLD, 28));
 
  GridBagConstraints gc = new GridBagConstraints();
 
  gc.fill = GridBagConstraints.NONE;
 
  gc.gridx = 0;
  gc.gridy = 0;
  gc.weightx = 1;
  gc.weighty = 1;
  add(countLabel1, gc);
 
  gc.gridx = 0;
  gc.gridy = 1;
  gc.weightx = 1;
  gc.weighty = 1;
  add(statusLabel, gc);
 
  gc.gridx = 0;
  gc.gridy = 2;
  gc.weightx = 1;
  gc.weighty = 1;
  add(startButton, gc);
 
  startButton.addActionListener(new ActionListener() {
   public void actionPerformed(ActionEvent arg0) {
    start();
   }
  });
 
  setSize(200, 400);
  setDefaultCloseOperation(EXIT_ON_CLOSE);
  setVisible(true);
 }
 
 private void start() {
 
 }
 
 public static void main(String[] args) {
  SwingUtilities.invokeLater(new Runnable() {
 
   @Override
   public void run() {
    new MainFrame('SwingWorker Demo');
   }
  });
 }
}

Мы собираемся добавить некоторый код в метод start (), который вызывается в ответ на нажатие кнопки запуска.

Сначала давайте попробуем нормальный поток.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
private void start() {
 Thread worker = new Thread() {
  public void run() {
 
   // Simulate doing something useful.
   for(int i=0; i<=10; i++) {
    // Bad practice
    countLabel1.setText(Integer.toString(i));
 
    try {
     Thread.sleep(1000);
    } catch (InterruptedException e) {
 
    }
   }
 
   // Bad practice
   statusLabel.setText('Completed.');
  }
 };
 
 worker.start();
}

На самом деле, этот код, кажется, работает (по крайней мере, для меня в любом случае). Программа выглядит так:

Однако это не рекомендуемая практика. Мы обновляем GUI из нашего собственного потока, и при некоторых обстоятельствах это, безусловно, приведет к возникновению исключений.

Если мы хотим обновить графический интерфейс из другого потока, мы должны использовать SwingUtilities, чтобы запланировать запуск нашего кода обновления в потоке диспетчеризации событий.

Следующий код хорош, но ужасен как сам дьявол.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
private void start() {
  Thread worker = new Thread() {
   public void run() {
 
    // Simulate doing something useful.
    for(int i=0; i<=10; i++) {
 
     final int count = i;
 
     SwingUtilities.invokeLater(new Runnable() {
      public void run() {
       countLabel1.setText(Integer.toString(count));
      }
     });
 
     try {
      Thread.sleep(1000);
     } catch (InterruptedException e) {
 
     }
    }
 
    SwingUtilities.invokeLater(new Runnable() {
     public void run() {
      statusLabel.setText('Completed.');
     }
    });
 
   }
  };
 
  worker.start();
 }

Конечно, должно быть что-то, что мы можем сделать, чтобы сделать наш код более элегантным?

Класс SwingWorker

SwingWorker — это альтернатива использованию Thread class , специально разработанного для Swing. Это абстрактный класс, который принимает два параметра шаблона, что делает его очень свирепым и отталкивает большинство людей от его использования. Но на самом деле это не так сложно, как кажется.

Давайте посмотрим на некоторый код, который просто запускает фоновый поток. В этом первом примере мы не будем использовать ни один из параметров шаблона, поэтому мы установим для них обоих значение Void , эквивалентный классу Java примитивного типа void (со строчной буквой «v»).

Запуск фоновой задачи

Мы можем запустить задачу в фоновом режиме, реализовав метод doInBackground и вызвав execute для запуска нашего кода.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
SwingWorker<Void, Void> worker = new SwingWorker<Void, Void>() {
   @Override
   protected Void doInBackground() throws Exception {
    // Simulate doing something useful.
    for (int i = 0; i <= 10; i++) {
     Thread.sleep(1000);
     System.out.println('Running ' + i);
    }
 
    return null;
   }
  };
 
  worker.execute();

Обратите внимание, что SwingWorker — это одноразовый SwingWorker , поэтому, если мы хотим запустить код снова, нам нужно будет создать еще один SwingWorker ; вы не можете перезапустить тот же.

Довольно просто, а? Но что, если мы хотим обновить графический интерфейс с некоторым статусом после запуска нашего кода? Вы не можете обновить графический интерфейс из doInBackground , потому что он не работает в основном потоке отправки событий.

Но есть решение. Нам нужно использовать первый параметр шаблона.

Обновление GUI после завершения потока

Мы можем обновить GUI, возвращая значение из doInBackground() а затем doInBackground() done() , что может безопасно обновить GUI. Мы используем метод get() чтобы получить значение, возвращаемое из doInBackground()

Таким образом, первый параметр шаблона определяет тип возвращаемого значения как doInBackground() и get() .

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
SwingWorker<Boolean, Void> worker = new SwingWorker<Boolean, Void>() {
   @Override
   protected Boolean doInBackground() throws Exception {
    // Simulate doing something useful.
    for (int i = 0; i <= 10; i++) {
     Thread.sleep(1000);
     System.out.println('Running ' + i);
    }
 
    // Here we can return some object of whatever type
    // we specified for the first template parameter.
    // (in this case we're auto-boxing 'true').
    return true;
   }
 
   // Can safely update the GUI from this method.
   protected void done() {
 
    boolean status;
    try {
     // Retrieve the return value of doInBackground.
     status = get();
     statusLabel.setText('Completed with status: ' + status);
    } catch (InterruptedException e) {
     // This is thrown if the thread's interrupted.
    } catch (ExecutionException e) {
     // This is thrown if we throw an exception
     // from doInBackground.
    }
   }
 
  };
 
  worker.execute();

Что если мы хотим обновить графический интерфейс по мере продвижения? Вот для чего нужен второй параметр шаблона.

Обновление GUI из запущенного потока

Чтобы обновить графический интерфейс из запущенного потока, мы используем второй параметр шаблона. Мы вызываем метод publish() для «публикации» значений, которыми мы хотим обновить пользовательский интерфейс (который может быть любого типа, указанного вторым параметром шаблона). Затем мы переопределяем метод process() , который получает значения, которые мы публикуем.

На самом деле process() получает списки опубликованных значений, потому что несколько значений могут быть опубликованы до того, как будет вызван process() .

В этом примере мы просто публикуем последнее значение в пользовательском интерфейсе.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
SwingWorker<Boolean, Integer> worker = new SwingWorker<Boolean, Integer>() {
 @Override
 protected Boolean doInBackground() throws Exception {
  // Simulate doing something useful.
  for (int i = 0; i <= 10; i++) {
   Thread.sleep(1000);
 
   // The type we pass to publish() is determined
   // by the second template parameter.
   publish(i);
  }
 
  // Here we can return some object of whatever type
  // we specified for the first template parameter.
  // (in this case we're auto-boxing 'true').
  return true;
 }
 
 // Can safely update the GUI from this method.
 protected void done() {
 
  boolean status;
  try {
   // Retrieve the return value of doInBackground.
   status = get();
   statusLabel.setText('Completed with status: ' + status);
  } catch (InterruptedException e) {
   // This is thrown if the thread's interrupted.
  } catch (ExecutionException e) {
   // This is thrown if we throw an exception
   // from doInBackground.
  }
 }
 
 @Override
 // Can safely update the GUI from this method.
 protected void process(List<Integer> chunks) {
  // Here we receive the values that we publish().
  // They may come grouped in chunks.
  int mostRecentValue = chunks.get(chunks.size()-1);
 
  countLabel1.setText(Integer.toString(mostRecentValue));
 }
 
};
 
worker.execute();

Больше …. ? Ты хочешь больше …. ?

Надеюсь, вам понравилось это введение в очень полезный класс SwingWorker.

На моем сайте Cave of Programming можно найти дополнительные учебные пособия, в том числе полный бесплатный видеокурс по многопоточности и курсы по Swing, Android и сервлетам.

Ссылка: многопоточность в Java Swing с SwingWorker от нашего партнера по JCG Джона Перселла в блоге Java Advent Calendar .