Это своего рода опрос об общих проблемах параллелизма в Java. Примером может быть классическая тупиковая ситуация или состояние гонки или, возможно, ошибки потоковой передачи EDT в Swing. Меня интересует не только широкий спектр возможных проблем, но и наиболее распространенные проблемы. Поэтому, пожалуйста, оставьте один конкретный ответ об ошибке параллелизма Java для каждого комментария и проголосуйте за него, если увидите тот, с которым столкнулись.
@Longpoke Сообщение о закрытии объясняет, почему он закрыт. Это не вопрос с конкретным «правильным» ответом, это скорее вопрос опроса / списка. И Stack Overflow не намерен размещать подобные вопросы. Если вы не согласны с этой политикой, вы можете обсудить ее на мета.
Я предполагаю, что сообщество не согласится с тем, что эта статья набирает более 100 просмотров в день! Мне это показалось очень полезным, так как я занимаюсь разработкой инструмента статического анализа, специально разработанного для устранения проблем параллелизма contemplateltd.com/threadsafe. Наличие банка часто встречающихся проблем параллелизма отлично подходит для тестирования и улучшения ThreadSafe.




Одна из классических проблем - это изменение синхронизируемого объекта во время синхронизации с ним:
synchronized(foo) {
foo = ...
}
Затем другие параллельные потоки синхронизируются на другом объекте, и этот блок не обеспечивает ожидаемого взаимного исключения.
Для этого есть проверка IDEA, которая называется «Синхронизация неоконечного поля, вряд ли будет иметь полезную семантику». Очень хорошо.
Ха ... вот это мучительное описание. «маловероятно, что у него будет полезная семантика» лучше было бы описать как «скорее всего, сломано». :)
Я думаю, что это была Bitter Java, у которой это было в ReadWriteLock. К счастью, теперь у нас есть java.util.concurrency.locks, и Дуг немного уверен в этом.
Я тоже часто сталкивался с этой проблемой. Если на то пошло, синхронизируйте только конечные объекты. FindBugs et al. помогите, да.
это проблема только при назначении? (см. пример @Alex Miller с картой ниже). Будет ли у этого примера карты такая же проблема?
@Nalandial - не понимаю, о чем вы спрашиваете. Изменить состояние присваиваемого объекта не проблема; это очень часто. Это проблема - изменить назначение, чтобы разные потоки могли синхронизироваться на разных объектах, что я и имел здесь в виду.
Вы знаете какой-либо проект с открытым исходным кодом, содержащий эту ошибку в какой-либо версии? Я ищу конкретные примеры этой ошибки в реальном программном обеспечении.
@reprogrammer - Нет. Если бы я это сделал, я бы сказал им исправить это. :)
Я просто удалил его из своего программного обеспечения, прочитав это.
Я действительно намеренно использовал эту конструкцию раньше. Что-то вроде synchronized(locks){key=locks.get(key);}synchronized(key){objects.put(key,modify(objects.get(key)));}, конечно с соответствующей синхронизацией на objects.
Использование глобального объекта, такого как статическая переменная, для блокировки.
Это приводит к очень плохой производительности из-за разногласий.
Ну, иногда, иногда нет. Если бы это было так просто ...
Предполагая, что многопоточность вообще помогает повысить производительность для данной проблемы, она всегда снижает производительность, как только более одного потока обращается к коду, защищенному блокировкой.
Моей самой большой проблемой всегда были тупики, особенно вызванные слушателями, которые запускаются с удерживаемой блокировкой. В этих случаях действительно легко получить обратную блокировку между двумя потоками. В моем случае между симуляцией, работающей в одном потоке, и визуализацией симуляции, работающей в потоке пользовательского интерфейса.
Обновлено: вторая часть перенесена в отдельный ответ.
Можете ли вы выделить последний вопрос в отдельный ответ? Оставим по 1 на пост. Это два действительно хороших.
Несбалансированная синхронизация, особенно с картами, кажется довольно распространенной проблемой. Многие люди считают, что достаточно синхронизации при размещении на карте (не ConcurrentMap, а, скажем, HashMap) и отсутствия синхронизации при получении. Однако это может привести к бесконечному циклу во время повторного хеширования.
Однако та же проблема (частичная синхронизация) может возникнуть везде, где у вас есть общее состояние с чтением и записью.
Честно? До появления java.util.concurrent самой распространенной проблемой, с которой я обычно сталкивался, было то, что я называю «перебоями потоков»: приложения, которые используют потоки для параллелизма, но порождают их слишком много и заканчивают «перебоями».
Вы подразумеваете, что столкнулись с проблемами более теперь, когда доступен java.util.concurrent?
Самая глупая ошибка, которую я часто делаю, - это забыть выполнить синхронизацию перед вызовом notify () или wait () для объекта.
В отличие от большинства проблем с параллелизмом, разве эту проблему не легко найти? По крайней мере, здесь вы получите исключение IllegalMonitorStateException ...
К счастью, его очень легко найти ... но это все еще глупая ошибка, которая тратит мое время больше, чем следовало бы :)
Хотя, вероятно, это не совсем то, о чем вы просите, самая частая проблема, связанная с параллелизмом, с которой я сталкивался (вероятно, потому, что она возникает в обычном однопоточном коде), - это
java.util.ConcurrentModificationException
вызвано такими вещами, как:
List<String> list = new ArrayList<String>(Arrays.asList("a", "b", "c"));
for (String string : list) { list.remove(string); }
Нет, это именно то, что я ищу. Спасибо!
Несколько объектов, которые защищены блокировкой, но к которым обычно обращаются последовательно. Мы столкнулись с парой случаев, когда блокировки получаются разным кодом в разном порядке, что приводит к тупиковой ситуации.
Неправильно синхронизация для объектов, возвращаемых 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.
Распространенной проблемой является использование таких классов, как Calendar и SimpleDateFormat, из нескольких потоков (часто путем их кеширования в статической переменной) без синхронизации. Эти классы не являются потокобезопасными, поэтому многопоточный доступ в конечном итоге вызовет странные проблемы с несогласованным состоянием.
Вы знаете какой-либо проект с открытым исходным кодом, содержащий эту ошибку в какой-либо версии? Я ищу конкретные примеры этой ошибки в реальном программном обеспечении.
Забывание 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-плееры. Разработчики всегда забывают сделать это должным образом и в конечном итоге получают поток, вызывающий объект, который был отключен.
Самая большая проблема, с которой я столкнулся, - это разработчики, добавляющие поддержку многопоточности в последнюю очередь.
Хотя это определенно проблема, это не ошибка параллелизма, как было указано в исходном вопросе.
Думая, что вы пишете однопоточный код, но используете изменяемую статику (включая синглтоны). Очевидно, они будут разделены между потоками. Это случается на удивление часто.
Да, в самом деле! Изменяемая статика нарушает ограничение потока. Удивительно, но я не нашел ничего об этой ловушке ни в JCiP, ни в CPJ.
Я надеюсь, что это очевидно для людей, занимающихся параллельным программированием. Глобальное состояние должно быть первым местом, где нужно проверить потокобезопасность.
@Gary Дело в том, что они не думают, что занимаются параллельным программированием.
Не осознает java.awt.EventQueue.invokeAndWaitдействует так, как если бы он держал замок (монопольный доступ к потоку отправки событий, EDT). Самое замечательное в тупиках заключается в том, что даже если это случается редко, вы можете получить трассировку стека с помощью jstack или тому подобного. Я видел это в ряде широко используемых программ (исправление проблемы, которая возникает только один раз в Netbeans, должно быть включено в следующий выпуск).
Не осознавая, что this внутреннего класса не является this внешнего класса. Обычно в анонимном внутреннем классе, реализующем Runnable. Основная проблема в том, что поскольку синхронизация является частью всех Object, статическая проверка типов фактически отсутствует. Я видел это по крайней мере дважды в usenet, и это также появляется в Brian Goetz'z Java Concurrency in Practice.
Замыкания BGGA от этого не страдают, поскольку для замыкания нет this (this ссылается на внешний класс). Если вы используете в качестве блокировок объекты, не относящиеся к this, тогда можно обойти эту и другие проблемы.
Самая распространенная ошибка, которую мы видим там, где я работаю, - это то, что программисты выполняют длительные операции, такие как вызовы сервера, на EDT, блокируя графический интерфейс на несколько секунд и заставляя приложение не отвечать.
один из тех ответов, на которые я хотел бы дать больше одного балла за
EDT = поток отправки событий
Еще одна распространенная ошибка - плохая обработка исключений. Когда фоновый поток генерирует исключение, если вы не обрабатываете его должным образом, вы можете вообще не увидеть трассировку стека. Или, возможно, ваша фоновая задача перестает выполняться и никогда не запускается снова, потому что вы не смогли обработать исключение.
Да, и теперь есть хорошие инструменты, чтобы справиться с этим с помощью обработчиков.
Не могли бы вы разместить ссылки на какие-либо статьи или ссылки, которые объясняют это более подробно?
Двойная проверка блокировки. В общем и целом.
Парадигма, проблемы которой я начал изучать, когда работал в 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-программистов.
Мне еще предстоит встретить ни одного случая, когда ленивая инициализация синглтона была действительно уместной. И если это так, просто объявите метод синхронизированным.
Это то, что я использую для отложенной инициализации классов Singleton. Также не требуется синхронизация, поскольку это неявно гарантируется java. class Foo {статический держатель класса {static Foo foo = new Foo (); } static Foo getInstance () {return Holder.foo; }}
Ирфан, это называется методом Пью, насколько я помню
@Robin, не проще ли использовать статический инициализатор? Они всегда гарантированно работают синхронно.
@Matt, не если вы хотите / нуждаетесь в ленивой инициализации. Однако в контексте DI обычно вы просто позволяете контейнеру IoC решать проблему за вас. Статические инициализаторы не поддерживают ленивую инициализацию (когда вы не знаете, хотите ли вы потратить стоимость в первую очередь) или позднюю инициализацию (когда вы не можете гарантировать порядок ряда взаимозависимых инициализаций).
Почему бы мне просто не синхронизировать всю getInstance ()?
@fhucho, потому что методы, которые были синхронизированы, несут большие накладные расходы при каждом доступе к этому конкретному методу. Вам нужно только синхронизировать создание синглтона, чтобы обеспечить создание только одного экземпляра класса. Если вы синхронизируете getInstance (), также будут ненужные накладные расходы при каждом доступе к одноэлементному классу.
@IbrahimArief, спасибо ... вопрос в том, стоит ли повышение производительности потери простоты и ясности кода.
@fhucho, это, конечно, зависит от вашего варианта использования. Синхронизированные методы обычно в несколько раз медленнее, чем несинхронизированные варианты. Существует миф о том, что вызов синхронизированного метода в 50 раз медленнее, чем вызов несинхронизированного метода, но современные JVM в большинстве случаев снижают это число примерно в 2-10 раз. По-прежнему довольно здоровенный, если вы спросите меня. Проверьте ibm.com/developerworks/java/library/j-threads1/index.html для дальнейшего чтения.
Отсутствие четко определенных методов жизненного цикла для объектов, которые управляют длительными потоками. Мне нравится создавать пары методов с именами init () и destroy (). Также важно вызвать destroy (), чтобы ваше приложение могло корректно завершиться.
Этот укусил меня на этой неделе. У меня был destroy (), но я не смог правильно определить сторону init (), что привело к некоторым гонкам.
Я столкнулся с проблемой параллелизма с сервлетами, когда есть изменяемые поля, которые будут устанавливаться каждым запросом. Но существует только один экземпляр сервлета для всех запросов, поэтому это отлично работало в однопользовательской среде, но когда более одного пользователя запрашивали сервлет, возникали непредсказуемые результаты.
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");
}
Это бесполезно.
Это наверное бесполезно, но процесс синхронизации делает кое-что интересное ... Конечно, создание нового объекта каждый раз - пустая трата времени.
Это не бесполезно. Это барьер памяти без блокировки.
@David: единственная проблема - jvm мог его оптимизировать, вообще сняв такую блокировку
@insighter Я вижу, что ваше мнение разделяют ibm.com/developerworks/java/library/j-jtp10185/index.html Я согласен с тем, что это глупо, поскольку вы не знаете, когда ваш барьер памяти синхронизируется, я просто указывал, что он делает больше, чем ничего.
Начиная с Java 5 существует Thread.getUncaughtExceptionHandler, но этот UncaughtExceptionHandler никогда не вызывается, когда используется ExecutorService / ThreadPool. По крайней мере, мне не удалось получить UncaughtExceptionHandler с работающим ExcutorService.
вы можете передать ThreadFactory в ExecutorService / ThreadPool, где в методе newThread вы можете прикрепить UncaughtExceptionHandler к новому каждому новому потоку.
Изменяемые классы в общих структурах данных
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;
}
}
Было бы потрясающе, если бы здесь было короткое объяснение ... здесь, кажется, есть более чем одна проблема.
Самая распространенная проблема параллелизма, которую я видел, - это непонимание того, что поле, записанное одним потоком, является не гарантировано, чтобы его мог видеть другой поток. Обычное применение этого:
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.
Я использую этот метод постоянно, не могли бы вы рассказать о проблемах, которые он вызывает? Мне всегда казалось, что это работает, но меня никогда не волновало, потребуется ли 1 мс или 100 мс, чтобы наконец увидеть изменение.
Карл, проблема с этим подходом заключается в том, что у вас нет абсолютно никакой гарантии, КОГДА поток, проверяющий флаг остановки, увидит его изменение другим потоком. Это может быть 100 мсек с вашей текущей платформой и виртуальной машиной, но это может быть несколько минут на другой.
И если поток, который нужно остановить, делает что-то критическое (например, тратит деньги вашей компании), каждая секунда, на которой поток не останавливается, может рассчитывать.
Это хуже, чем «на несколько минут» - можно НИКОГДА этого не увидеть. В рамках модели памяти JVM разрешено оптимизировать while (! Stop) до while (true), а затем вы облажаетесь. Это может произойти только на некоторых виртуальных машинах, только в режиме сервера, только когда JVM перекомпилируется после x итераций цикла и т. д. Ой!
Почему вы хотите использовать AtomicBoolean вместо volatile boolean? Я разрабатываю версию 1.4+, есть ли подводные камни при простом объявлении volatile?
Ник, я думаю, это потому, что атомарный CAS обычно даже быстрее, чем volatile. Если вы разрабатываете для 1.4, ваш единственный безопасный вариант IMHO - использовать synchronized as volatile в 1.4, не имеет сильных гарантий барьера памяти, как в Java 5.
Ух ты. Это удивительная вещь. Мне нужно пойти и исправить некоторые ошибки сейчас.
Я никогда не видел этого на практике. Есть ли способ заставить его потерпеть неудачу?
Может ли кто-нибудь объяснить остальным, почему это не работает?
@Thomas: это из-за модели памяти Java. Вы должны прочитать об этом, если хотите узнать об этом подробно (Java Concurrency in Practice, Брайан Гетц, например, хорошо это объясняет). Вкратце: если вы не используете ключевые слова / конструкции для синхронизации памяти (например, volatile, synchronized, AtomicXyz, но также и когда поток завершен), один поток не имеет никаких гарантий, чтобы увидеть изменения, внесенные в любое поле, выполненное другим потоком
@KirkWylie Да, AtomicBoolean, и разве он не должен быть "окончательным"? Я думаю, что это единственный способ безопасно назначить переменную в многопоточном режиме.
@Adrien: Нет смысла делать это окончательным, потому что при этом вы не можете изменить значение переменной.
@Rachel "final" может использоваться в некоторых случаях, иначе AtomicBoolean небезопасно опубликовать, см. stackoverflow.com/a/6457285/965134
@Kutzi Мне бы хотелось увидеть ссылку на ваше заявление о том, что атомарный CAS быстрее, чем volatile. Я использую классы Atomic *, когда мне нужно увеличить значение или иным образом нужно сравнить и установить, но для типа кода, предоставленного OP, я бы использовал volatile
@NamshubWriter AFAIK, это потому, что CAS может быть сопоставлен с атомарными операциями CAS на базовой машине, тогда как volatile не может быть (во всех случаях). По крайней мере, это было сказано, когда java.lang.atomic был представлен несколько лет назад, так что это больше не должно быть правдой. Просто погуглите, и я думаю, вы найдете много ссылок.
Произвольные вызовы методов не должны выполняться из синхронизированных блоков.
Дэйв Рэй коснулся этого в своем первом ответе, и на самом деле я также столкнулся с тупиком, также связанным с вызовом методов на слушателях из синхронизированного метода. Я думаю, что более общий урок заключается в том, что вызовы методов не должны выполняться «на произвол судьбы» из синхронизированного блока - вы понятия не имеете, будет ли вызов длительным, приведет к тупиковой ситуации или чему-то еще.
В этом случае и обычно в целом решение заключалось в том, чтобы уменьшить объем синхронизированного блока, чтобы просто защитить критический раздел кода частный.
Кроме того, поскольку теперь мы обращались к Коллекции слушателей вне синхронизированного блока, мы изменили ее на Коллекцию копирования при записи. Или мы могли бы просто сделать защитную копию Коллекции. Дело в том, что обычно существуют альтернативы безопасному доступу к Коллекции неизвестных объектов.
Я верю, что в будущем основная проблема с Java будет заключаться в (отсутствии) гарантий видимости для конструкторов. Например, если вы создадите следующий класс
class MyClass {
public int a = 1;
}
а затем просто прочтите свойство MyClass а из другого потока, MyClass.a может иметь значение 0 или 1, в зависимости от реализации и настроения JavaVM. Сегодня шансы попасть в 1 очень высоки. Но на будущих машинах с NUMA это может быть иначе. Многие люди не знают об этом и считают, что им не нужно заботиться о многопоточности на этапе инициализации.
Я нахожу это слегка удивительным, но я знаю, что ты умный чувак, Тим, поэтому я возьму это без ссылки. :) Однако, если бы a было окончательным, это не было бы проблемой, верно? Тогда вы будете связаны семантикой окончательного замораживания во время построения?
Я до сих пор нахожу в JMM то, что меня удивляет, поэтому я не доверяю мне, но я почти уверен в этом. См. Также cs.umd.edu/~pugh/java/memoryModel/…. Если бы поле было окончательным, это не было бы проблемой, оно было бы видно после фазы инициализации.
Это проблема только в том случае, если ссылка на только что созданный экземпляр уже используется до того, как конструктор вернул / завершил работу. Например, во время построения класс регистрируется в публичном пуле, и к нему начинают обращаться другие потоки.
MyClass.a указывает на статический доступ, а «a» не является статическим членом MyClass. Помимо этого, как говорится в «ReneS», это проблема только в том случае, если утечка ссылки на незавершенный объект, например, добавление «this» к некоторой внешней карте в конструкторе.
Другой распространенной проблемой «параллелизма» является использование синхронизированного кода, когда в этом нет необходимости. Например, я все еще вижу программистов, использующих StringBuffer или даже java.util.Vector (в качестве локальных переменных метода).
Это не проблема, но в этом нет необходимости, потому что это указывает JVM синхронизировать данные с глобальной памятью и, следовательно, может плохо работать на нескольких процессорах, даже если никто не использует блок синхронизации одновременно.
Синхронизация строкового литерала или константы, определенной строковым литералом, является (потенциально) проблемой, поскольку строковый литерал интернирован и будет использоваться кем-либо еще в JVM, используя тот же строковый литерал. Я знаю, что эта проблема возникла на серверах приложений и в других «контейнерных» сценариях.
Пример:
private static final String SOMETHING = "foo";
synchronized(SOMETHING) {
//
}
В этом случае любой, кто использует строку «foo» для блокировки, использует ту же самую блокировку.
Возможно, он заблокирован. Проблема в том, что семантика WHEN Strings не определена (или, IMNSHO, не определена). Константа времени компилятора "foo" интернируется, "foo", поступающая из сетевого интерфейса, интернируется только в том случае, если вы это сделаете.
Правильно, поэтому я специально использовал буквальную строковую константу, которая гарантированно будет интернирована.
Моя проблема параллелизма # 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 ();
Обожаю - ох, это мерзко :)
Это хороший вариант для Java Puzzlers 2.
Это было не очень хорошо, когда я устранял неполадки в процессе производства: P .... но, оглядываясь назад, это, по крайней мере, иронично.
На самом деле ... это действительно заставляет меня хотеть, чтобы компилятор отказался разрешить вам синхронизацию на String. Учитывая интернирование String, нет случая, когда это было бы «хорошо (тм)».
+1 приватный статический финальный объект LOCK = новый объект ();
Я не думаю, что это совсем правильно. Не гарантируется, что вы будете каждый раз получать один и тот же экземпляр String. То же значение да, возможно, тот же экземпляр.
Тебе больно из-за «может быть». Пока строка не интернирована, вы получаете другой экземпляр, но как только он интернирован, вы действительно получаете точно такой же экземпляр (по oid). Кроме того, вы не можете контролировать, когда строка интернируется - JVM вполне может это сделать для вас, как только он будет создан. Это точно такая же ошибка, которая возникает, когда вы используете == вместо .equals в строке - и это очень известный случай сбоя.
@Jared: «пока строка не интернирована» не имеет смысла. Струны не «интернируются» волшебным образом. String.intern () возвращает другой объект, если у вас еще нет канонического экземпляра указанной String. Кроме того, интернируются все литеральные строки и константные выражения со строковыми значениями. Всегда. См. Документацию для String.intern () и §3.10.5 JLS.
«Однако; поскольку строки интернированы в Java» на самом деле должно быть «Однако; потому что строковые литералы интернированы в Java». (Не все строки интернированы, только литералы) Конечно, "LOCK" интернировано, что является корнем этой проблемы.
Вы помните два разных проекта с открытым исходным кодом, содержащих эту ошибку? Я ищу конкретные примеры этой ошибки в реальном программном обеспечении.
Я видел простой synchronized("LOCK") { this.work(); }: P
Это прекрасный пример сильного воздействия неразложимости.
Похоже, это также должно происходить в некоторых случаях с классами-оболочками: если упаковываемое значение p равно true, false, byte, char в диапазоне от \ u0000 до \ u007f или int или короткое число от -128 до 127 , тогда пусть r1 и r2 будут результатами любых двух упаковочных преобразований p. Всегда бывает так, что r1 == r2.
Все потоки остаются занятыми.
Чаще всего это связано с необходимостью исправлять проблемы в чужом коде, потому что они злоупотребляли блокирующими конструкциями. В последнее время мои коллеги, кажется, сочли, что замки читателя / писателя довольно забавными, чтобы их можно было разбрасывать, в то время как небольшая мысль полностью устраняет их потребность.
В моем собственном коде удерживать потоки занятыми менее очевидно, но сложно. Это требует более глубокого осмысления алгоритмов, таких как написание новых структур данных или тщательное проектирование системы, чтобы гарантировать, что при использовании блокировки она никогда не будет оспариваться.
Исправить ошибки параллелизма легко - попытка выяснить, как избежать конфликта блокировок, может быть сложной задачей.
1) Распространенная ошибка, с которой я столкнулся, связана с повторением синхронизированного класса Collection. Перед получением итератора и во время итерации требуется синхронизировать вручную.
2) Другая ошибка состоит в том, что в большинстве учебников создается впечатление, что создание потокобезопасного класса - это просто вопрос добавления synchronized для каждого метода. Само по себе это не является гарантией - это только защитит целостность конкретного класса, но результаты все равно могут быть неопределенными.
3) Помещение слишком большого количества затратных по времени операций в синхронизированный блок часто приводит к очень плохой производительности. К счастью, шаблон Future в пакете параллелизма может спасти ситуацию.
4) Кеширование изменяемых объектов для повышения производительности также часто приводит к проблемам с многопоточностью (а иногда их очень трудно отследить, поскольку вы предполагаете, что являетесь единственным пользователем).
5) Использование нескольких объектов синхронизации требует осторожности.
Помощь в реализации акторов в функциональной Java и тестирование миллионов потоков на многоядерных машинах.
while(true)
{
if (...)
break
doStuff()
}
Всегда, когда разработчики пишут циклы while, они пропускают "фиксацию ресурса". в их собственном коде.
А именно, если этот блок не выйдет, приложение и, возможно, даже система заблокируются и умрут. Просто из-за простого while(fantasy_land)...if (...) break.
Это проблема, но я не считаю это ошибкой параллелизма?
Мои два цента на попытки избежать проблем с синхронизацией с самого начала - обратите внимание на следующие проблемы / запахи:
new Thread() - это запах. Вместо этого используйте специализированные ExecutorServices, которые заставят вас задуматься об общей концепции потоковой передачи вашего приложения (см. 1) и побудят других следовать ей.AtomicBooleanи другие, синхронизированные коллекции и т. д.). Опять же: принимайте сознательное решение о том, важна ли безопасность потоков в данном контексте, а не просто используйте их вслепую.Конечно, это хороший совет, но он не указывает на ошибку параллелизма Java.
Запуск 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 ++)
Самой последней ошибкой, связанной с параллелизмом, с которой я столкнулся, был объект, который в своем конструкторе создал 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 () с таймаутом в несколько секунд и регистрировать дамп стека и жалобу каждый раз, когда истекает тайм-аут. Это сохраняло желаемое поведение, когда это было возможно, и выявляло проблемы кодирования по мере их возникновения.
или вы можете получить поток петлителя и не вызывать ожидание, когда остановка вызывается из того же потока
Пока я не взял курс с Брайаном Гетцем, я не понимал, что несинхронизированный 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 также исправит это.
Не обязательно. volatile дает вам последнее значение, поэтому он предотвращает возврат 1 навсегда, но не обеспечивает блокировки. Это близко, но не совсем то же самое.
@JohnRussell Я думал, что volatile гарантирует, что отношения случатся раньше. разве это не "запирание"? «Запись в изменчивую переменную (§8.3.1.4) v синхронизируется со всеми последующими чтениями v любым потоком (где последующее определяется в соответствии с порядком синхронизации)».
Запуск потока внутри конструктора класса проблематичен. Если класс расширен, поток может быть запущен. перед конструктором подкласса выполняется.
Метод, сохраняющий данные в переменной экземпляра, чтобы «сэкономить усилия», передавая их вспомогательным методам, когда другой метод, который может быть вызван одновременно, использует те же переменные экземпляра для своих собственных целей.
Вместо этого данные должны передаваться как параметры метода на время синхронизированного вызова. Это лишь небольшое упрощение моих худших воспоминаний:
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 с логическим флагом, чтобы предотвратить любые ложные пробуждения.
Обновление компонента пользовательского интерфейса 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. Мне просто любопытно, каковы шансы, что этот код создаст проблему со временем, в зависимости от среднего количества одновременных запросов (и периода времени). Это может быть очень полезно для оценки того, стоит ли исправлять некоторые приложения.
Я думаю, что наиболее частая проблема параллелизма в Java - это код, который, кажется, до сих пор работает, хотя на самом деле он вообще не является потокобезопасным. Благодаря крошечной ошибке это становится бомбой замедленного действия, и почти во всех случаях вы не знаете об этом заранее, потому что для вас это не очевидно. В то время как обычный ошибочный код, мы надеемся, дает сбой во время тестов, параллельный код часто дает сбой только в конце концов и не может быть воспроизведен.
Почему это закрыто? Это полезно как для других программистов, требующих параллелизма в Java, так и для понимания того, какие классы дефектов параллелизма чаще всего наблюдаются другими разработчиками Java.