С какой проблемой параллелизма вы чаще всего сталкивались в Java?

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

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

L̲̳o̲̳̳n̲̳̳g̲̳̳p̲̳o̲̳̳k̲̳̳e̲̳̳ 31.03.2013 05:56

@Longpoke Сообщение о закрытии объясняет, почему он закрыт. Это не вопрос с конкретным «правильным» ответом, это скорее вопрос опроса / списка. И Stack Overflow не намерен размещать подобные вопросы. Если вы не согласны с этой политикой, вы можете обсудить ее на мета.

Andrzej Doyle 28.05.2013 14:56

Я предполагаю, что сообщество не согласится с тем, что эта статья набирает более 100 просмотров в день! Мне это показалось очень полезным, так как я занимаюсь разработкой инструмента статического анализа, специально разработанного для устранения проблем параллелизма contemplateltd.com/threadsafe. Наличие банка часто встречающихся проблем параллелизма отлично подходит для тестирования и улучшения ThreadSafe.

Craig Manson 04.03.2014 20:35
Контрольный список проверки кода для Java Concurrency переваривает большинство подводных камней, упомянутых в ответах на этот вопрос, в форме, удобной для повседневной проверки кода.
leventov 01.09.2019 19:28
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
В компьютерном программировании биты играют важнейшую роль в представлении и манипулировании данными на двоичном уровне. Побитовые операции...
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Приходилось ли вам сталкиваться с требованиями, в которых вас могли попросить поднять тревогу или выдать ошибку, когда метод Java занимает больше...
Полный курс Java для разработчиков веб-сайтов и приложений
Полный курс Java для разработчиков веб-сайтов и приложений
Получите сертификат Java Web и Application Developer, используя наш курс.
192
4
73 947
49
Перейти к ответу Данный вопрос помечен как решенный

Ответы 49

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

synchronized(foo) {
  foo = ...
}

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

Для этого есть проверка IDEA, которая называется «Синхронизация неоконечного поля, вряд ли будет иметь полезную семантику». Очень хорошо.

Jen S. 20.01.2009 19:28

Ха ... вот это мучительное описание. «маловероятно, что у него будет полезная семантика» лучше было бы описать как «скорее всего, сломано». :)

Alex Miller 20.01.2009 19:45

Я думаю, что это была Bitter Java, у которой это было в ReadWriteLock. К счастью, теперь у нас есть java.util.concurrency.locks, и Дуг немного уверен в этом.

Tom Hawtin - tackline 20.01.2009 19:55

Я тоже часто сталкивался с этой проблемой. Если на то пошло, синхронизируйте только конечные объекты. FindBugs et al. помогите, да.

gimpf 23.01.2009 18:20

это проблема только при назначении? (см. пример @Alex Miller с картой ниже). Будет ли у этого примера карты такая же проблема?

Alex Beardsley 26.01.2009 17:07

@Nalandial - не понимаю, о чем вы спрашиваете. Изменить состояние присваиваемого объекта не проблема; это очень часто. Это проблема - изменить назначение, чтобы разные потоки могли синхронизироваться на разных объектах, что я и имел здесь в виду.

Alex Miller 27.01.2009 19:43

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

reprogrammer 07.09.2010 19:45

@reprogrammer - Нет. Если бы я это сделал, я бы сказал им исправить это. :)

Alex Miller 08.09.2010 01:37

Я просто удалил его из своего программного обеспечения, прочитав это.

boutta 14.01.2011 16:13

Я действительно намеренно использовал эту конструкцию раньше. Что-то вроде synchronized(locks){key=locks.get(key);}synchronized(key){ob‌​jects.put(key,modify‌​(objects.get(key)));‌​}, конечно с соответствующей синхронизацией на objects.

yingted 16.03.2012 06:59

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

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

Ну, иногда, иногда нет. Если бы это было так просто ...

gimpf 23.01.2009 18:28

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

kohlerm 28.01.2009 14:49

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

Обновлено: вторая часть перенесена в отдельный ответ.

Можете ли вы выделить последний вопрос в отдельный ответ? Оставим по 1 на пост. Это два действительно хороших.

Alex Miller 20.01.2009 19:08

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

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

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

Вы подразумеваете, что столкнулись с проблемами более теперь, когда доступен java.util.concurrent?

Andrzej Doyle 21.01.2009 13:02

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

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

Outlaw Programmer 26.01.2009 22:21

К счастью, его очень легко найти ... но это все еще глупая ошибка, которая тратит мое время больше, чем следовало бы :)

