Pull to refresh

Comments 13

        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 сталкиваемся.

Оптимистичной блокировки не хватает.

Вроде как compareAndSet у AtomicReference это оно и есть.

Пометка поля как "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-объекта. Это нужно для того, чтобы разные потоки не смогли это сделать одноврменно.

Sign up to leave a comment.