Когда требуется блокировка

Хорошо, я знаю, это может показаться довольно глупым (и я боюсь, что это так), но я не вполне доволен ответом, который я дал себе, поэтому я подумал, что стоит того, чтобы его спросили здесь. Я имею дело с упражнением о concurrency (на Java), который выглядит следующим образом

Учитывая решённую диаграмму Судоку, определите, используя фиксированное число потоков, выполняемых одновременно, правильно ли решена карта, т.е. не происходит нарушения канонических правил (число должно появляться внутри его строки, ее столбца, и его блок только один раз).

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

Не блокируются ли блокировки/синхронизированные блоки/синхронизированные методы тогда и только тогда, когда существует риск потери последовательности ресурсов? Другими словами, правильно ли я понял concurrency?

Ответ 1

Это довольно тонкий вопрос, а не глупо.

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

См. раздел 3.5 Goetz, et. al., Java Concurrency In Practice, для дальнейшего обсуждения концепции безопасной публикации. Раздел 3.5.4 "Эффективно неизменяемые объекты", по-видимому, применим здесь, поскольку доска становится фактически неизменной в определенный момент, потому что она никогда не записывается после того, как она достигла решаемого состояния.

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

Существуют другие способы координации видимости памяти, такие как запись/чтение в изменчивую переменную или AtomicReference. Использование конструкций более высокого уровня Concurrency, таких как защелки или барьеры, или отправка задач на ExecutorService, также обеспечит гарантии видимости памяти.

UPDATE

Основываясь на обмене комментариями Donal Fellows, я должен также указать, что требование безопасной публикации также применяется, когда результаты возвращаются из потоков читателей. То есть, как только один из потоков читателей будет иметь результат из его части вычисления, он должен опубликовать этот результат где-нибудь, чтобы он мог быть объединен с результатами других потоков читателей. Те же самые методы можно использовать по-прежнему, такие как блокировка/синхронизация по общей структуре данных, летучие и т.д. Однако это обычно не требуется, поскольку результаты могут быть получены из Future, возвращаемого ExecutorService.submit или invoke. Эти конструкции автоматически обрабатывают требования к безопасной публикации, поэтому приложению не нужно иметь дело с синхронизацией.

Ответ 2

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

Если вы на 100% уверены, что нить не пишет, тогда безопасно пропускать синхронизацию и блокировку...

РЕДАКТИРОВАНИЕ: переключение блокировки в этих случаях - лучшая практика! :)

Ответ 3

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

Ответ 4

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

Ответ 5

Сценарий, в котором Thread1 записывает некоторые данные, а затем куча потоков, необходимая для чтения этих данных, не требует блокировки, если выполняется правильно. Правильно я имею в виду, что ваша доска SUDOKU является неизменным объектом, а по неизменяемому объекту я имею в виду:

  • Состояние не может быть изменено после строительства
  • Состояние на самом деле не изменяется с помощью какой-либо отраженной темной магии.
  • Все поля окончательные
  • 'this' ссылка не исчезает во время строительства (это может произойти, если во время строительства вы делаете что-то вдоль линий MyClass.instnce = this).

Если вы передаете этот объект рабочим потокам, вам хорошо идти. Если ваши объекты не удовлетворяют всем этим условиям, вы все равно можете столкнуться с проблемами concurrency, в большинстве случаев это связано с тем, что JVM может произвольно изменять порядок операторов (по соображениям производительности) и может изменять порядок этих операторов в таких способ, которым рабочие потоки запускаются до того, как была построена доска sudoku.

Вот очень хорошая статья о неизменяемых объектах.

Ответ 6

Абстрактный

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

Подробнее

Спецификация языка Java пишет:

Два действия могут быть упорядочены с помощью отношения "происходить-до". Если происходит одно действие - перед другим, то первое видно и упорядочивается до второго.

и

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

В вашем случае вы хотите, чтобы потоки чтения разрешали правильную судоку. То есть инициализация объекта sudoku должна быть видимой для потоков чтения, и поэтому инициализация должна произойти - до чтения потоков, считанных с судоку.

Определяет спецификацию-before следующим образом:

Если мы имеем два действия x и y, пишем hb(x, y), чтобы указать, что x происходит до y.

  • Если x и y являются действиями одного и того же потока, а x предшествует y в порядке программы, тогда hb(x, y).

  • Для этого объекта существует конец до конца от конца конструктора объекта до начала финализатора (§12.6).

  • Если действие x синхронизируется со следующим действием y, то мы также имеем hb (x, y).

  • Если hb (x, y) и hb (y, z), то hb (x, z).

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

  • Действие разблокировки на мониторе m синхронизируется со всеми последующими действиями блокировки на m (где "последующее" определяется в соответствии с порядком синхронизации).

  • Запись в изменчивую переменную v (§8.3.1.4) синхронизируется со всеми последующими чтениями v любым потоком (где "последующее" определяется в соответствии с порядком синхронизации).

  • Действие, запускающее поток, синхронизируется с первым действием в запущенном потоке.

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

  • Последнее действие в потоке T1 синхронизируется с любым действием в другом потоке T2, который обнаруживает, что T1 завершен (T2 может выполнить это, вызывая T1.isAlive() или T1.join())

  • Если поток T1 прерывает поток T2, прерывание по T1 синхронизируется с любой точкой, где любой другой поток (включая T2) определяет, что T2 был прерван (путем исключения InterruptedException или путем вызова Thread.interrupted или Thread.isInterrupted).

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

Ответ 7

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