С какой наиболее частой проблемой параллелизма вы столкнулись в Java? [закрыто]

192

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

Алекс Миллер
источник
16
Почему это закрыто? Это полезно как для других программистов, просящих параллелизма в Java, так и для того, чтобы иметь представление о том, какие классы дефектов параллелизма чаще всего наблюдаются другими разработчиками Java.
L̲̳o̲̳̳n̲̳̳g̲̳̳p̲̳o̲̳̳k̲̳̳e̲̳̳
@Longpoke Сообщение о закрытии объясняет, почему оно закрыто. Это не вопрос с конкретным «правильным» ответом, это скорее вопрос опроса / списка. А Stack Overflow не намерен принимать подобные вопросы. Если вы не согласны с этой политикой, вы можете обсудить ее в мета- версии .
Анджей Дойл
7
Я предполагаю, что сообщество не согласно, поскольку эта статья получает более 100 просмотров в день! Я нашел это очень полезным, так как я вовлечен в разработку инструмента статического анализа, специально разработанного для устранения проблем параллелизма contemplateltd.com/threadsafe . Наличие банка часто встречающихся проблем с параллелизмом отлично подходит для тестирования и улучшения ThreadSafe.
Крейг Мэнсон
Контрольный список проверки кода для Java Concurrency переваривает большинство подводных камней, упомянутых в ответах на этот вопрос, в форме, удобной для ежедневных проверок кода.
Левентов

Ответы:

125

Самая распространенная проблема параллелизма, с которой я столкнулся, это отсутствие понимания того, что поле, написанное одним потоком, не гарантированно будет видно другому потоку. Распространенное применение этого:

class MyThread extends Thread {
  private boolean stop = false;

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

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

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

Kutzi
источник
9
Отличным решением для этого является сделать переменную экземпляра stop AtomicBoolean. Он решает все проблемы энергонезависимой, защищая вас от проблем JMM.
Кирк Уайли
39
Это хуже, чем «на несколько минут» - вы никогда не увидите это. В соответствии с моделью памяти, JVM разрешено оптимизировать время (! Stop) в то время как (true), а затем вы попадаете. Это может произойти только на некоторых виртуальных машинах, только в режиме сервера, только когда JVM перекомпилируется после x итераций цикла и т. Д. Ой!
Коуэн
2
Почему вы хотите использовать AtomicBoolean вместо volatile boolean? Я занимаюсь разработкой для версии 1.4+, так есть ли какие-либо подводные камни с объявлением volatile?
Бассейн
2
Ник, я думаю, это потому, что атомный CAS обычно даже быстрее, чем энергозависимый. Если вы разрабатываете для 1.4, ваш единственный безопасный вариант, IMHO, это использовать синхронизированный как volatile в 1.4, который не имеет такого сильного барьера памяти, как в Java 5.
Kutzi
5
@ Томас: это из-за модели памяти Java. Вы должны прочитать об этом, если вы хотите узнать это подробно (например, Java Concurrency in Practice от Brian Goetz это хорошо объясняет). Короче говоря: если вы не используете ключевые слова / конструкции синхронизации памяти (такие как volatile, synchronized, AtomicXyz, но также и когда поток завершен), один поток не имеет
никакой
179

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

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

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

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

Джаред
источник
63
Это одна из причин, почему я предпочитаю закрытый статический final Object Object LOCK = new Object ();
Анджей Дойл
17
Я люблю это - о, это противно :)
Thorbjørn Ravn Andersen
7
Это хороший вариант для Java Puzzlers 2.
Дов Вассерман
12
На самом деле ... это действительно заставляет меня хотеть, чтобы компилятор отказался позволить вам синхронизироваться на строке. Учитывая интернирование String, нет ни одного случая, когда это было бы «хорошо (тм)».
Джаред
3
@Jared: «пока строка не интернирована» не имеет смысла. Струны волшебным образом не «становятся» интернированными. String.intern () возвращает другой объект, если у вас уже нет канонического экземпляра указанной строки. Кроме того, все литеральные строки и строковые константные выражения интернированы. Всегда. См. Документы для String.intern () и §3.10.5 JLS.
Лоуренс Гонсалвес
65

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

synchronized(foo) {
  foo = ...
}

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

