Написание потокобезопасного модульного счетчика на Java

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

Также может быть проблема с 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 является поточно-ориентированным , но на самом деле не атомарным .

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

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

Приложение

Вот Список аналогия "проблема" не по порядку.

  • Thread1 вызывает add (first)
  • Thread2 вызывает add (second)

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

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

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

17
задан Carlos Muñoz 8 August 2010 в 13:55
поделиться

4 ответа

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

public final int getAndIncrement(int modulo) {
    for (;;) {
        int current = atomicInteger.get();
        int next = (current + 1) % modulo;
        if (atomicInteger.compareAndSet(current, next))
            return current;
    }
}
7
ответ дан 30 November 2019 в 14:20
поделиться

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

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

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

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

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, может быть интересной.

1
ответ дан 30 November 2019 в 14:20
поделиться

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

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

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

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

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

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

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

     long tmp;
    а ((tmp = tick.get ())> БЕЗОПАСНОЕ_ЗНАЧЕНИЕ))
    {
    long newValue = tmp - SAFETY_VALUE;
    tick.compareAndSet (tmp, newValue);
    }
    

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

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

5
ответ дан 30 November 2019 в 14:20
поделиться

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

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

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

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


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

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

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

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

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

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

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

int hash = 0;

int hashCode(){
    int hash = this.hash;
    if(hash==0){
        hash = this.hash = calcHash();
    }
    return hash;
 }
1
ответ дан 30 November 2019 в 14:20
поделиться