Dave Ray 27.01.2009 03:15

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

java.util.ConcurrentModificationException

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

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

Нет, это именно то, что я ищу. Спасибо!

Alex Miller 20.01.2009 19:15

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

Неправильно синхронизация для объектов, возвращаемых 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");

Или лучше использовать ConcurrentHashMap и putIfAbsent.

Tom Hawtin - tackline 20.01.2009 19:43

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

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

reprogrammer 07.09.2010 19:44

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

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

Условия гонки во время метода finalize / release / shutdown / destructor объекта и обычных вызовов.

Из Java я часто выполняю интеграцию с ресурсами, которые необходимо закрыть, такими как COM-объекты или Flash-плееры. Разработчики всегда забывают сделать это должным образом и в конечном итоге получают поток, вызывающий объект, который был отключен.

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

Хотя это определенно проблема, это не ошибка параллелизма, как было указано в исходном вопросе.

Alex Miller 20.01.2009 19:38

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

Да, в самом деле! Изменяемая статика нарушает ограничение потока. Удивительно, но я не нашел ничего об этой ловушке ни в JCiP, ни в CPJ.

Julien Chastang 20.01.2009 20:43

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

gtrak 11.01.2011 00:40

@Gary Дело в том, что они не думают, что занимаются параллельным программированием.

Tom Hawtin - tackline 11.01.2011 00:52

Не осознает java.awt.EventQueue.invokeAndWaitдействует так, как если бы он держал замок (монопольный доступ к потоку отправки событий, EDT). Самое замечательное в тупиках заключается в том, что даже если это случается редко, вы можете получить трассировку стека с помощью jstack или тому подобного. Я видел это в ряде широко используемых программ (исправление проблемы, которая возникает только один раз в Netbeans, должно быть включено в следующий выпуск).

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

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

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

один из тех ответов, на которые я хотел бы дать больше одного балла за

Epaga 04.02.2009 11:45

EDT = поток отправки событий

mjj1409 14.01.2016 04:39

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

Да, и теперь есть хорошие инструменты, чтобы справиться с этим с помощью обработчиков.

Alex Miller 20.01.2009 20:19

Не могли бы вы разместить ссылки на какие-либо статьи или ссылки, которые объясняют это более подробно?

Abhijeet Kashnia 01.11.2010 15:19

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

Парадигма, проблемы которой я начал изучать, когда работал в 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 больше не имеет значения null. Итак, естественное изменение состоит в следующем:

  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, портят все время.

Одноэлементный шаблон enum решает все эти проблемы (см. Комментарии Джоша Блоха по этому поводу). Знание о его существовании должно быть более широко распространено среди Java-программистов.

Robin 21.01.2009 11:59

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

Dov Wasserman 29.01.2009 21:32

Это то, что я использую для отложенной инициализации классов Singleton. Также не требуется синхронизация, поскольку это неявно гарантируется java. class Foo {статический держатель класса {static Foo foo = new Foo (); } static Foo getInstance () {return Holder.foo; }}

Irfan Zulfiqar 27.03.2009 14:17

Ирфан, это называется методом Пью, насколько я помню

Chris R 29.05.2009 00:52

@Robin, не проще ли использовать статический инициализатор? Они всегда гарантированно работают синхронно.

matt b 12.06.2009 22:54

@Matt, не если вы хотите / нуждаетесь в ленивой инициализации. Однако в контексте DI обычно вы просто позволяете контейнеру IoC решать проблему за вас. Статические инициализаторы не поддерживают ленивую инициализацию (когда вы не знаете, хотите ли вы потратить стоимость в первую очередь) или позднюю инициализацию (когда вы не можете гарантировать порядок ряда взаимозависимых инициализаций).

Kirk Wylie 09.08.2009 13:53

Почему бы мне просто не синхронизировать всю getInstance ()?

fhucho 23.10.2011 14:10

@fhucho, потому что методы, которые были синхронизированы, несут большие накладные расходы при каждом доступе к этому конкретному методу. Вам нужно только синхронизировать создание синглтона, чтобы обеспечить создание только одного экземпляра класса. Если вы синхронизируете getInstance (), также будут ненужные накладные расходы при каждом доступе к одноэлементному классу.

Ibrahim Arief 16.11.2011 16:51

@IbrahimArief, спасибо ... вопрос в том, стоит ли повышение производительности потери простоты и ясности кода.

fhucho 16.11.2011 21:14

