В Java какова производительность AtomicInteger compareAndSet () против синхронизированного ключевого слова?

Я реализовывал очередь запросов FIFO (предварительно распределенные объекты запросов для скорости) и начал с использования ключевого слова «synchronized» в методе add. Метод был довольно коротким (проверьте, есть ли место в буфере фиксированного размера, затем добавьте значение в массив). При использовании visualVM поток блокировался чаще, чем мне нравилось (точнее, «монитор»). Поэтому я преобразовал код, чтобы использовать значения AtomicInteger для таких вещей, как отслеживание текущего размера, а затем использование compareAndSet () в циклах while (как это делает AtomicInteger для внутренних методов, таких как incrementAndGet ()). Код теперь выглядит немного длиннее.

Мне было интересно, каковы издержки производительности при использовании синхронизированного и более короткого кода по сравнению с более длинным кодом без синхронизированного ключевого слова (поэтому никогда не следует блокировать при блокировке).

Вот старый метод get с ключевым словом synchronized:

public synchronized Request get()
{
    if (head == tail)
    {
        return null;
    }
    Request r = requests[head];
    head = (head + 1) % requests.length;
    return r;
}

Вот новый метод get без ключевого слова synchronized:

public Request get()
{
    while (true)
    {
        int current = size.get();
        if (current <= 0)
        {
            return null;
        }
        if (size.compareAndSet(current, current - 1))
        {
            break;
        }
    }

    while (true)
    {
        int current = head.get();
        int nextHead = (current + 1) % requests.length;
        if (head.compareAndSet(current, nextHead))
        {
            return requests[current];
        }
    }
}

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

Спасибо!

27
задан Péter Török 24 August 2010 в 12:39
поделиться

3 ответа

Я предполагаю, что ключевое слово synchronized хуже из-за риска блокировки при блокировке (потенциально вызывая переключение контекста потока и т. д.)

Да, в общем случае вы правы. Параллелизм в Java на практике обсуждает это в разделе 15.3.2:

[...] на высоких уровнях конкуренции блокировка имеет тенденцию превосходить атомарные переменные, но на более реалистичных уровнях конкуренции атомарные переменные превосходят блокировки. Это связано с тем, что блокировка реагирует на конкуренцию, приостанавливая потоки, уменьшая использование ЦП и трафик синхронизации на общей шине памяти. (Это похоже на то, как блокирование производителей в схеме производитель-потребитель снижает нагрузку на потребителей и тем самым позволяет им наверстать упущенное.) С другой стороны, с атомарными переменными управление конфликтами отодвигается обратно к вызывающему классу. Как и большинство алгоритмов на основе CAS, AtomicPseudoRandom реагирует на конкуренцию, повторяя немедленную попытку, что обычно является правильным подходом, но в среде с высокой конкуренцией только усиливает конкуренцию.

Прежде чем осуждать AtomicPseudoRandom как плохо написанное, а атомарные переменные — как плохой выбор по сравнению с блокировками, мы должны понять, что уровень разногласий на рис. блокировка или атомарная переменная.На практике атомарные методы масштабируются лучше, чем блокировки, потому что атомарные методы более эффективно справляются с типичными уровнями конкуренции.

Инверсия производительности между блокировками и атомарными блоками на разных уровнях конкуренции иллюстрирует сильные и слабые стороны каждого из них. При низкой и умеренной конкуренции атомарные методы предлагают лучшую масштабируемость; при высокой конкуренции блокировки обеспечивают лучшее предотвращение конкуренции. (Алгоритмы на основе CAS также превосходят алгоритмы на основе блокировок в однопроцессорных системах, поскольку CAS всегда успешно работает в однопроцессорной системе, за исключением маловероятного случая, когда поток прерывается в середине операции чтения-модификации-записи. )

(На рисунках, на которые ссылается текст, рисунок 15.1 показывает, что производительность AtomicInteger и ReentrantLock более или менее одинакова при высокой конкуренции, а рисунок 15.2 показывает, что при умеренной конкуренции первый превосходит последний в несколько раз. из 2–3.)

Обновление: о неблокирующих алгоритмах

Как уже отмечали другие, неблокирующие алгоритмы, хотя и потенциально быстрее, более сложны, поэтому их труднее реализовать правильно. Подсказка из раздела 15.4 JCiA:

Хорошие неблокирующие алгоритмы известны для многих распространенных структур данных, включая стеки, очереди, приоритетные очереди и хеш-таблицы, хотя разработку новых лучше оставить экспертам.

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

33
ответ дан 28 November 2019 в 05:30
поделиться

Интересно, выполняет ли jvm уже несколько циклов, прежде чем действительно приостановить поток. Предполагается, что хорошо написанные критические разделы, такие как ваш, будут очень короткими и заполненными почти сразу. Следовательно, оптимистично он должен подождать, я не знаю, десятков циклов, прежде чем отказаться и приостановить поток. В этом случае он должен вести себя так же, как ваша вторая версия.

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

4
ответ дан 28 November 2019 в 05:30
поделиться

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

Да, синхронизация при некоторых условиях может быть медленнее, чем атомарная операция, но сравните исходный метод и методы замены. Первый действительно понятен и прост в обслуживании, второй - определенно более сложен. Из-за этого могут быть очень тонкие ошибки параллелизма, которые вы не обнаружите во время первоначального тестирования. Я уже вижу одну проблему, size и head действительно могут рассинхронизироваться, потому что, хотя каждая из этих операций является атомарной, комбинация не является, и иногда это может привести к противоречивое состояние.

Итак, мой совет:

  1. Начните с простого
  2. Профиль
  3. Если производительность достаточно высока, оставьте простую реализацию как есть
  4. Если вам нужно улучшить производительность, начните умничать (возможно, используя более специализированные сначала заблокировать) и ТЕСТ , ТЕСТ , ТЕСТ
1
ответ дан 28 November 2019 в 05:30
поделиться
Другие вопросы по тегам:

Похожие вопросы: