В Java какова производительность AtomicInteger сравниватьAndSet () с синхронизированным ключевым словом? - PullRequest
25 голосов
/ 24 августа 2010

Я реализовывал очередь запросов 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хуже из-за риска блокировки блокировки (что может привести к переключению контекста потока и т. д.), хотя код короче.

Спасибо!

Ответы [ 4 ]

33 голосов
/ 24 августа 2010

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

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

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

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

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

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

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

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

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

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

4 голосов
/ 25 августа 2010

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

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

1 голос
/ 24 августа 2010

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

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

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

  1. Простой старт
  2. Профиль
  3. Если производительность достаточно хорошая, оставьте простую реализацию такой, как
  4. Если вам нужноулучшение производительности, затем начинайте умничать (возможно, сначала используя более специализированную блокировку) и TEST , TEST , TEST
0 голосов
/ 04 февраля 2011

Вот код для блокировки ожидания ожидания.

public class BusyWaitLock
{
    private static final boolean LOCK_VALUE = true;
    private static final boolean UNLOCK_VALUE = false;
    private final static Logger log = LoggerFactory.getLogger(BusyWaitLock.class);

    /**
     * @author Rod Moten
     *
     */
    public class BusyWaitLockException extends RuntimeException
    {

        /**
         * 
         */
        private static final long serialVersionUID = 1L;

        /**
         * @param message
         */
        public BusyWaitLockException(String message)
        {
            super(message);
        }



    }

    private AtomicBoolean lock = new AtomicBoolean(UNLOCK_VALUE);
    private final long maximumWaitTime ; 

    /**
     * Create a busy wait lock with that uses the default wait time of two minutes.
     */
    public BusyWaitLock()
    {
        this(1000 * 60 * 2); // default is two minutes)
    }

    /**
     * Create a busy wait lock with that uses the given value as the maximum wait time.
     * @param maximumWaitTime - a positive value that represents the maximum number of milliseconds that a thread will busy wait.
     */
    public BusyWaitLock(long maximumWaitTime)
    {
        if (maximumWaitTime < 1)
            throw new IllegalArgumentException (" Max wait time of " + maximumWaitTime + " is too low. It must be at least 1 millisecond.");
        this.maximumWaitTime = maximumWaitTime;
    }

    /**
     * 
     */
    public void lock ()
    {
        long startTime = System.currentTimeMillis();
        long lastLogTime = startTime;
        int logMessageCount = 0;
        while (lock.compareAndSet(UNLOCK_VALUE, LOCK_VALUE)) {
            long waitTime = System.currentTimeMillis() - startTime;
            if (waitTime - lastLogTime > 5000) {
                log.debug("Waiting for lock. Log message # {}", logMessageCount++);
                lastLogTime = waitTime;
            }
            if (waitTime > maximumWaitTime) {
                log.warn("Wait time of {} exceed maximum wait time of {}", waitTime, maximumWaitTime);
                throw new BusyWaitLockException ("Exceeded maximum wait time of " + maximumWaitTime + " ms.");
            }
        }
    }

    public void unlock ()
    {
        lock.set(UNLOCK_VALUE);
    }
}
...