@fhucho, это, конечно, зависит от вашего варианта использования. Синхронизированные методы обычно в несколько раз медленнее, чем несинхронизированные варианты. Существует миф о том, что вызов синхронизированного метода в 50 раз медленнее, чем вызов несинхронизированного метода, но современные JVM в большинстве случаев снижают это число примерно в 2-10 раз. По-прежнему довольно здоровенный, если вы спросите меня. Проверьте ibm.com/developerworks/java/library/j-threads1/index.html для дальнейшего чтения.

Ibrahim Arief 17.11.2011 08:19

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

Этот укусил меня на этой неделе. У меня был destroy (), но я не смог правильно определить сторону init (), что привело к некоторым гонкам.

Alex Miller 20.01.2009 20:18

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

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 ...
    }
}

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

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

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

Это наверное бесполезно, но процесс синхронизации делает кое-что интересное ... Конечно, создание нового объекта каждый раз - пустая трата времени.

TREE 22.01.2009 18:59

Это не бесполезно. Это барьер памяти без блокировки.

David Roussel 04.05.2011 18:41

@David: единственная проблема - jvm мог его оптимизировать, вообще сняв такую ​​блокировку

yetanothercoder 11.05.2011 14:15

@insighter Я вижу, что ваше мнение разделяют ibm.com/developerworks/java/library/j-jtp10185/index.html Я согласен с тем, что это глупо, поскольку вы не знаете, когда ваш барьер памяти синхронизируется, я просто указывал, что он делает больше, чем ничего.

David Roussel 02.06.2011 20:29

Начиная с Java 5 существует Thread.getUncaughtExceptionHandler, но этот UncaughtExceptionHandler никогда не вызывается, когда используется ExecutorService / ThreadPool. По крайней мере, мне не удалось получить UncaughtExceptionHandler с работающим ExcutorService.

вы можете передать ThreadFactory в ExecutorService / ThreadPool, где в методе newThread вы можете прикрепить UncaughtExceptionHandler к новому каждому новому потоку.

yetanothercoder 11.05.2011 14:41

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

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

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

public class ThreadA implements Runnable {
    private volatile SharedObject obj;

    public void run() {
        while (true) {
            obj = new SharedObject();
            obj.setValue("Hallo");
        }
    }

    public SharedObject getObj() {
        return obj;
    }
}

Проблема, которую я пытаюсь указать здесь (среди прочего), заключается в том, что сброс объекта SharedObject происходит до установки значения «Hallo». Это означает, что потребитель getObj () может получить экземпляр, в котором getValue () возвращает null.

public class ThreadB implements Runnable {
    ThreadA a = null;

    public ThreadB(ThreadA a) {
        this.a = a;
    }

