Комментарии 15
for (;;) {
Map<String, String> currentMap = parameters.get();
Map<String, String> newMap = new HashMap<>(currentMap);
newMap.put(key, value);
newMap = Collections.unmodifiableMap(newMap);
if (parameters.compareAndSet(currentMap, newMap)) {
break;
}
}
Очень классно в busy-waiting лупе плодить объекты, хотя конечно в сценарии использования этого кода вряд ли будет больше двух спинов, но пример может быть заразителен.
Кратко, понятно, все по делу. Спасибо.
З.Ы. Про volatile почему то забываю. Блокировки и атомики иногда использую в своем коде, а вот volatile наверно нигде у меня не найти. Хотя часть блокировок вполне на него можно было бы заменить наверно. Впрочем на мобилках мы довольно редко с проблемами concurrency сталкиваемся.
Оптимистичной блокировки не хватает.
Пометка поля как "volatile" означает что любое чтение/запись из/в него приводит к инвалидации кешей CPU, не так ли? Как это сказывается на производительности вашего приложения?
Под капотом у AtomicReference сидит тот же volatile, это просто обёртка с доп методами.
Чисто логически если рассуждать, чтение не должно к инвалидации кешей приводить. Ну потому что нет смысла если не было записи по идее. Пока не было новой записи - в кешах ядер можно считать данные актуальными. Но то чисто мои рассуждения. Последний раз уж очень давно уже читал про это все, года 4-5 назад. И тогда только переходил с 1с на андроид, еще не очень понимал зачем оно и как работает.
Дело в том, что каждое ядро работает со своей копией данных, и понятия не имеет что делают другие ядра. И если специально не попросить перечитать переменную из памяти, то ядро будет его брать всегда из своего локального кэша. Так же и при записи, в основную память ничего не сбрасывается автоматом, а изменения делаются в кэше, и сидят там пока эту страницу памяти не вытяснят, когда место потребуется для других данных. Это может произойти через несколько микросекунд, а может и дней...
Volatile как раз и заставляет каждый раз при чтении и записи ходить в RAM, что ужасно дорого, и составляет скажем так тысячу циклов CPU. Единица синхронизации - одна кэш-линия, 64 байт на x86. Причём не только это одно поле синкается, а вообще все переменные видимые текущем потоку. А они наверняка сидят на разных кэш-линиях.
Ещё volatile запрещает делать instruction reordering, что также убивает параллелизм.
Отсюда и вопрос. На высоких скоростях volatile это огромный тормоз. И AtomicReference тоже.
Хм, спасибо. Мне почему то казалось что есть возможность явно указать ядрам что вот по такому адресу надо кэш сбросить. Буду знать что это не так.
З.Ы. Вроде не обязательно в оперативку. Вроде у какого то из вендоров CPU L3 кажется общий на все ядра, или снова путаю что то?
L3 общий, да.
На самом деле, есть ещё когерентность кэшей L1 и 2. Некоторые источники утверждают что все современные CPU сразу же синкают все изменения между кэшами всех ядер при записи. И что volatile в таком случае не требует каждый раз ходить именно в RAM. Но другие пишут что не всегда это работает. Надо сидеть и разбираться, конечно ))
Это всё так, но, как говорится, есть нюанс. А именно, любая синхронизация данных между потоками в Java делает всё тоже самое, чтобы соблюдался принципhappens-before
.
Даже если взять самый быстрый lock в JDK - ReentrantReadWriteLock.ReadLock и посмотреть на его реализацию, то видно, что он работает через CAS своего состояния.
На самом деле, принцип happens-before
как раз и говорит о том, что запрещён instruction reordering и данные в КЭШах долны быть перезачитаны из RAM.
Таким образом, volatile является самым быстрым вариантом межпоточного взаимодействия в Java.
И да, ставить модификатор volatile у каждой переменной класса и надеяться на то, что всё будет само по себе волшебно - это, конечно глупость. В случае, если область видимости данной переменной - это только один поток, то модификатор volatile только затормозит выполнение кода.
Модификатор volatile нужен именно для межпотокового взаимодействия и маркировать им нужно переменные понимая их жизненный цикл - из какого потока создаются, из какого - закрываются (выходят из области видимости всех потоков для сборки GC), из каких потоков читаются и из каких изменяются.
В этом примере, если на методе стоит synchronized
, то и volatile, и immutable нам ведь уже не нужны.
Вы невнимательно читали статью. Здесь synchronized не защищает доступ к переменной parameters, а является критической секцией на время пересборки Immutable-объекта. Это нужно для того, чтобы разные потоки не смогли это сделать одноврменно.
Доступ к переменной parameters
защищает модификатор private
. Immutable был бы всё-ещё нужен, если бы нужно было отдавать всю мапу во вне, и тем самым давать возможность вносить изменения в неё вне класса Configuration. А в данном примере внести изменения в мапу можно только через setValue()
. Так что immutable тут избыточен. Единственный профит, который тут даёт immutable, это барьер от соблазна ещё как-то модифицировать parameters
в этом же классе, кроме как через setValue()
.
Если сделать метод getValue() synchronized
, то и volatile можно будет убрать. Кстати, такой вариант мне кажется более правильным, т.к. к моменту входа в метод setValue()
мы уже намерены внести изменения и рассчитываем, что другие потоки будут видеть их.
Во первых вы не правильно оцениваете жизненный цикл. В любом коде мы можем рассчитывать, что метод внёс какие-то изменения только по выходу из метода, а не на основании входа в метод. Если исходить из правильных предпосылок, то после выхода пишущего потока из метода setValue, все другие потоки будут иметь актуальные данные.
Во вторых, если вы, говоря Immutable, подразумеваете unmodifableMap, то в статье было написано, что это избыточная конструкция. Как вы правильно заметили, parameters имеет область видимости только внутри класса. Если класс написан правильно, то нет нужды закрывать его враппером с защитой от изменений. Однако, сам паттерн Immutable здесь нужен, чтобы обеспечить чтение без блокировки.
В третьих, если вы добавите блокировку на чтение, то всё, что написано в статье вообще не нужно, так как принцип happends before будет обеспечен ключевым словом synchronized. Но это будет уже не lock-free алгоритм, а банальная блокировка по монитору объекта и читающие потоки будут выстраиваться друг за другом для занятия этой блокировки.
Volatile, Lock-free, Immutable, Atomic в Java. Как понять и начать использовать