Алекс Миллер
источник
19
Для этого существует проверка IDEA, которая называется «Синхронизация в неконечном поле, которая вряд ли будет иметь полезную семантику». Очень хорошо.
Джен С.
8
Ха ... теперь это замученное описание. «вряд ли иметь полезную семантику» можно лучше охарактеризовать как «скорее всего нарушенная». :)
Алекс Миллер
Я думаю, что это была Bitter Java, которая имела это в своем ReadWriteLock. К счастью, у нас теперь есть java.util.concurrency.locks, и Даг немного больше относится к делу.
Том Хотин - tackline
Я также часто видел эту проблему. Только для синхронизации на конечных объектах. FindBugs и соавт. помогите да
Gimpf
это только проблема во время назначения? (см. пример @Alex Miller ниже с картой) Будет ли этот пример карты иметь такую ​​же проблему?
Алекс Бердсли
50

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

Алекс Миллер
источник
Знаете ли вы какой-либо проект с открытым исходным кодом, содержащий эту ошибку в какой-либо версии? Я ищу конкретные примеры этой ошибки в программном обеспечении реального мира.
репрограммист
48

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

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

...

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

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

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

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

map.putIfAbsent("foo", "bar");
Dave Ray
источник
6
Или лучше использовать ConcurrentHashMap и putIfAbsent.
Том Хотин - Tackline
47

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

Парадигма, с которой я начал изучать проблемы, связанные с работой в 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 Memory Model не поддерживает это. Вам нужно объявить s_instance как volatile, чтобы он работал, и даже тогда он работает только на Java 5.

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

Кирк Уайли
источник
7
Шаблон enum singleton решает все эти проблемы (см. Комментарии Джоша Блоха). Знание о его существовании должно быть более распространено среди программистов на Java.
Робин
Мне все еще предстоит столкнуться с одним случаем, когда ленивая инициализация синглтона была действительно уместной. И если это так, просто объявите метод синхронизированным.
Дов Вассерман
3
Это то, что я использую для отложенной инициализации классов Singleton. Также не требуется синхронизация, так как это гарантируется Java неявно. class Foo {статический класс Holder {static Foo foo = new Foo (); } static Foo getInstance () {return Holder.foo; }}
Ирфан Зульфикар
Ирфан, это называется метод Пью, насколько я помню
Крис Р
@ Робин, не проще ли просто использовать статический инициализатор? Те всегда гарантированно работают синхронно.
Мэтт б
37

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

java.util.ConcurrentModificationException

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

List<String> list = new ArrayList<String>(Arrays.asList("a", "b", "c"));
for (String string : list) { list.remove(string); }
Фабиан Стиг
источник
Нет, это именно то, что я ищу. Спасибо!
Алекс Миллер
30

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

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

Например, во второй строке выше, toArray()и size()методы являются поточно в своем собственном праве, но size()оцениваются отдельно от toArray(), и замок в списке не проводятся между этими двумя вызовами.

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

Ник
источник
5
хороший пример. Я думаю, что записал бы это более широко, поскольку «состав атомных операций не атомарен». (См. Volatile field ++ для другого простого примера)
Алекс Миллер
29

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

Эрик Берк
источник
один из тех ответов, за которые я хотел бы дать более одного очка
Epaga
2
EDT =
поток рассылки
28

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

 synchronized (obj) {
     while (<condition does not hold>) {
         obj.wait();
     }
     // do stuff based on condition being true
 }
Алекс Миллер
источник
26

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

Эрик Берк
источник
Да, и сейчас есть хорошие инструменты для обработки этого с помощью обработчиков.
Алекс Миллер
3
Не могли бы вы опубликовать ссылки на какие-либо статьи или ссылки, которые объясняют это более подробно?
Абхиджит Кашня
22

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

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;  
    }
}
Джон Рассел
источник
5
В более поздних версиях JVM (я думаю, 1.5 и более поздние) использование volatile также исправит это.
Джеймс Шек
2
Не обязательно. volatile дает вам последнее значение, поэтому оно предотвращает возврат 1 навсегда, но не обеспечивает блокировку. Его близко, но не совсем то же самое.
Джон Рассел
1
@JohnRussell Я думал, что изменчивость гарантирует отношения до того. разве это не «блокировка»? «Запись в энергозависимую переменную (§8.3.1.4) v синхронизирует со всеми последующими чтениями v любым потоком (где последующие определены в соответствии с порядком синхронизации)».
Шон
15

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

Том Хотин - Tackline
источник
3
Да, в самом деле! Изменяемая статика нарушает ограничение потока. Удивительно, но я никогда не нашел ничего об этой ловушке ни в JCiP, ни в CPJ.
Жюльен Частанг
Я надеюсь, что это очевидно для людей, занимающихся параллельным программированием. Глобальное состояние должно быть первым местом для проверки безопасности потока.
gtrak
1
@ Гэри Дело в том, что они не думают, что занимаются параллельным программированием.
Том Хотин - tackline
15

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

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

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

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

Скотт Бэйл
источник
13

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

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

Эдди
источник
11

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

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

Алекс Миллер
источник
11

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

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 ...
    }
}
Людвиг Венсауэр
источник
10

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

Все больше людей должны использовать аннотации параллелизма (например, @ThreadSafe, @GuardedBy и т. Д.), Описанные в книге Гетца.

Нил Бартлетт
источник
9

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

РЕДАКТИРОВАТЬ: перенес второй части в отдельный ответ.

Дейв Рэй
источник
Можете ли вы разделить последний в отдельный ответ? Давайте сохраним это 1 за пост. Это два действительно хороших.
Алекс Миллер
9

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

грейджер
источник
8

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

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");

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

Стив Маклеод
источник
8

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

Пример:

private static final String SOMETHING = "foo";

synchronized(SOMETHING) {
   //
}

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

Алекс Миллер
источник
Потенциально он заблокирован. Проблема заключается в том, что семантика в случае, когда строки интернированы, не определена (или, IMNSHO, недоопределена). Постоянная времени компилятора "foo" интернирована, "foo", поступающая из сетевого интерфейса, интернируется, только если вы сделаете это так.
Кирк Уайли
Правильно, именно поэтому я специально использовал константу литеральной строки, которая гарантированно будет интернирована.
Алекс Миллер
8

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

class MyClass {
    public int a = 1;
}

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

Тим Янсен
источник
Я нахожу это немного удивительным, но я знаю, что ты умный чувак Тим, поэтому я возьму это без справки. :) Однако, если бы он был окончательным, это не было бы проблемой, правильно? Вы бы тогда были связаны с окончательной заморозкой семантики во время строительства?
Алекс Миллер
Я все еще нахожу в JMM вещи, которые меня удивляют, поэтому я бы не стал доверять мне, но я в этом уверен. Смотрите также cs.umd.edu/~pugh/java/memoryModel/… . Если поле было окончательным, это не было бы проблемой, тогда это было бы видно после фазы инициализации.
Тим Янсен
2
Это только проблема, если ссылка на только что созданный экземпляр используется еще до того, как конструктор вернул / завершил работу. Например, класс регистрируется во время построения в публичном пуле, и другие потоки начинают получать к нему доступ.
ReneS
3
MyClass.a указывает на статический доступ, а «a» не является статическим членом MyClass. Кроме того, это как состояния 'ReneS', это проблема только в случае утечки ссылки на незавершенный объект, как, например, добавление 'this' к некоторой внешней карте в конструкторе.
Маркус Йевринг
7

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

Дэйв Рэй
источник
8
В отличие от большинства проблем с параллелизмом, разве это не легко найти? По крайней мере, вы получаете IllegalMonitorStateException здесь ...
Outlaw Programmer
К счастью, это очень легко найти ... но это все еще глупая ошибка, которая тратит мое время больше, чем следовало бы :)
Дэйв Рэй
7

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

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

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

Людвиг Венсауэр
источник
2
Это, вероятно, бесполезно, но акт синхронизации вообще делает некоторые интересные вещи ... Конечно, каждый раз создание нового Объекта - пустая трата времени.
TREE
4
Это не бесполезно. Это барьер памяти без замка.
Дэвид Руссел
1
@David: единственная проблема - jvm может оптимизировать ее, вообще сняв такую ​​блокировку
butanothercoder
@insighter Я вижу, что ваше мнение разделяют ibm.com/developerworks/java/library/j-jtp10185/index.html Я согласен, что это глупо, так как вы не знаете, когда ваш барьер для синхронизации будет синхронизироваться, я просто указывал, что делает больше, чем ничего.
Дэвид Руссел
7

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

Kutzi
источник
1
Это не проблема, но излишняя, поскольку она сообщает JVM о синхронизации данных с глобальной памятью и, следовательно, может работать плохо на многопроцессорных системах, даже если никто не использует блок синхронизации одновременно.
ReneS
6

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

Брендан Кэшман
источник
5

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

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

Том Хотин - Tackline
источник
3

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

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

kohlerm
источник
Ну, иногда, иногда нет. Если бы это было так просто ...
gimpf
Предполагая, что многопоточность помогает вообще повысить производительность для данной проблемы, она всегда снижает производительность, как только несколько потоков получают доступ к коду, защищенному блокировкой.
kohlerm
3

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

Брайан Клаппер
источник
Вы намекаете на то, что теперь у вас больше проблем, когда java.util.concurrent доступен?
Анджей Дойл