Какова самая частая проблема с concurrency, с которой вы столкнулись в Java?

Это опрос о распространенных проблемах concurrency в Java. Примером может быть классический тупик или состояние гонки или, возможно, EDT резьбовые ошибки в Swing. Меня интересуют как широкие возможности, так и проблемы, которые наиболее распространены. Поэтому, пожалуйста, оставьте один конкретный ответ ошибки Java concurrency на комментарий и проголосуйте, если увидите тот, с которым вы столкнулись.

Ответ 1

Наиболее часто встречающаяся проблема concurrency, которую я видел, не понимает, что поле, написанное одним потоком, не может быть замечено другим потоком. Общее применение этого:

class MyThread extends Thread {
  private boolean stop = false;

  public void run() {
    while(!stop) {
      doSomeWork();
    }
  }

  public void setStop() {
    this.stop = true;
  }
}

Пока остановка нестабильна или setStop и run не синхронизированы, это не гарантируется. Эта ошибка особенно дьявольская, как в 99,999%. На практике это будет неважно, так как читательский поток в конце концов увидит изменение - но мы не знаем, как скоро он это увидел.

Ответ 2

Моя самая болезненная проблема concurrency № 1, когда две разные библиотеки с открытым исходным кодом сделали что-то вроде этого:

private static final String LOCK = "LOCK";  // use matching strings 
                                            // in two different libraries

public doSomestuff() {
   synchronized(LOCK) {
       this.work();
   }
}

На первый взгляд это похоже на довольно простой пример синхронизации. Однако; потому что Strings интернированы в Java, буквальная строка "LOCK" оказывается тем же экземпляром java.lang.String(даже если они объявлены полностью несопоставимыми друг с другом). Результат явно плох.

Ответ 3

Одна классическая проблема заключается в изменении объекта, который вы синхронизируете, при синхронизации на нем:

synchronized(foo) {
  foo = ...
}

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

Ответ 4

Общей проблемой является использование таких классов, как Calendar и SimpleDateFormat, из нескольких потоков (часто путем кэширования их в статической переменной) без синхронизации. Эти классы не являются потокобезопасными, поэтому многопоточный доступ в конечном итоге вызывает странные проблемы с несогласованным состоянием.

Ответ 5

Двойная проверка блокировки. В целом.

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

public Class MySingleton {
  private static MySingleton s_instance;
  public static MySingleton getInstance() {
    if(s_instance == null) {
      synchronized(MySingleton.class) { s_instance = new MySingleton(); }
    }
    return s_instance;
  }
}

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

  public static MySingleton getInstance() {
    if(s_instance == null) {
      synchronized(MySingleton.class) {
        if(s_instance == null) s_instance = new MySingleton();
      }
    }
    return s_instance;
  }

Это тоже не работает, потому что модель памяти Java не поддерживает ее. Вам нужно объявить s_instance как volatile, чтобы заставить его работать, и даже тогда он работает только на Java 5.

Люди, которые не знакомы с тонкостями модели памяти Java, все время путают это.

Ответ 6

Неправильная синхронизация объектов, возвращаемых Collections.synchronizedXXX(), особенно во время итерации или нескольких операций:

Map<String, String> map = Collections.synchronizedMap(new HashMap<String, String>());

...

if(!map.containsKey("foo"))
    map.put("foo", "bar");

Это неверно. Несмотря на синхронизацию одиночных операций, состояние карты между вызовами contains и put может быть изменено другим потоком. Это должно быть:

synchronized(map) {
    if(!map.containsKey("foo"))
        map.put("foo", "bar");
}

Или с реализацией ConcurrentMap:

map.putIfAbsent("foo", "bar");

Ответ 7

Хотя, вероятно, не совсем то, о чем вы просите, самая частая проблема, связанная с concurrency, с которой я столкнулся (вероятно, потому, что она появляется в обычном однопоточном коде) - это

java.util.ConcurrentModificationException

вызванный такими вещами, как:

List<String> list = new ArrayList<String>(Arrays.asList("a", "b", "c"));
for (String string : list) { list.remove(string); }

Ответ 8

Самая распространенная ошибка, которую мы видим, когда я работаю, - это программисты, которые выполняют длительные операции, такие как вызовы серверов, в EDT, блокируя GUI в течение нескольких секунд и делая приложение невосприимчивым.

Ответ 9

Забыть ждать() (или Condition.await()) в цикле, проверяя, что условие ожидания действительно истинно. Без этого вы сталкиваетесь с ошибками из ложных ожиданий() пробуждения. Каноническое использование должно быть:

 synchronized (obj) {
     while (<condition does not hold>) {
         obj.wait();
     }
     // do stuff based on condition being true
 }

Ответ 10

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

 List<String> l = Collections.synchronizedList(new ArrayList<String>());
 String[] s = l.toArray(new String[l.size()]);

Например, во второй строке выше методы toArray и size() сами по себе являются потокобезопасными, но размер() оценивается отдельно от параметра toArray(), а блокировка в списке не является между этими двумя вызовами. Если вы запустите этот код с другим потоком, одновременно удалив элементы из списка, рано или поздно вы получите новую строку String [], которая больше, чем требуется для хранения всех элементов в списке, и имеет нулевые значения в хвосте, Легко думать, что, поскольку два метода вызова в List происходят в одной строке кода, это как-то атомная операция, но это не так.

Ответ 11

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

Ответ 12

До тех пор, пока я не взял класс с Брайаном Гетцем, я не понял, что несинхронизированный getter частного поля, мутировавший через синхронизированный сеттер, никогда не сможет вернуть обновленное значение. Только когда переменная защищена синхронизированным блоком на обоих чтениях и записи, вы получите гарантию последнего значения переменной.

public class SomeClass{
    private Integer thing = 1;

    public synchronized void setThing(Integer thing)
        this.thing = thing;
    }

    /**
     * This may return 1 forever and ever no matter what is set
     * because the read is not synched
     */
    public Integer getThing(){
        return thing;  
    }
}

Ответ 13

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

Ответ 14

Произвольные вызовы методов не должны выполняться из синхронизированных блоков.

Дэйв Рэй коснулся этого в своем первом ответе, и на самом деле я также столкнулся с тупиком, который также связан с вызовом методов слушателей из синхронного метода. Я думаю, что более общий урок заключается в том, что вызовы методов не должны выполняться "в диком" из синхронизированного блока - вы не знаете, будет ли вызов длительным, приведет к тупиковой ситуации или что-то еще.

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

Кроме того, поскольку мы теперь обращались к сборке слушателей за пределами синхронизированного блока, мы изменили его на коллекцию Copy-on-Write. Или мы могли бы просто сделать защитную копию Коллекции. Суть в том, что обычно есть альтернативы безопасному доступу к коллекции неизвестных объектов.

Ответ 15

Самый последний связанный с ошибкой Concurrency, с которым я столкнулся, был объектом, который в его конструкторе создал ExecutorService, но когда объект больше не ссылался, он никогда не закрывал ExecutorService. Таким образом, в течение нескольких недель тысячи потоков просочились, что в конечном итоге привело к сбою системы. (Технически, он не разбился, но он прекратил функционировать должным образом, продолжая работать.)

Технически я полагаю, что это не проблема concurrency, но это проблема, связанная с использованием библиотек java.util.concurrency.

Ответ 16

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

РЕДАКТИРОВАТЬ: Перемещена вторая часть для разделения ответа.

Ответ 17

Несбалансированная синхронизация, особенно против карт, кажется довольно распространенной проблемой. Многие считают, что синхронизация на переносится на карту (а не на ConcurrentMap, но говорят HashMap), а не на синхронизацию на основе. Однако это может привести к бесконечному циклу во время повторного хеширования.

Такая же проблема (частичная синхронизация) может произойти в любом месте, где есть общее состояние с чтением и записью.

Ответ 18

