Search
Write a publication
Pull to refresh
16
0
Денис Агапитов @DenAgapitov

Руководитель группы Platform Core

Send message

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

Во вторых, если вы, говоря Immutable, подразумеваете unmodifableMap, то в статье было написано, что это избыточная конструкция. Как вы правильно заметили, parameters имеет область видимости только внутри класса. Если класс написан правильно, то нет нужды закрывать его враппером с защитой от изменений. Однако, сам паттерн Immutable здесь нужен, чтобы обеспечить чтение без блокировки.

В третьих, если вы добавите блокировку на чтение, то всё, что написано в статье вообще не нужно, так как принцип happends before будет обеспечен ключевым словом synchronized. Но это будет уже не lock-free алгоритм, а банальная блокировка по монитору объекта и читающие потоки будут выстраиваться друг за другом для занятия этой блокировки.

Вы невнимательно читали статью. Здесь synchronized не защищает доступ к переменной parameters, а является критической секцией на время пересборки Immutable-объекта. Это нужно для того, чтобы разные потоки не смогли это сделать одноврменно.

Это всё так, но, как говорится, есть нюанс. А именно, любая синхронизация данных между потоками в Java делает всё тоже самое, чтобы соблюдался принципhappens-before.

Даже если взять самый быстрый lock в JDK - ReentrantReadWriteLock.ReadLock и посмотреть на его реализацию, то видно, что он работает через CAS своего состояния.

На самом деле, принцип happens-before как раз и говорит о том, что запрещён instruction reordering и данные в КЭШах долны быть перезачитаны из RAM.

Таким образом, volatile является самым быстрым вариантом межпоточного взаимодействия в Java.

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

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

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

Когда начиналась наша шина данных (во времена становления Glassfish и JBoss), проект netty существовал только как часть JBoss (вроде не ошибаюсь). В то время, при исследовании производительности веб-сервисов на серверах Glassfish и JBoss (ещё спецификация EJB 2.x на xml-файлах без аннотаций и асинхронности), стало понятно, что их производительности не достаточно для telecom-нагрузок. Это стало той самой точкой, с которой началось развитие нашей шины на Java (более 15 лет назад).

Да, вы правильно поняли. Относительно асинхронности - все наши приложения полностью асинхронны с самого начала, как и шина. Есть, конечно, и синхронный API, но внутри он всё равно работает через асинхронный.

Запись с потока бизнес-логики уменьшает время отклика до определённого уровня нагрузки на данном железе. На принимающей стороне ответ также пытается уйти с потока бизнес-логики. У нас есть реальные сервисы, которые отвечают со временем отклика менее 1 мс. Многократное перекладывание с потока на поток ощутимо увеличивает время отклика при цепочке из нескольких вызовов.

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

Не думаю, что для тех, кто пишет микро-сервисы при помощи аннотаций RestController, PostMapping и тому подобных, вообще интересно знание о том, как Spring работает с сетью.

Но, если кто-то написал свой сетевой клиент/сервер при помощи NIO, то вопрос производительности в этом случае, как правило, стоит остро. Искренне надеюсь, что для таких проектов статья будет полезна.

Решение с установкой "jdk.nio.maxCachedBufferSize", конечно, рабочее. Причём рабочее без изменения кода вообще, благодаря free при возвращении буфера:

    /**
     * Releases a temporary buffer by returning to the cache or freeing it. If
     * returning to the cache then insert it at the start so that it is
     * likely to be returned by a subsequent call to getTemporaryDirectBuffer.
     */
    static void offerFirstTemporaryDirectBuffer(ByteBuffer buf) {
        // If the buffer is too large for the cache we don't have to
        // check the cache. We'll just free it.
        if (isBufferTooLarge(buf)) {
            free(buf);
            return;
        }

        assert buf != null;
        BufferCache cache = bufferCache.get();
        if (!cache.offerFirst(buf)) {
            // cache is full
            free(buf);
        }
    }

Однако, такое решение прилично снизит пропускную способность, так как в этом случае мы имеем и копирование памяти из HeapByteBuffer в DirectByteBuffer и аллокацию direct-памяти при создании DirectByteBuffer.

На мой взгляд, для сервисов с небольшими нагрузками проще сразу работать с DirectByteBuffer и самому его аллоцировать.

Information

Rating
Does not participate
Location
Санкт-Петербург, Санкт-Петербург и область, Россия
Date of birth
Registered
Activity

Specialization

Backend Developer, Database Developer
Lead
Java
High-loaded systems
Designing application architecture
Database design
Multiple thread
Code Optimization
SOA
REST
SQL
NoSQL