Если у получателей моделей Swing нет потокобезопасности, как вы справляетесь с ними?

Хорошо известно, что обновление GUI Swing должно выполняться исключительно в EDT. Меньше объявляется, что чтение материала из GUI должно/должно быть сделано в EDT. Например, пусть метод ButtonModel isSelected(), который сообщает (например) состояние ToggleButton ( "вниз" или "вверх" ).

В каждом примере, который я видел, isSelected() запрашивается отдельно от основного или любого потока. Но когда я смотрю на реализацию DefaultButtonModel, он не синхронизируется, и значение не является изменчивым. Так что, строго говоря, isSelected() может возвращать мусор, если он читает из любого другого потока, кроме того, из которого он установлен (который является EDT, когда пользователь нажимает кнопку). Или я ошибаюсь?

Я изначально думал об этом, когда был шокирован абзацем № 66 в Bloch Effective Java, этот пример:

public class StopThread {
    private static boolean stopRequested;

    public static void main(String[] args) throws InterruptedException {
        Thread backgroundThread = new Thread(new Runnable() {
            public void run() {
                int i = 0;
                while(!stopRequested) i++;
            }
        });
        backgroundThread.start();

        TimeUnit.SECONDS.sleep(1);
        stopRequested = true;
    }
}

Вопреки тому, что кажется, эта программа никогда не заканчивается, по крайней мере на некоторых машинах. Обновление флага stopRequested из основного потока невидимо для фонового потока. Ситуацию можно устранить с помощью синхронизированных геттеров и сеттеров или установить флаг volatile.

Итак:

  • Неправильно запрашивает состояние модели Swing вне EDT (строго говоря)?
  • Если нет, то как?
  • Если да, как вы справляетесь с этим? К счастью, или каким-то умным обходным решением? InvokeAndWait?

Ответы

Ответ 1

  • Нет, не так, но, как и при любой перекрестной передаче, вам необходимо обеспечить соответствующие механизмы защиты потоков, если вы решите это сделать (например, использование synchronized или volatile). Например, я обычно пишу свои собственные реализации TableModel, обычно сидя на List<X>, где X - мой бизнес-объект. Если я намереваюсь, чтобы другие потоки запрашивали Список, я сделаю это синхронизированным Collection. Также стоит отметить, что я никогда не обновлял бы List из других потоков; запросите его только.
  • Потому что это точно такая же ситуация, как и с любым другим многопоточным приложением.
  • Я обычно делаю сбор синхронизированным (см. 1).

Caveat

Несмотря на мой ответ выше, я обычно не модели доступа непосредственно за пределами EDT. Как упоминает Карл, если действие выполнения обновления вызывается через какое-то действие GUI, нет необходимости выполнять какую-либо синхронизацию, поскольку код уже выполняется EDT. Однако, если я хочу выполнить некоторую фоновую обработку, которая приведет к изменению модели, я, как правило, вызываю SwingWorker, а затем присваиваю результаты doInBackground() из метода done() (т.е. На EDT). Это чище IMHO, поскольку метод doInBackground() не имеет побочных эффектов.

Ответ 2

Да, это неправильно; если оба потока не синхронизируют на одном и том же объекте или не используют какой-либо другой барьер памяти, такой как volatile, как определено JMM, тот или иной может наблюдать непоследовательное содержимое памяти. Период. Конец истории. Нарушение этого может работать на некоторых, даже многих, архитектурах, но это в конечном итоге укусит вас в hiney.

Проблема заключается в том, что с некоторыми исключениями, когда вы предоставляете модель, например, о которой говорил Адамски, код Swing не собирается синхронизироваться ни с чем.

И стоит отметить, что JMM (модель памяти Java) изменилась с JSR-133 на Java 5 очень важными способами, поэтому поведение с J4 и более ранней JVM может дать разные результаты, чем J5 и более поздние. Пересмотренная JMM, как и следовало ожидать, значительно улучшилась.

Ответ 3

Swing в целом не только не потокобезопасен, но и нить-враждебный. Однако большинство моделей, кроме текста Swing, являются нитевидными. Это означает, что вы можете использовать модели в любом потоке, если используете стандартную защиту резьбы. Текст Swing был попыткой быть потокобезопасным, но не удался, и на самом деле является нить-враждебным. Используйте только Document из потока диспетчеризации событий (EDT).