Я столкнулся с проблемой concurrency с сервлетами, когда есть изменяемые поля, которые будут заданы каждым запросом. Но для всего запроса есть только один экземпляр сервлета, поэтому он отлично работал в одной пользовательской среде, но когда несколько пользователей запросили непредвиденные результаты сервлета.

public class MyServlet implements Servlet{
    private Object something;

    public void service(ServletRequest request, ServletResponse response)
        throws ServletException, IOException{
        this.something = request.getAttribute("something");
        doSomething();
    }

    private void doSomething(){
        this.something ...
    }
}

Ответ 19

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

Больше людей должны использовать аннотации concurrency (например, @ThreadSafe, @GuardedBy и т.д.), описанные в книге Goetz.

Ответ 20

Переменные классы в структурах общих данных

Thread1:
    Person p = new Person("John");
    sharedMap.put("Key", p);
    assert(p.getName().equals("John");  // sometimes passes, sometimes fails

Thread2:
    Person p = sharedMap.get("Key");
    p.setName("Alfonso");

Когда это произойдет, код намного сложнее, чем этот упрощенный пример. Репликация, поиск и исправление ошибки сложны. Возможно, этого можно было бы избежать, если бы мы могли отмечать определенные классы как неизменные и определенные структуры данных, поскольку они содержали неизменные объекты.

Ответ 21

Я полагаю, что в будущем основной проблемой Java станет (отсутствие) гарантии видимости для конструкторов. Например, если вы создаете следующий класс

class MyClass {
    public int a = 1;
}

а затем просто прочитайте свойство MyClass a из другого потока, MyClass.a может быть 0 или 1, в зависимости от реализации JavaVM и настроения. Сегодня шансы на "а" быть 1 очень высоки. Но на будущих машинах NUMA это может быть другим. Многие люди не знают об этом и считают, что им не нужно заботиться о многопоточности на этапе инициализации.

Ответ 22

Синхронизация строкового литерала или константы, определяемая строковым литералом, является (потенциально) проблемой, поскольку строковый литерал интернирован и будет использоваться кем-либо из JVM, используя тот же строковый литерал. Я знаю, что эта проблема возникла на серверах приложений и других сценариях "контейнера".

Пример:

private static final String SOMETHING = "foo";

synchronized(SOMETHING) {
   //
}

В этом случае любой, кто использует строку "foo" для блокировки, использует один и тот же замок.

Ответ 23

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

Ответ 24

Самая туманная ошибка, которую я часто делаю, - это забыть синхронизировать, прежде чем вызывать notify() или wait() для объекта.

Ответ 25

Использование локального "нового объекта()" в качестве мьютекса.

synchronized (new Object())
{
    System.out.println("sdfs");
}

Это бесполезно.

Ответ 26

Другой распространенной проблемой "concurrency" является использование синхронизированного кода, когда это необязательно. Например, я все еще вижу программистов, использующих StringBuffer или даже java.util.Vector (как локальные переменные метода).

Ответ 27

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

Ответ 28

Не осознавая, что this во внутреннем классе не является this внешнего класса. Обычно в анонимном внутреннем классе, который реализует Runnable. Корневая проблема заключается в том, что поскольку синхронизация является частью всего Object, проверка статического типа фактически не выполняется. Я видел это по крайней мере дважды на usenet, и он также появляется в Brian Goetz'z Java Concurrency на практике.

Замыкания BGGA не страдают от этого, так как для закрытия нет this (this ссылается на внешний класс). Если вы используете объекты не this в качестве блокировок, то он обходит эту проблему и другие.

Ответ 29

Использование глобального объекта, такого как статическая переменная для блокировки.

Это приводит к очень плохой производительности из-за конкуренции.

Ответ 30

Honesly? До появления java.util.concurrent наиболее распространенной проблемой, с которой я регулярно сталкивался, было то, что я называю "thread-thrashing": приложения, которые используют потоки для concurrency, но порождают слишком много из них и в конечном итоге разбиваются.