    public void run() {
        while (true) {
            try {
                System.out.println("SharedObject: " + a.getObj().getVal());
                Thread.sleep(50);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

public class SharedObject {
    private String val = null;

    public SharedObject() {
    }

    public String getVal() {
        return val;
    }

    public void setVal(String val) {
        this.val = val;
    }
}

Было бы потрясающе, если бы здесь было короткое объяснение ... здесь, кажется, есть более чем одна проблема.

Alex Miller 20.01.2009 22:53
Ответ принят как подходящий

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

class MyThread extends Thread {
  private boolean stop = false;

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

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

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

Отличное решение - сделать переменную экземпляра остановки AtomicBoolean. Он решает все проблемы энергонезависимости, защищая вас от проблем с JMM.

Kirk Wylie 21.01.2009 02:59

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

Karl 29.01.2009 05:33

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

Kutzi 07.02.2009 20:07

И если поток, который нужно остановить, делает что-то критическое (например, тратит деньги вашей компании), каждая секунда, на которой поток не останавливается, может рассчитывать.

Kutzi 07.02.2009 20:08

Это хуже, чем «на несколько минут» - можно НИКОГДА этого не увидеть. В рамках модели памяти JVM разрешено оптимизировать while (! Stop) до while (true), а затем вы облажаетесь. Это может произойти только на некоторых виртуальных машинах, только в режиме сервера, только когда JVM перекомпилируется после x итераций цикла и т. д. Ой!

Cowan 11.02.2009 09:15

Почему вы хотите использовать AtomicBoolean вместо volatile boolean? Я разрабатываю версию 1.4+, есть ли подводные камни при простом объявлении volatile?

Pool 30.03.2009 03:26

Ник, я думаю, это потому, что атомарный CAS обычно даже быстрее, чем volatile. Если вы разрабатываете для 1.4, ваш единственный безопасный вариант IMHO - использовать synchronized as volatile в 1.4, не имеет сильных гарантий барьера памяти, как в Java 5.

Kutzi 05.04.2009 17:04

Ух ты. Это удивительная вещь. Мне нужно пойти и исправить некоторые ошибки сейчас.

Chris R 29.05.2009 00:46

Я никогда не видел этого на практике. Есть ли способ заставить его потерпеть неудачу?

Jon 19.06.2010 14:56

Может ли кто-нибудь объяснить остальным, почему это не работает?

Thomas Ahle 23.08.2010 01:00

@Thomas: это из-за модели памяти Java. Вы должны прочитать об этом, если хотите узнать об этом подробно (Java Concurrency in Practice, Брайан Гетц, например, хорошо это объясняет). Вкратце: если вы не используете ключевые слова / конструкции для синхронизации памяти (например, volatile, synchronized, AtomicXyz, но также и когда поток завершен), один поток не имеет никаких гарантий, чтобы увидеть изменения, внесенные в любое поле, выполненное другим потоком

Kutzi 24.08.2010 18:03

@KirkWylie Да, AtomicBoolean, и разве он не должен быть "окончательным"? Я думаю, что это единственный способ безопасно назначить переменную в многопоточном режиме.

Adrien 18.03.2014 16:58

@Adrien: Нет смысла делать это окончательным, потому что при этом вы не можете изменить значение переменной.

Rachel 05.09.2014 23:25

@Rachel "final" может использоваться в некоторых случаях, иначе AtomicBoolean небезопасно опубликовать, см. stackoverflow.com/a/6457285/965134

Adrien 07.09.2014 11:42

@Kutzi Мне бы хотелось увидеть ссылку на ваше заявление о том, что атомарный CAS быстрее, чем volatile. Я использую классы Atomic *, когда мне нужно увеличить значение или иным образом нужно сравнить и установить, но для типа кода, предоставленного OP, я бы использовал volatile

NamshubWriter 07.06.2015 20:01

@NamshubWriter AFAIK, это потому, что CAS может быть сопоставлен с атомарными операциями CAS на базовой машине, тогда как volatile не может быть (во всех случаях). По крайней мере, это было сказано, когда java.lang.atomic был представлен несколько лет назад, так что это больше не должно быть правдой. Просто погуглите, и я думаю, вы найдете много ссылок.

Kutzi 08.06.2015 20:35

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

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

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

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

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

class MyClass {
    public int a = 1;
}

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

Я нахожу это слегка удивительным, но я знаю, что ты умный чувак, Тим, поэтому я возьму это без ссылки. :) Однако, если бы a было окончательным, это не было бы проблемой, верно? Тогда вы будете связаны семантикой окончательного замораживания во время построения?

Alex Miller 20.01.2009 22:49

Я до сих пор нахожу в JMM то, что меня удивляет, поэтому я не доверяю мне, но я почти уверен в этом. См. Также cs.umd.edu/~pugh/java/memoryModel/…. Если бы поле было окончательным, это не было бы проблемой, оно было бы видно после фазы инициализации.

Tim Jansen 21.01.2009 00:59

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

ReneS 12.03.2009 05:59

MyClass.a указывает на статический доступ, а «a» не является статическим членом MyClass. Помимо этого, как говорится в «ReneS», это проблема только в том случае, если утечка ссылки на незавершенный объект, например, добавление «this» к некоторой внешней карте в конструкторе.

Markus Jevring 25.01.2011 10:54

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

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

ReneS 12.03.2009 06:01

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

Пример:

private static final String SOMETHING = "foo";

synchronized(SOMETHING) {
   //
}

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

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

Kirk Wylie 21.01.2009 03:02

Правильно, поэтому я специально использовал буквальную строковую константу, которая гарантированно будет интернирована.

Alex Miller 21.01.2009 07:48

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

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

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

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

Это одна из причин, почему я предпочитаю private static final Object LOCK = new Object ();

Andrzej Doyle 21.01.2009 12:59

Обожаю - ох, это мерзко :)

Thorbjørn Ravn Andersen 21.01.2009 19:40

Это хороший вариант для Java Puzzlers 2.

Dov Wasserman 29.01.2009 21:30

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

Jared 04.02.2009 18:29

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

Jared 04.02.2009 18:30

+1 приватный статический финальный объект LOCK = новый объект ();

parkr 18.02.2009 15:08

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

matt b 12.06.2009 22:53

Тебе больно из-за «может быть». Пока строка не интернирована, вы получаете другой экземпляр, но как только он интернирован, вы действительно получаете точно такой же экземпляр (по oid). Кроме того, вы не можете контролировать, когда строка интернируется - JVM вполне может это сделать для вас, как только он будет создан. Это точно такая же ошибка, которая возникает, когда вы используете == вместо .equals в строке - и это очень известный случай сбоя.

Jared 15.06.2009 20:15

@Jared: «пока строка не интернирована» не имеет смысла. Струны не «интернируются» волшебным образом. String.intern () возвращает другой объект, если у вас еще нет канонического экземпляра указанной String. Кроме того, интернируются все литеральные строки и константные выражения со строковыми значениями. Всегда. См. Документацию для String.intern () и §3.10.5 JLS.

Laurence Gonsalves 06.08.2009 08:21

«Однако; поскольку строки интернированы в Java» на самом деле должно быть «Однако; потому что строковые литералы интернированы в Java». (Не все строки интернированы, только литералы) Конечно, "LOCK" интернировано, что является корнем этой проблемы.

djb 07.09.2010 17:14

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

reprogrammer 07.09.2010 19:43

Я видел простой synchronized("LOCK") { this.work(); }: P

Peter Lawrey 06.05.2011 11:44

Это прекрасный пример сильного воздействия неразложимости.

L̲̳o̲̳̳n̲̳̳g̲̳̳p̲̳o̲̳̳k̲̳̳e̲̳̳ 31.03.2013 05:58

Похоже, это также должно происходить в некоторых случаях с классами-оболочками: если упаковываемое значение p равно true, false, byte, char в диапазоне от \ u0000 до \ u007f или int или короткое число от -128 до 127 , тогда пусть r1 и r2 будут результатами любых двух упаковочных преобразований p. Всегда бывает так, что r1 == r2.

Danny Dan 04.07.2017 19:26

Все потоки остаются занятыми.

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

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

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

1) Распространенная ошибка, с которой я столкнулся, связана с повторением синхронизированного класса Collection. Перед получением итератора и во время итерации требуется синхронизировать вручную.

2) Другая ошибка состоит в том, что в большинстве учебников создается впечатление, что создание потокобезопасного класса - это просто вопрос добавления synchronized для каждого метода. Само по себе это не является гарантией - это только защитит целостность конкретного класса, но результаты все равно могут быть неопределенными.

3) Помещение слишком большого количества затратных по времени операций в синхронизированный блок часто приводит к очень плохой производительности. К счастью, шаблон Future в пакете параллелизма может спасти ситуацию.

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

5) Использование нескольких объектов синхронизации требует осторожности.

Помощь в реализации акторов в функциональной Java и тестирование миллионов потоков на многоядерных машинах.

while(true)
{
   if (...)
     break

   doStuff()
}

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

А именно, если этот блок не выйдет, приложение и, возможно, даже система заблокируются и умрут. Просто из-за простого while(fantasy_land)...if (...) break.

Это проблема, но я не считаю это ошибкой параллелизма?

Alex Miller 21.01.2009 16:36

Мои два цента на попытки избежать проблем с синхронизацией с самого начала - обратите внимание на следующие проблемы / запахи:

  1. При написании кода всегда знать, в какой теме ты.
  2. При разработке класса или API для повторного использования всегда спросите себя, должен ли код быть потокобезопасным. Лучше принять обдуманное решение и задокументировать, что ваш модуль нет потокобезопасен, чем вводить неразумную синхронизацию с возможностью тупиковой ситуации.
  3. Вызов new Thread() - это запах. Вместо этого используйте специализированные ExecutorServices, которые заставят вас задуматься об общей концепции потоковой передачи вашего приложения (см. 1) и побудят других следовать ей.
  4. Знайте и используйте классы библиотеки (например, AtomicBooleanи другие, синхронизированные коллекции и т. д.). Опять же: принимайте сознательное решение о том, важна ли безопасность потоков в данном контексте, а не просто используйте их вслепую.

Конечно, это хороший совет, но он не указывает на ошибку параллелизма Java.

Alex Miller 21.01.2009 16:41

Запуск Java RMI вызывает выполнение фоновой задачи, которая заставляет сборщик мусора запускаться каждые 60 секунд. Само по себе это может быть хорошо, однако может оказаться, что сервер RMI был запущен не вами напрямую, а с помощью фреймворка / инструмента, который вы используете (например, JRun). И RMI на самом деле может ни для чего не использоваться.

