[Java] вопрос про synchronized

ax222

Всем привет. Просветите, пожалуйста одну непонятку. Вот простой синтетический пример:
 
public class AsyncAddTest {
private static boolean is_r = true;

public static void setR(boolean new_r) {
is_r = new_r;
}

public static final boolean isR {
return is_r;
}

public static void main(String[] args) {
final long start_moment = System.currentTimeMillis;

new Thread {
@Override public void run {
while(System.currentTimeMillis - start_moment < 5000) {}
System.out.println("me here");
setR(false);
}
}.start;

while(isR {}
}
}

В основном потоке цикл while. В условии переменная, которая сначала равна true (к ней обращаемся через метод isR. Тело цикла пустое. Перед while стартует еще один тред, который через 5 секунд меняет переменную с true на false. Я ожидаю, что программа после этого завершит работу (тред и основной поток оба завершатся).
Этот код работает по разному на разных машинах.
На машине с одним ядром (формально - два виртуальных ядра через Hyper Threading) программа работает пять секунд, в консоль выводится "me here" и программа завершается.
На машине с многоядерным процессором работа программы не завершается. Строчка "me here" выводится, но дальше прога продолжает висеть (можно по ctrl-c выйти).
Если снабдить методы setR и isR ключевым словом synchronized, на многоядерной машине все также начинает работать корректно. Программа завершается через 5 секунд.
Знающие люди, расскажите, пожалуйста, каков в данном случае механизм работы synchronized, почему это помогает? Я так себе представлял, что synchronized навешивается на метод или участок кода с целью, чтобы к нему обращался за раз только один поток (если стукнулись два, один ждет). Но в данном случае мне не важна ведь очередность обращения к переменной is_r.

andra1980

http://en.wikipedia.org/wiki/Java_Memory_Model
For example, everything that happens before the release of a lock will be seen to be ordered before and visible to everything that happens after a subsequent acquisition of that same lock.

psm-home

Вход/выход в/из synchronized блока (т. е захват/освобождение лока) влияют еще и на видимость (visibility) изменений. Много где описано, на русском коротко и хорошо есть например тут .

ax222

О, спасибо за ссылку. Я так понимаю, к моему вопросу имеет отношение первый же абзац?
Видимость (visibility)
Один поток может в какой-то момент временно сохранить значение некоторых полей не в основную память, а в регистры или локальный кэш процессора, таким образом второй поток, выполняемый на другом процессоре, читая из основной памяти, может не увидеть последних изменений поля. И наоборот, если поток на протяжении какого-то времени работает с регистрами и локальными кэшами, читая данные оттуда, он может сразу не увидеть изменений, сделанных другим потоком в основную память.
То есть, условно говоря при вызове setR второй поток поменял некое локальное значение переменной, и основному потоку эти изменения не видны (и никогда не становятся видны)?
Еще замечал что если не делать тело while пустым, вносить какие-то минимальные задержки в работу основного потока, все также становится корректным.

psm-home

То есть, условно говоря при вызове setR второй поток поменял некое локальное значение переменной, и основному потоку эти изменения не видны (и никогда не становятся видны)?

Да.
если не делать тело while пустым, вносить какие-то минимальные задержки в работу основного потока, все также становится корректным

Если что-то работает в конкретном случае, оно от этого не становится корректным.

ifani

То есть, условно говоря при вызове setR второй поток поменял некое локальное значение переменной, и основному потоку эти изменения не видны (и никогда не становятся видны)?
Да, упрощённо говоря, можно и так считать (реально java может и более хитрые оптимизации делать).
Еще замечал что если не делать тело while пустым, вносить какие-то минимальные задержки в работу основного потока, все также становится корректным.

На это, точно, не стоит закладываться. Как уже сказали выше, нужно ботать java memory model и отношение "happens before".
Кстати, конкретно в твоём случае можно переменную объявить как volatile - это создаст необходимое отношение hb.

ax222

На это, точно, не стоит закладываться. Как уже сказали выше, нужно ботать java memory model и отношение "happens before".
Закладываться на это, я конечно не собираюсь, просто описал, что заметил в процессе исследования вопроса.
Кстати, конкретно в твоём случае можно переменную объявить как volatile - это создаст необходимое отношение hb.

Это синтетический пример, реальный case немного сложнее. Он на Scala, в цикле там просматривается контейнер функций, и его элементы выполняются. Сначала контейнер пустой, потом в процессе работы может наполняться.
Когда добавлял в него функцию из другого потока, ничего осмысленного не происходило, хотя jvisualvm показывал что все замечательно работает (никакие потоки не в дедлоке и ничего такого). Я еще потратил некоторое количество времени, прежде чем вычленил суть проблемы в этот простой пример в оп-посте (сначала грешил что это может быть какой-нибудь баг Scala).
Всем спасибо за ссылки на JMM, буду читать)

ifani

Я, к сожалению, не знаю Scala, но, на всякий случай, уточню, что в Java когда идёт работа с контейнером (в одном потоке обновляется, а в другом используется то хотя synchronized поможет решить проблему видимости, но большинстве случаев это будет слишком сильным средством, ограничивающим реальную многопоточность. И лучше пользоваться контейнерами, которые предназначены специально для использования в многопоточной среде - в Java это контейнеры из java.util.concurrency - наверняка, в Scala есть что-то подобное.

ax222

В Scala также довольно богатая библиотека для многопоточности, и есть даже целый фреймворк (actors) для проектирования программы в виде набора легких тредов, взаимодействующих через обмен иммутабельными сообщениями (что-то наподобие как в Erlang).
Но есть и более простые варианты: например для всех типов коллекций сделаны synchronized варианты в виде трейтов. Их наследуешь и сразу же все методы в классе коллекции становятся синхронизированными. Например

val arr = new ArrayBuffer[Int]

просто буфер (некий аналог ArrayList а:

val arr = new ArrayBuffer[Int] with SynchronizedBuffer[Int]

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

ifani

Но есть и более простые варианты: например для всех типов коллекций сделаны synchronized варианты в виде трейтов. Их наследуешь и сразу же все методы в классе коллекции становятся синхронизированными.
В Java тоже можно любую коллекцию сделать synchronized c помощью Collections.synchronizedCollection, но, как я писал выше, если коллекция активно используется разными потоками, то она может стать тем самым узким местом, из-за которого упадёт производительность. В общем, нужно отдавать себе отчёт, когда делаешь или используешь synchronized методы.
работа с сетью реализована через акторы, как раз

Как впечатления от акторов?
А то под Java тоже есть реализация (AKKA которую давно хочу попробовать, но по работе подключать новый фреймворк только лишь из интереса - это как-то слишком а чисто для себя - не могу придумать задачу под него.

ax222

 

Как впечатления от акторов?
 

Я читал только Одерски, Programming In Scala, ничего специализированного по акторам не читал, и голову поломать пришлось, прежде чем я добился от них чего хотел) Получился довольно строгий паттерн их использования, от которого никак не получается отклоняться, но в целом, он достаточно универсален.
Во первых, акторы нельзя воспринимать как треды. Я уж не знаю, баг это или by-design, но акторы живут поверх ThreadPoolExecutor'а, который стартует строго определенное количество тредов, пропорциональное количеству ядер процессора (в дефолте коэффициент равен 2 - например на 4ядерном проце он стартанет 8 тредов. Считается, что если иметь тредов больше чем по 2 на ядро, лаг ява-машины при переключении между тредами нивелирует всякие преимущества многопоточности) и далее новые треды не создаются! То есть, если у тебя есть восемь акторов и все заняты какими-то вычислениями, новый созданный актор будет ждать, и очень вероятно что никогда управления не получит.
То есть каждый актор должен четко следовать парадигме приема-отправки сообщений. Внутри актора строится такая структура:
 

loop {
react {
case 'some_case' => // ...
case 'some_other_case' => //....
// ....
}
}

Структура react имеет тип возвращаемого значения - Nothing, это означает, что она никогда не вернет значение (выход из нее только через бросание исключения). Это позволяет Scala-рантайму не хранить стек-вызовов, и он получает возможность раскидывать куски кода под разными case по разным тредам и проводить другие оптимизации.
Таким образом, каждый актор должен только принимать сообщения и делать что-то в ответ на них. А если актору помимо этого требуется делать какую-то периодическую работу, ему следует... посылать сообщения самому себе (это делается через старт нового вспомогательного актора. Который в свою очередь должен либо послать сообщение основному актору, если время пришло, либо стартовать новый такой же актор и завершить работу - типа он не должен висеть и ждать. Вообще, акторов полагается плодить на каждый чих сколько угодно).
Короче, очень завиральная схема работы получается, но в принципе, если разобраться, ничего сложного) У меня получилось сделать все, что мне требовалось, так что фреймворком доволен.

evgen5555

То есть, если у тебя есть восемь акторов и все заняты какими-то вычислениями, новый созданный актор будет ждать, и очень вероятно что никогда управления не получит.
а какая гарантия выполнения, lockfree или waitfree?
с wait-free бесконечного ожидания быть не должно

ax222

Не очень знаком с терминологией. Я наблюдал следующее:
в своем сетевом стеке я приделал подсистему которая пинговала клиентов, чтобы оперативно дисконнектить не отвечающих. Все замечательно работало на 4ядерной машине, но на другой, где ядер меньше, работало все не так замечательно. Раз в минуту проходил пинг и раз в минуту, происходила пересылка остальных сообщений, а остальное время мой сервер был очень молчалив) Суть была в том, что у меня был актор на пинг, внутри которого стоял Thread.sleep, актор на проверку результатов пинга, внутри которого тоже Thread.sleep, актор на прием новых соединений (и там ServerSocket.accept блокирующий) и еще ряд таких же.
Вдумчивое изучение с помощью jvisualvm привело меня к выводу, что тредов фиксированное количество, и пока какой-нибудь актор не закончит работу, рабочий тред не подхватит новый. Я-то думал (по крайней мере книга Programming In Scala оставила мне такое впечатление что если тредов не хватает, будет создан новый, но это не так. И это поведение даже твикнуть нельзя (вроде бы можно увеличить количество тредов, но это не универсальное решение - для каждой машины понадобилась бы своя настройка).
В общем, вот здесь по ссылке написано подробнее:
http://www.tikalk.com/incubator/blog/scala-actors-thread-pool-pitfall
Но в общем, с помощью акторов возможен воркэраунд этой проблемы, это самое главное.
Оставить комментарий
Имя или ник:
Комментарий: