Написание потокобезопасного модульного счетчика на Java - PullRequest
17 голосов
/ 07 августа 2010

Полный отказ от ответственности: на самом деле это не домашняя работа, но я пометил ее как таковую, потому что это в основном упражнение для самообучения, а не на самом деле "для работы".

Скажем такЯ хочу написать простой потокобезопасный модульный счетчик на Java.То есть, если модуль M равен 3, то счетчик должен циклически проходить через 0, 1, 2, 0, 1, 2, … до бесконечности.

Вот одна попытка:

import java.util.concurrent.atomic.AtomicInteger;

public class AtomicModularCounter {
    private final AtomicInteger tick = new AtomicInteger();
    private final int M;

    public AtomicModularCounter(int M) {
        this.M = M;
    }
    public int next() {
        return modulo(tick.getAndIncrement(), M);
    }
    private final static int modulo(int v, int M) {
        return ((v % M) + M) % M;
    }
}

Мой анализ (который может быть ошибочным)) этого кода заключается в том, что, поскольку он использует AtomicInteger, он вполне безопасен для потоков даже без какого-либо явного synchronized метода / блока.

К сожалению, сам «алгоритм» не совсем"работа", потому что когда tick обернут вокруг Integer.MAX_VALUE, next() может вернуть неправильное значение в зависимости от модуля M.То есть:

System.out.println(Integer.MAX_VALUE + 1 == Integer.MIN_VALUE); // true
System.out.println(modulo(Integer.MAX_VALUE, 3)); // 1
System.out.println(modulo(Integer.MIN_VALUE, 3)); // 1

То есть два вызова на next() вернут 1, 1, когда по модулю 3 и tick обтекает.

Также может быть проблемас next() получением значений, не соответствующих порядку, например:

  1. Thread1 вызовы next()
  2. Thread2 вызовы next()
  3. Thread2 завершает tick.getAndIncrement(), возвращает x
  4. Thread1 завершает tick.getAndIncrement(), возвращает y= x + 1 (mod M)

Здесь, за исключением вышеупомянутой проблемы обертывания, x и y действительно являются двумя правильными значениями длявернуть для этих двух next() вызовов, но в зависимости от того, как указано поведение счетчика, можно утверждать, что они вышли из строя.То есть теперь у нас есть (Thread1, y) и (Thread2, x) , но, возможно, действительно следует указать, что (Thread1, x) и (Thread2, y) - это «правильное» поведение.

Таким образом, по определению некоторых слов, AtomicModularCounter является поточно-безопасным , но не на самом деле atomic .

Итак, вопросы:

  • Правильно ли выполнен мой анализ?Если нет, то, пожалуйста, укажите на любые ошибки.
  • Используется ли в моем последнем утверждении правильная терминология?Если нет, то каково правильное утверждение?
  • Если вышеупомянутые проблемы реальны, то как бы вы их исправили?
  • Можете ли вы исправить это, не используя synchronized, используя атомарностьиз AtomicInteger?
  • Как бы вы написали это так, что tick сам по себе контролируется по диапазону по модулю и никогда даже не получает шанса обернуть Integer.MAX_VALUE?
    • Мы можем предположить, что M по крайней мере на порядок меньше Integer.MAX_VALUE при необходимости

Приложение

ВотList аналог "нестандартной" проблемы ".

  • Thread1 звонки add(first)
  • Thread2 звонкиadd(second)

Теперь, если мы успешно обновили список с добавлением двух элементов, но second предшествует first, что в конце, это "потокобезопасно"?

Если это "потокобезопасно", то что это не так?То есть, если мы укажем, что в вышеприведенном сценарии first всегда должно предшествовать second, как называется это свойство параллелизма?(Я назвал это «атомарностью», но я не уверен, что это правильная терминология).

Для чего это стоит, каково поведение Collections.synchronizedList в отношении этого неупорядоченного аспекта?

Ответы [ 4 ]

7 голосов
/ 07 августа 2010

Насколько я вижу, вам просто нужен вариант метода getAndIncrement ()

public final int getAndIncrement(int modulo) {
    for (;;) {
        int current = atomicInteger.get();
        int next = (current + 1) % modulo;
        if (atomicInteger.compareAndSet(current, next))
            return current;
    }
}
5 голосов
/ 07 августа 2010

Я бы сказал, что кроме упаковки это нормально. Когда два вызова метода практически одновременны, вы не можете гарантировать, что произойдет первым.

Код все еще атомарный, потому что, что бы ни случилось на самом деле первым, они вообще не могут мешать друг другу.

Обычно, если у вас есть код, который пытается рассчитывать порядок одновременных вызовов, у вас уже есть условие гонки. Даже если в вызывающем коде один поток достигает начала вызова next() раньше другого, вы можете представить, что он подходит к концу своего временного интервала до того, как он входит в вызов next() - позволяя второму потоку попасть туда.

Если у вызова next() был какой-либо другой побочный эффект - например, он распечатал «Начиная с потока (идентификатор потока)» и , затем вернул следующее значение, тогда оно не будет атомарным; у вас будет заметная разница в поведении. На самом деле, я думаю, что ты в порядке.

Одна вещь, о которой стоит подумать об упаковке: вы можете заставить счетчик работать намного дольше перед упаковкой, если используете AtomicLong:)

