Всякий раз, когда в SO возникает вопрос о синхронизации Java, некоторые люди очень хотят указать, чего synchronized(this)
следует избегать. Вместо этого, они утверждают, что блокировка частной ссылки должна быть предпочтительной.
Некоторые из приведенных причин:
- какой-то злой код может украсть вашу блокировку (очень популярный, также имеет «случайно» вариант)
- все синхронизированные методы в одном классе используют одинаковую блокировку, что снижает пропускную способность
- Вы (излишне) выставляете слишком много информации
Другие люди, включая меня, утверждают, что synchronized(this)
эта идиома, которая часто используется (также в библиотеках Java), безопасна и понятна. Этого не следует избегать, потому что у вас есть ошибка, и вы не знаете, что происходит в вашей многопоточной программе. Другими словами: если это применимо, то используйте его.
Я заинтересован в том, чтобы увидеть некоторые примеры из реальной жизни (без всяких вещей в foobar), где this
предпочтительнее избегать блокировки, когда synchronized(this)
это тоже можно сделать.
Поэтому: следует ли вам всегда избегать synchronized(this)
и заменять его блокировкой частной ссылки?
Некоторая дополнительная информация (обновляется по мере получения ответов):
- мы говорим о синхронизации экземпляров
- как неявные (
synchronized
методы), так и явные формыsynchronized(this)
рассматриваются - если вы цитируете Блоха или другие авторитетные источники по этому вопросу, не пропускайте части, которые вам не нравятся (например, «Эффективная Java», пункт «Потокобезопасность». Обычно это блокировка самого экземпляра, но есть исключения).
- если вам нужна гранулярность в блокировке, отличной от
synchronized(this)
предоставленной, тоsynchronized(this)
это не применимо, так что это не проблема
Ответы:
Я расскажу о каждой точке отдельно.
Меня больше беспокоит случайно . Это означает, что это использование
this
является частью открытого интерфейса вашего класса и должно быть задокументировано. Иногда требуется способность другого кода использовать вашу блокировку. Это верно для таких вещей, какCollections.synchronizedMap
(см. Javadoc).Это слишком упрощенное мышление; просто избавление от
synchronized(this)
не решит проблему. Надлежащая синхронизация для пропускной способности займет больше мысли.Это вариант № 1. Использование
synchronized(this)
является частью вашего интерфейса. Если вы не хотите / нуждаетесь в этом, не делайте этого.источник
keySet()
иvalues()
не блокируются (их)this
, а экземпляром карты, что важно для обеспечения согласованного поведения для всех операций с картой. Причина, по которой объект блокировки преобразован в переменную, заключается в том, что подклассSynchronizedSortedMap
нуждается в нем для реализации вложенных карт, которые блокируют исходный экземпляр карты.Ну, во-первых, следует отметить, что:
семантически эквивалентно:
что является одной из причин не использовать
synchronized(this)
. Вы можете утверждать, что вы можете делать вещи вокругsynchronized(this)
блока. Обычной причиной является попытка вообще избежать синхронизированной проверки, что приводит к всевозможным проблемам параллелизма, в частности к проблеме двойной проверенной блокировки , которая показывает, насколько сложно выполнить относительно простую проверку. поточно.Закрытый замок - это защитный механизм, который никогда не бывает плохой идеей.
Кроме того, как вы упоминали, частные блокировки могут контролировать детализацию. Один набор операций над объектом может быть совершенно не связан с другим, но
synchronized(this)
взаимно исключает доступ ко всем из них.synchronized(this)
просто действительно ничего тебе не дает.источник
Пока вы используете синхронизированный (это), вы используете экземпляр класса в качестве самой блокировки. Это означает, что пока блокировка получена потоком 1 , поток 2 должен ждать.
Предположим, следующий код:
Метод 1, модифицирующий переменную a, и метод 2, модифицирующий переменную b , следует избегать одновременного изменения одной и той же переменной двумя потоками, и это так. НО, хотя thread1 модифицирует a и thread2 модифицирует b, это может быть выполнено без каких-либо условий гонки.
К сожалению, приведенный выше код не позволит этого, так как мы используем ту же ссылку для блокировки; Это означает, что потоки, даже если они не находятся в состоянии гонки, должны ждать и, очевидно, код жертвует параллелизмом программы.
Решение состоит в том, чтобы использовать 2 разных блокировки для двух разных переменных:
В приведенном выше примере используются более мелкозернистые блокировки (2 блокировки вместо одной ( lockA и lockB для переменных a и b соответственно) и, как результат, обеспечивает лучший параллелизм, с другой стороны, он стал более сложным, чем в первом примере ...
источник
Хотя я согласен с тем, что не следует слепо придерживаться догматических правил, сценарий «воровства замков» кажется вам таким эксцентричным? Поток действительно может получить блокировку вашего объекта «извне» (
synchronized(theObject) {...}
), блокируя другие потоки, ожидающие синхронизированных методов экземпляра.Если вы не верите во вредоносный код, учтите, что этот код может быть получен от третьих лиц (например, если вы разрабатываете какой-либо сервер приложений).
«Случайная» версия кажется менее вероятной, но, как говорится, «сделайте что-нибудь защищенное от идиота, и кто-то изобрел лучшего идиота».
Так что я согласен со школой мышления «зависит от того, что в классе».
Редактировать следующие первые 3 комментария eljenso:
Я никогда не сталкивался с проблемой кражи блокировки, но вот воображаемый сценарий:
Допустим, ваша система является контейнером сервлетов, а рассматриваемый нами объект - это
ServletContext
реализация. ЕгоgetAttribute
метод должен быть потокобезопасным, поскольку атрибуты контекста являются общими данными; поэтому вы объявляете это какsynchronized
. Давайте также представим, что вы предоставляете публичный хостинг на основе реализации вашего контейнера.Я ваш клиент и развернул мой "хороший" сервлет на вашем сайте. Бывает, что мой код содержит вызов
getAttribute
.Хакер, замаскированный под другого клиента, размещает свой вредоносный сервлет на вашем сайте. Он содержит следующий код в
init
методе:Предполагая, что у нас один и тот же контекст сервлета (разрешено спецификацией, пока два сервлета находятся на одном виртуальном хосте), мой вызов
getAttribute
заблокирован навсегда. Хакер достиг DoS на моем сервлете.Эта атака невозможна, если
getAttribute
она синхронизирована с частной блокировкой, поскольку сторонний код не может получить эту блокировку.Я допускаю, что этот пример является надуманным и упрощенно показывает, как работает контейнер сервлетов, но ИМХО это доказывает суть.
Поэтому я бы выбрал свой дизайн исходя из соображений безопасности: получу ли я полный контроль над кодом, который имеет доступ к экземплярам? Каковы будут последствия того, что поток удерживает блокировку экземпляра на неопределенный срок?
источник
Это зависит от ситуации.
Если существует только один объект совместного использования или более одного.
Смотрите полный рабочий пример здесь
Небольшое вступление.
Потоки и разделяемые объекты
Для нескольких потоков существует возможность доступа к одному и тому же объекту, например, для нескольких соединенийThreads, совместно использующих одно сообщениеQueue. Поскольку потоки выполняются одновременно, может возникнуть вероятность переопределения своих данных другими, что может привести к путанице.
Таким образом, нам нужен какой-то способ обеспечить доступ к разделяемому объекту одновременно только одному потоку. (Параллелизм).
Синхронизированный блок Синхронизированный блок
() - это способ обеспечить одновременный доступ к разделяемому объекту.
Сначала небольшая аналогия
Предположим, что в умывальнике есть два человека P1, P2 (нити), раковина (разделяемый объект) и дверь (замок).
Теперь мы хотим, чтобы один человек пользовался раковиной одновременно.
Подход заключается в том, чтобы запереть дверь с помощью P1, когда дверь заперта. P2 ждет, пока p1 завершит свою работу.
P1 отпирает дверь,
тогда только p1 может использовать умывальник.
синтаксис.
«this» обеспечивает внутреннюю блокировку, связанную с классом (Java-разработчик разработал класс Object таким образом, что каждый объект может работать как монитор). Вышеупомянутый подход работает нормально, когда есть только одна общая сущность и несколько потоков (1: N). N общих сущностей - темы M Теперь представьте себе ситуацию, когда в умывальнике есть два умывальника и только одна дверь. Если мы используем предыдущий подход, только p1 может использовать одну раковину за раз, в то время как p2 будет ждать снаружи. Это пустая трата ресурсов, так как никто не использует B2 (умывальник). Более разумным подходом было бы создать меньшую комнату внутри уборной и предоставить им одну дверь на раковину. Таким образом, P1 может получить доступ к B1, а P2 может получить доступ к B2 и наоборот.
Подробнее о темах ----> здесь
источник
Похоже, что в лагерях C # и Java существует другое мнение по этому поводу. Большая часть кода Java, который я видел, использует:
в то время как большая часть кода C # выбирает более безопасный:
Идиома C #, безусловно, безопаснее. Как упоминалось ранее, за пределами экземпляра нельзя получить злонамеренный / случайный доступ к блокировке. В коде Java тоже есть этот риск, но кажется, что со временем сообщество Java тяготело к чуть менее безопасной, но немного более краткой версии.
Это не означает, что нужно разбираться с Java, а просто отражает мой опыт работы с обоими языками.
источник
java.util.concurrent
Пакет значительно уменьшить сложность моей потокобезопасной коды. У меня есть только неподтвержденные доказательства, но большинство работ, которые я видел сsynchronized(x)
которыми похоже, заново реализуют блокировку, семафор или защелку, но с использованием мониторов более низкого уровня.Учитывая это, синхронизация с использованием любого из этих механизмов аналогична синхронизации на внутреннем объекте, а не утечке блокировки. Это выгодно тем, что вы абсолютно уверены, что вы контролируете вход в монитор двумя или более потоками.
источник
final
переменные)Lock
API]Пример кода для использования,
ReentrantLock
который реализуетLock
интерфейсПреимущества блокировки перед синхронизированной (это)
Использование синхронизированных методов или операторов заставляет все получение и снятие блокировки происходить блочно-структурированным способом.
Реализации блокировки предоставляют дополнительные функциональные возможности по сравнению с использованием синхронизированных методов и операторов, предоставляя
tryLock()
)lockInterruptibly()
)tryLock(long, TimeUnit)
).Класс Lock также может предоставлять поведение и семантику, которые сильно отличаются от неявной блокировки монитора, такие как
Посмотрите на этот вопрос SE относительно различных типов
Locks
:Синхронизация против блокировки
Вы можете достичь безопасности потоков, используя расширенный API параллелизма вместо синхронизированных блоков. На этой странице документации представлены хорошие программные конструкции для обеспечения безопасности потоков.
Объекты блокировки поддерживают идиомы блокировки, которые упрощают многие параллельные приложения.
Исполнители определяют высокоуровневый API для запуска и управления потоками. Реализации исполнителя, предоставляемые java.util.concurrent, обеспечивают управление пулом потоков, подходящее для крупномасштабных приложений.
Параллельные сборы упрощают управление большими коллекциями данных и могут значительно сократить потребность в синхронизации.
Атомарные переменные имеют функции, которые минимизируют синхронизацию и помогают избежать ошибок согласованности памяти.
ThreadLocalRandom (в JDK 7) обеспечивает эффективную генерацию псевдослучайных чисел из нескольких потоков.
См. Также пакеты java.util.concurrent и java.util.concurrent.atomic для других программных конструкций.
источник
Если вы решили, что:
тогда я не вижу табу на синхронизацию (это).
Некоторые люди сознательно используют синхронизированный (это) (вместо того, чтобы помечать метод как синхронизированный) внутри всего содержимого метода, потому что они думают, что читателю «понятнее», какой объект на самом деле синхронизируется. Пока люди делают осознанный выбор (например, понимают, что таким образом они фактически вставляют дополнительные байтовые коды в метод, и это может оказать влияние на потенциальную оптимизацию), я не вижу особой проблемы с этим , Вы должны всегда документировать параллельное поведение вашей программы, поэтому я не вижу аргумента «синхронизированный» публикует поведение »как столь убедительный.
Что касается вопроса о том, какую блокировку объекта вы должны использовать, я думаю, что нет ничего плохого в синхронизации текущего объекта, если этого ожидала бы логика того, что вы делаете, и то, как обычно будет использоваться ваш класс . Например, в случае коллекции объектом, который вы, по логике, ожидаете заблокировать, обычно является сама коллекция.
источник
Я думаю, что есть хорошее объяснение того, почему каждый из этих жизненно важных приемов находится под вашим пристальным вниманием, в книге Брайана Гетца «Практический параллелизм Java». Он делает одно замечание очень ясным - вы должны использовать один и тот же замок «ВЕЗДЕ» для защиты состояния вашего объекта. Синхронизированный метод и синхронизация на объекте часто идут рука об руку. Например, Вектор синхронизирует все свои методы. Если у вас есть дескриптор векторного объекта и вы собираетесь выполнить команду «положить, если отсутствует», то простая синхронизация Vector с собственными индивидуальными методами не защитит вас от искажения состояния. Вам нужно синхронизировать с помощью synchronized (vectorHandle). Это приведет к тому, что ЖЕ блокировка будет получена каждым потоком, имеющим дескриптор вектора, и защитит общее состояние вектора. Это называется блокировкой на стороне клиента. Мы действительно знаем, что вектор синхронизирует (это) / синхронизирует все свои методы, и, следовательно, синхронизация с объектом vectorHandle приведет к правильной синхронизации состояния векторных объектов. Глупо полагать, что вы потокобезопасны только потому, что используете потокобезопасную коллекцию. Именно поэтому ConcurrentHashMap явно ввел метод putIfAbsent - чтобы сделать такие операции атомарными.
В итоге
источник
Нет, вы не должны всегда . Тем не менее, я склонен избегать этого, когда существует множество проблем для конкретного объекта, которые должны быть поточно-ориентированными по отношению к самим себе. Например, у вас может быть изменяемый объект данных с полями «метка» и «родитель»; они должны быть потокобезопасными, но изменение одного не должно блокировать запись / чтение другого. (На практике я бы избежал этого, объявив поля volatile и / или используя оболочки AtomicFoo из java.util.concurrent).
Синхронизация в целом немного неуклюжа, так как она устанавливает большую блокировку, а не думает, как именно потоки могут работать друг с другом. Использование
synchronized(this)
еще более неуклюже и антисоциально, так как говорит, что «никто не может ничего изменить в этом классе, пока я держу замок». Как часто вам действительно нужно это делать?Я бы предпочел иметь более гранулированные замки; даже если вы хотите остановить все изменения (возможно, вы сериализуете объект), вы можете просто получить все блокировки, чтобы добиться того же самого, плюс это более явным образом. Когда вы используете
synchronized(this)
, неясно, почему именно вы синхронизируете или какие могут быть побочные эффекты. Если вы используетеsynchronized(labelMonitor)
, или даже лучшеlabelLock.getWriteLock().lock()
, становится ясно, что вы делаете и чем ограничены эффекты вашего критического раздела.источник
Краткий ответ : Вы должны понимать разницу и делать выбор в зависимости от кода.
Длинный ответ : В общем, я бы предпочел избегать синхронизации (это), чтобы уменьшить конкуренцию, но частные блокировки увеличивают сложность, о которой вы должны знать. Поэтому используйте правильную синхронизацию для правильной работы. Если вы не имеете большого опыта в многопоточном программировании, я бы предпочел использовать блокировку экземпляров и прочитать эту тему. (При этом просто использование синхронизации (это) не делает ваш класс полностью поточно-ориентированным.) Это непростая тема, но как только вы к ней привыкнете, ответ, использовать синхронизацию (это) или нет, естественен. ,
источник
Блокировка используется либо для видимости, либо для защиты некоторых данных от одновременной модификации, которая может привести к гонке.
Когда вам нужно просто сделать примитивные операции типа атомарными, есть доступные опции, такие
AtomicInteger
как лайки.Но предположим, что у вас есть два целых числа, которые связаны друг с другом, как
x
иy
координаты, которые связаны друг с другом и должны быть изменены атомарным образом. Тогда вы защитите их, используя тот же замок.Блокировка должна защищать только то состояние, которое связано друг с другом. Не меньше и не больше. Если вы используете
synchronized(this)
в каждом методе, то даже если состояние класса не связано, все потоки столкнутся с конфликтом, даже если обновится несвязанное состояние.В приведенном выше примере у меня есть только один метод, который мутирует как,
x
аy
не два разных метода, посколькуx
иy
связан, и если бы я дал два разных метода для мутированияx
иy
отдельно, то он не был бы поточно-ориентированным.Этот пример просто для демонстрации и не обязательно так, как он должен быть реализован. Лучший способ сделать это - сделать его неизменным .
Теперь, в противоположность
Point
примеру, есть пример,TwoCounters
уже предоставленный @Andreas, где состояние, которое защищается двумя различными блокировками, поскольку состояние не связано друг с другом.Процесс использования различных блокировок для защиты несвязанных состояний называется « чередование блокировок» или «разделение блокировок».
источник
Причина не синхронизировать это то , что иногда вам нужно больше , чем один замок (второй замок часто получает удаляется после некоторого дополнительного мышления, но вам все равно нужно в промежуточном состоянии). Если вы фиксируете это , вы всегда должны помнить, какой из двух блокировок это ; если вы заблокируете закрытый объект, имя переменной сообщит вам об этом.
С точки зрения читателя, если вы видите блокировку на этом , вы всегда должны ответить на два вопроса:
Пример:
Если два потока начинаются
longOperation()
в двух разных случаяхBadObject
, они получают свои блокировки; когда пришло время вызыватьl.onMyEvent(...)
, мы имеем тупик, потому что ни один из потоков не может получить блокировку другого объекта.В этом примере мы можем устранить взаимоблокировку, используя две блокировки: одну для коротких операций и одну для длинных.
источник
BadObject
A вызываетlongOperation
B, минуя AmyListener
, и наоборот. Не невозможно, но довольно запутанно, поддерживая мои предыдущие пункты.Как уже было сказано, синхронизированный блок может использовать пользовательскую переменную в качестве объекта блокировки, когда синхронизированная функция использует только «this». И, конечно, вы можете манипулировать областями вашей функции, которые должны быть синхронизированы, и так далее.
Но все говорят, что нет никакой разницы между синхронизированной функцией и блоком, который покрывает всю функцию, используя «this» в качестве объекта блокировки. Это не так, разница в байт-коде, который будет сгенерирован в обеих ситуациях. В случае использования синхронизированного блока должна быть выделена локальная переменная, которая содержит ссылку на «это». В результате мы получим немного больший размер функции (не имеет значения, если у вас всего несколько функций).
Более подробное объяснение разницы вы можете найти здесь: http://www.artima.com/insidejvm/ed2/threadsynchP.html
Также использование синхронизированного блока не является хорошим из-за следующей точки зрения:
Для получения более подробной информации в этой области я рекомендую вам прочитать эту статью: http://java.dzone.com/articles/synchronized-considered
источник
Это на самом деле просто дополняет другие ответы, но если ваше основное возражение против использования закрытых объектов для блокировки состоит в том, что он загромождает ваш класс полями, не связанными с бизнес-логикой, тогда Project Lombok должен
@Synchronized
сгенерировать шаблон во время компиляции:компилируется в
источник
Хороший пример для использования синхронизированного (это).
Как вы можете видеть здесь, мы используем для этого синхронизацию, чтобы облегчить совместную работу (возможно, с бесконечным циклом метода выполнения) с некоторыми синхронизированными методами.
Конечно, это может быть очень легко переписано с использованием синхронизации на частном поле. Но иногда, когда у нас уже есть некоторый дизайн с синхронизированными методами (то есть унаследованный класс, от которого мы наследуем, синхронизированный (это) может быть единственным решением).
источник
this
. Это может быть личное поле.Это зависит от задачи, которую вы хотите выполнить, но я бы не стал ее использовать. Кроме того, проверьте, не удалось ли выполнить сохранение потока, которое вы хотите выполнить, с помощью синхронизации (это) в первую очередь? Есть также несколько хороших блокировок в API, которые могут вам помочь :)
источник
Я только хочу упомянуть возможное решение для уникальных частных ссылок в атомарных частях кода без зависимостей. Вы можете использовать статический Hashmap с блокировками и простой статический метод с именем atomic (), который автоматически создает необходимые ссылки, используя информацию стека (полное имя класса и номер строки). Затем вы можете использовать этот метод для синхронизации операторов без записи нового объекта блокировки.
источник
Избегайте использования
synchronized(this)
в качестве механизма блокировки: это блокирует весь экземпляр класса и может вызвать взаимные блокировки. В таких случаях реорганизуйте код, чтобы заблокировать только определенный метод или переменную, чтобы весь класс не блокировался.Synchronised
может использоваться внутри уровня метода.Вместо использования
synchronized(this)
приведенный ниже код показывает, как можно просто заблокировать метод.источник
Мои два цента в 2019 году, хотя этот вопрос уже мог быть решен.
Блокировка «this» - это неплохо, если вы знаете, что делаете, но за сценой стоит блокировка «this» (что, к сожалению, позволяет синхронизированное ключевое слово в определении метода).
Если вы действительно хотите, чтобы пользователи вашего класса могли «украсть» вашу блокировку (то есть не допустить, чтобы другие потоки имели с ней дело), вы действительно хотите, чтобы все синхронизированные методы ожидали, пока запущен другой метод синхронизации, и так далее. Он должен быть преднамеренным и хорошо продуманным (и, следовательно, задокументированным, чтобы помочь вашим пользователям понять это).
Более подробно, наоборот, вы должны знать, что вы «получаете» (или «теряете»), если вы блокируете недоступную блокировку (никто не может «украсть» вашу блокировку, вы полностью контролируете и так далее). ..).
Для меня проблема в том, что ключевое слово synchronized в сигнатуре определения метода позволяет программистам слишком легко не думать о том, что блокировать, о чем очень важно думать, если вы не хотите сталкиваться с проблемами в мульти программа
Нельзя утверждать, что «как правило» вы не хотите, чтобы пользователи вашего класса могли делать такие вещи, или что «обычно» вы хотите ... Это зависит от того, какую функциональность вы кодируете. Вы не можете сделать правило большого пальца, поскольку вы не можете предсказать все варианты использования.
Рассмотрим, например, устройство печати, которое использует внутреннюю блокировку, но затем люди пытаются использовать ее из нескольких потоков, если они не хотят, чтобы их выходные данные чередовались.
Если ваша блокировка доступна вне класса или нет, это ваше решение как программиста, исходя из того, какие функциональные возможности есть в классе. Это часть API. Например, вы не можете перейти от синхронизированного (этого) к синхронизированному (provateObjet), не рискуя нарушить изменения в коде, использующем его.
Примечание 1: я знаю, что вы можете добиться того, что синхронизируется (это) «достигает», используя явный объект блокировки и выставляя его, но я думаю, что это не нужно, если ваше поведение хорошо документировано и вы действительно знаете, что означает блокировка «this».
Примечание 2: я не согласен с аргументом, что если какой-то код случайно украл вашу блокировку, это ошибка, и вы должны ее решить. В некотором смысле это тот же аргумент, что и я могу сделать все мои методы публичными, даже если они не предназначены для публичности. Если кто-то «случайно» называет мой метод закрытым, это ошибка. Зачем включать эту аварию в первую очередь !!! Если способность украсть ваш замок - проблема для вашего класса, не позволяйте это. Так просто.
источник
Я думаю, что пункты один (кто-то другой использует вашу блокировку) и два (все методы используют одну и ту же блокировку без необходимости) могут встречаться в любом довольно большом приложении. Особенно, когда нет хорошего общения между разработчиками.
Это не камень, это в основном вопрос хорошей практики и предотвращения ошибок.
источник