Обычный совет - запустить потенциально длительные (обычно блокирующие) задачи с EDT. Однако нарезание резьбы трудно. SwingWorker - популярный способ вызвать плохой дизайн - избегать его. Попытайтесь разделить EDT и код без EDT. Старайтесь не делиться изменчивым состоянием.

Ошибки Threading сложны. Возможно, вы не увидите их на своей машине, но клиенты могут это сделать. Возможно, ошибки возникают из-за оптимизаций в более поздних версиях JRE. Ошибки резьбы трудно отследить. Поэтому будьте консервативны. Даже блокировка EDT ненадолго лучше неприятных ошибок.

Ответ 4

  • Я никогда не беспокоился об этом, но, строго говоря, да, возможно, это неправильно.
  • Н/Д.
  • Я допускаю изменения в моделях графического интерфейса пользователя (через слушателей) моей собственной конструкции, которые не содержатся или не построены Swing. Никогда не нужно спрашивать GUI, потому что GUI обновляет модель.

Так как я сам создаю модели (это может быть что-то очень простое, например, String с getter/setter и PropertyChangeSupport), я могу сделать синхронизацию для доступа, если захочу. Я редко сталкиваюсь с неровным поведением из-за конфликта слов или такого.

Ответ 5

Если да, как вы справляетесь с этим? К счастью, или каким-то умным обходным решением? InvokeAndWait?

Другие уже упомянули SwingWorker, и вы уже знаете invoke*. javax.swing.Timer также может быть полезна. Для безопасного многопоточного программирования нет серебряных пуль, хотя хороший дизайн будет долгим.

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

  public static class ThreadUnsafeAccessException extends RuntimeException {
    private static final long serialVersionUID = 1L;
  }

  @Override public Object getElementAt(int index) {
    if (!SwingUtilities.isEventDispatchThread()) {
      throw new ThreadUnsafeAccessException();
    }
    return decorated.getElementAt(index);
  }

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

Ответ 6

Это отличный вопрос, и Software Monkey имеет правильный ответ. Если вы хотите эффективно выполнять потоки, вы должны полностью понять Java Memory Модель (JMM). Обратите внимание, что модель java-памяти изменилась с помощью JSR-133, поэтому многие старые примеры потоков, которые вы найдете, будут просто ошибочными. Например, ключевое слово volatile изменилось с того, что оно почти бесполезно, и теперь оно почти так же принудительно, как синхронизированное ключевое слово.

Кроме того, теперь у нас есть истинные, вездесущие многоядерные процессоры, что означает истинную многопоточность. Многопоточность до нескольких лет назад только подталкивала ее на одноядерные процессоры, поэтому очень плохой пример кода. Это работало. Теперь это не будет.

Серьезно, если Джошуа Блох может понять это неправильно, пройдите очень осторожно. Это сложный материал (и, да, его код неправильный. Сделайте эту переменную изменчивой, и она будет работать во всех случаях).

О да, Адамски это правильно. Используйте SwingWorker, чтобы убедиться, что длинный код выполняется в фоновом потоке и что, когда он будет обрабатывать данные, он должен получить доступ к Swing по методу done(). Если вам нужно прочитать, сделайте это, прежде чем делать свой SwingWorker и сделайте info final.

Пример кода, который будет вызываться в EDT, скорее всего, в ActionListener или somesuch (psuedocode):

public void someOverlySimpleExampleThatIsCalledOnEDT() {
   /*
    * Any code run here is guaranteed to happen-before 
    * the start of a background thread.
    */
   final String text = myTextField().getText();
   SwingWorker sw = new SwingWorker() {
      private volatile List data;
      public Object doInBackground() {
         //happens in background thread. No Swing access
         data = myDao.getDataFromInternet(text);
         return null;
      }
      public void done() {
         //Happens in EDT. Swing away Merrill
         loadDataIntoJTable(data);
      }
   }
   sw.execute();
   /* 
    * Any code run here happens concurrently with doInBackground(). Be careful. 
    * Usually leave empty
    */
}