РЕДАКТИРОВАТЬ: Я только что подумал о аккуратном способе избежать проблемы обтекания во всех реалистичных сценариях:

  • Определите некоторое большое число M * 100000 (или что-то еще). Это должно быть выбрано достаточно большим, чтобы избежать слишком частых ударов (так как это приведет к снижению производительности), но достаточно маленьким, чтобы можно было ожидать, что приведенный ниже цикл «исправления» будет эффективным до того, как слишком много потоков будет добавлено в тик, чтобы он завернуть.
  • Когда вы выбираете значение с помощью getAndIncrement(), убедитесь, что оно больше этого числа. Если это так, перейдите в «цикл сокращения», который будет выглядеть примерно так:

    long tmp;
    while ((tmp = tick.get()) > SAFETY_VALUE))
    {
        long newValue = tmp - SAFETY_VALUE;
        tick.compareAndSet(tmp, newValue);
    }
    

По сути, это говорит: «Нам нужно вернуть значение в безопасный диапазон, уменьшив несколько кратных модуля» (чтобы оно не изменило значение mod M). Он делает это в тесном цикле, в основном выясняя, каким должно быть новое значение, но внося изменения только в том случае, если между ними ничего не изменилось.

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

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

Атомная (как я понимаю) относится к тому факту, что промежуточное состояние не наблюдается снаружи. atomicInteger.incrementAndGet() является атомарным, а return this.intField++; - нет, в том смысле, что в первом случае вы не можете наблюдать состояние, в котором целое число было увеличено, но еще не возвращено.

Что касается Потоковая безопасность , авторы Java Concurrency на практике дают одно определение в своей книге:

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

(Мое личное мнение следующее)


Теперь, если у нас есть список обновлен успешно с двумя элементами добавлено, но второе предшествует первому, который в конце, это та нить безопасный "?

Если thread1 ввел набор записей объекта мьютекса (в случае Collections.synchronizedList () самого списка) перед thread2, гарантируется, что first будет находиться впереди second в списке после обновления. Это потому, что ключевое слово synchronized использует справедливую блокировку. Кто бы ни сидел впереди очереди, первым делали что-то. Справедливые блокировки могут быть довольно дорогими, и вы также можете иметь несправедливые блокировки в java (с помощью утилит java.util.concurrent). Если вы сделаете это, то такой гарантии нет.

Тем не менее, Java-платформа не является вычислительной платформой в реальном времени, поэтому вы не можете предсказать, сколько времени требуется для выполнения части кода. Это означает, что если вы хотите, чтобы first опережал second, вы должны явно указать это в Java. Это невозможно обеспечить путем «контроля времени» вызова.

Теперь, что здесь потокобезопасно или небезопасно? Я думаю, что это просто зависит от того, что нужно сделать. Если вам просто нужно избежать повреждения списка, и не имеет значения, является ли first первым или second первым в списке, чтобы приложение работало правильно, то достаточно просто избежать повреждения, чтобы установить поток- безопасность. Если это не так, это не так.

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

Знаменитый String.hashCode() не использует какой-либо конкретный «механизм синхронизации», предусмотренный в Java, но он по-прежнему поточно-ориентирован, потому что его можно безопасно использовать в своем собственном приложении. не заботясь о синхронизации и т. д.

Известный трюк String.hashCode ():

int hash = 0;

int hashCode(){
    int hash = this.hash;
    if(hash==0){
        hash = this.hash = calcHash();
    }
    return hash;
 }
1 голос
/ 07 августа 2010

Относительно проблемы атомарности: я не верю, что сам Счетчик может обеспечить поведение, гарантирующее семантику, которую вы подразумеваете.

Я думаю, что у нас есть поток, выполняющий некоторую работу

  A - get some stuff (for example receive a message)
  B - prepare to call Counter
  C - Enter Counter <=== counter code is now in control
  D - Increment
  E - return from Counter <==== just about to leave counter's control
  F - application continues

Посредник, который вы ищете, касается порядка идентификации «полезной нагрузки», установленного в A.

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

Следовательно, любое упорядочение должно быть наложено на все шагиAF, и принудительно контролируется несколькими конкурентами за пределами Counter.Например:

pre-A - Get a lock on Counter (or other lock)
  A - get some stuff (for example receive a message)
  B - prepare to call Counter
  C - Enter Counter <=== counter code is now in control
  D - Increment
  E - return from Counter <==== just about to leave counter's control
  F - application continues
post- F - release lock

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

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

a). Two threads attempt to add
b). One thread adds item with key "X", another attempts to delete the item with key "X"
C). One thread is iterating while a second thread is adding

При условии, что реализация имеет четко определенное поведение в каждом случае, она поточно-ориентирована.Интересный вопрос - какое поведение удобно.

Мы можем просто синхронизировать список и, следовательно, легко дать понятное поведение для a и b.Однако это связано с параллелизмом.И я утверждаю, что это не имело никакого значения, поскольку вам все равно нужно синхронизироваться на более высоком уровне, чтобы получить полезную семантику.Поэтому я хотел бы иметь спецификацию интерфейса, в которой говорится: «Добавление происходит в любом порядке».

Что касается итерации - это сложная проблема, взгляните на то, что обещают коллекции Java: не так уж много!

Эта статья , в которой рассматриваются коллекции Java, может быть интересной.

...