Конечный результат - вызов System.gc () один раз в минуту. В сильно загруженной системе вы увидите следующий вывод в своих журналах - 60 секунд активности, за которыми следует длинная пауза gc, за которой следует 60 секунд активности, за которой следует длинная пауза gc. Это фатально для производительности.

Решение состоит в том, чтобы отключить явный gc с помощью -XX: + DisableExplicitGC

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

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

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

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

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

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

хороший пример. Думаю, я бы охарактеризовал это в более общем плане как «состав атомарных операций не атомарен». (См. Еще один простой пример в volatile field ++)

Alex Miller 21.01.2009 21:40

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

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

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

public class MyReader implements Runnable {

  private final CountDownLatch done = new CountDownLatch(1);
  private volatile isOkToRun = true;

  public void run() {
    while (isOkToRun) {
       sendMessage(getMessaage());
    }
    done.countDown();
  }

  public void stop() {
    isOkToRun = false;
    done.await();
  }

}

Идея stop () заключается в том, что он не возвращался, пока поток не завершился, поэтому, когда он вернулся, система была в известном состоянии. Это нормально, если только sendMessage () не приведет к вызову stop (), где он будет ждать вечно. Пока stop () никогда не вызывается из Runnable, все будет работать так, как вы ожидаете. Однако в большом приложении активность потока Runnable может быть неочевидной!

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

или вы можете получить поток петлителя и не вызывать ожидание, когда остановка вызывается из того же потока

ratchet freak 31.07.2011 23:13

Пока я не взял курс с Брайаном Гетцем, я не понимал, что несинхронизированный 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;  
    }
}

В более поздних JVM (я думаю, 1.5 и новее) использование volatile также исправит это.

James Schek 28.01.2009 02:18

Не обязательно. volatile дает вам последнее значение, поэтому он предотвращает возврат 1 навсегда, но не обеспечивает блокировки. Это близко, но не совсем то же самое.

John Russell 06.03.2009 00:29

@JohnRussell Я думал, что volatile гарантирует, что отношения случатся раньше. разве это не "запирание"? «Запись в изменчивую переменную (§8.3.1.4) v синхронизируется со всеми последующими чтениями v любым потоком (где последующее определяется в соответствии с порядком синхронизации)».

Shawn 01.02.2012 22:04

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

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

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

public class UserService {

    private String userName;

    public String getUserName() {
        return userName;
    }

    public void login(String name) {
        this.userName = name; 
        doLogin();
    }

    private void doLogin() {
        userDao.login(getUserName());
    }

    public void delete(String name) {
        this.userName = name; 
        doDelete();
    }

    private void doDelete() {
        userDao.delete(getUserName());
    }

}

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

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

Я пытался использовать методы wait () и notifyAll (), и вот как я их использовал и попал в ад.

Поток1

Object o1 = new Object();

synchronized(o1) {
    o1.wait();
}

И в другой ветке. Резьба - 2

Object o2 = new Object();

synchronized(o2) {
    o2.notifyAll();
}

Thread1 будет ждать o1, а Thread2, который должен был вызвать o1.notifyAll (), вызывает o2.notifyAll (). Поток 1 никогда не проснется.

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

Object o2 = new Object();

synchronized(o2) {
    notifyAll();
}

Это вызовет исключение IllegalMonitorStateException, поскольку поток, вызвавший notifyAll (), вызвал notifyAll () с использованием этого объекта, но не является владельцем объекта блокировки this. Но текущий поток является владельцем объекта o2 lock.

Рекомендуется использовать o1.wait () в состоянии while с логическим флагом, чтобы предотвратить любые ложные пробуждения.

Prabhash Rathore 31.01.2016 01:11

Обновление компонента пользовательского интерфейса Swing (обычно индикатора выполнения) в рабочем потоке, а не в потоке Swing (конечно, следует использовать SwingUtilities.invokeLater(Runnable), но если вы забудете это сделать, то ошибка может появиться довольно долго).

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

Очевидно, вы не должны делать это в первую очередь (используйте ConcurrentHashMap или Collections.synch ... wrapper), но, похоже, это тот, который всегда проходит через сеть и вызывает застревание правильного потока, полностью сломанную систему, обычно из-за чтобы служебный класс, содержащий такую ​​карту, находился на несколько уровней ниже по стеку, и никто об этом не думал.

Попробуйте этот код ..

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 ...
    }
}

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

Tijuana 26.03.2013 16:29

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

Другие вопросы по теме