Избегать синхронизации (это) в Java?
Всякий раз, когда в SO возникает вопрос о синхронизации Java, некоторые люди очень хотят указать, что synchronized(this)
необходимо избегать. Вместо этого, они утверждают, что блокировка частной ссылки должна быть предпочтительной.
Некоторые из приведенных причин:
- какой-то злой код может украсть вашу блокировку (очень популярный, также имеет "случайно" вариант)
- все синхронизированные методы в одном классе используют одинаковую блокировку, что снижает пропускную способность
- Вы (излишне) выставляете слишком много информации
Другие люди, включая меня, утверждают, что synchronized(this)
это идиома, которая часто используется (также в библиотеках Java), безопасна и понятна Этого не следует избегать, потому что у вас есть ошибка, и вы не знаете, что происходит в вашей многопоточной программе. Другими словами: если это применимо, то используйте его.
Я заинтересован в том, чтобы увидеть некоторые примеры из реальной жизни (без фуобара), где можно избежать блокировки this
предпочтительнее, когда synchronized(this)
также сделал бы работу.
Следовательно: всегда следует избегать synchronized(this)
и заменить его замком на личную ссылку?
Некоторая дополнительная информация (обновляется по мере получения ответов):
- мы говорим о синхронизации экземпляров
- оба неявных (
synchronized
методы) и явная формаsynchronized(this)
считаются - если вы цитируете Блоха или другие авторитетные источники по этому вопросу, не пропускайте части, которые вам не нравятся (например, "Эффективная Java", пункт "Потокобезопасность": как правило, это блокировка самого экземпляра, но есть исключения).
- если вам нужна гранулярность в вашем замке, кроме
synchronized(this)
обеспечивает, тоsynchronized(this)
не применимо, так что это не проблема
23 ответа
Я расскажу о каждой точке отдельно.
Какой-то злой код может украсть вашу блокировку (очень популярный, также имеет "случайно" вариант)
Меня больше беспокоит случайно. Это означает, что это использование
this
является частью открытого интерфейса вашего класса и должен быть задокументирован. Иногда требуется способность другого кода использовать вашу блокировку. Это верно для таких вещей, какCollections.synchronizedMap
(см. Javadoc).Все синхронизированные методы в одном классе используют одинаковую блокировку, что снижает пропускную способность
Это слишком упрощенное мышление; просто избавиться от
synchronized(this)
не решит проблему. Надлежащая синхронизация для пропускной способности займет больше мысли.Вы (излишне) выставляете слишком много информации
Это вариант № 1. Использование
synchronized(this)
является частью вашего интерфейса. Если вы не хотите / нуждаетесь в этом, не делайте этого.
Ну, во-первых, следует отметить, что:
public void blah() {
synchronized (this) {
// do stuff
}
}
семантически эквивалентно:
public synchronized void blah() {
// do stuff
}
что является одной из причин не использовать synchronized(this)
, Вы можете утверждать, что вы можете делать вещи вокруг synchronized(this)
блок. Обычной причиной является попытка вообще избежать синхронизированной проверки, что приводит к всевозможным проблемам параллелизма, в частности, к проблеме с двойной проверкой блокировки, которая показывает, насколько сложно выполнить относительно простую проверку. поточно.
Закрытый замок - это защитный механизм, который никогда не бывает плохой идеей.
Кроме того, как вы упоминали, частные блокировки могут контролировать детализацию. Один набор операций над объектом может быть совершенно не связан с другим, но synchronized(this)
взаимно исключит доступ ко всем из них.
synchronized(this)
просто действительно ничего тебе не дает.
Пока вы используете синхронизированный (это), вы используете экземпляр класса в качестве самой блокировки. Это означает, что пока блокировка получена потоком 1, поток 2 должен ждать
Предположим, следующий код
public void method1() {
do something ...
synchronized(this) {
a ++;
}
................
}
public void method2() {
do something ...
synchronized(this) {
b ++;
}
................
}
Метод 1, модифицирующий переменную a, и метод 2, модифицирующий переменную b, следует избегать одновременного изменения одной и той же переменной двумя потоками, и это так. НО, в то время как thread1 модифицирует a и thread2 модифицирует b, это можно выполнить без каких-либо условий гонки.
К сожалению, приведенный выше код не позволит этого, так как мы используем ту же ссылку для блокировки; Это означает, что потоки, даже если они не находятся в состоянии гонки, должны ждать и, очевидно, код жертвует параллелизмом программы.
Решение состоит в том, чтобы использовать 2 разных блокировки для двух разных переменных.
class Test {
private Object lockA = new Object();
private Object lockB = new Object();
public void method1() {
do something ...
synchronized(lockA) {
a ++;
}
................
}
public void method2() {
do something ...
synchronized(lockB) {
b ++;
}
................
}
В приведенном выше примере используются более мелкозернистые блокировки (2 блокировки вместо одной (lockA и lockB для переменных a и b соответственно) и, как результат, обеспечивает лучший параллелизм, с другой стороны, он стал более сложным, чем в первом примере...
Хотя я согласен с тем, что не следует слепо придерживаться догматических правил, сценарий "воровства замков" кажется вам таким эксцентричным? Поток действительно может получить блокировку вашего объекта "извне" (synchronized(theObject) {...}
), блокируя другие потоки, ожидающие синхронизированных методов экземпляра.
Если вы не верите во вредоносный код, учтите, что этот код может быть получен от третьих лиц (например, если вы разрабатываете какой-либо сервер приложений).
"Случайная" версия кажется менее вероятной, но, как говорится, "сделайте что-нибудь защищенное от идиота, и кто-то изобрел лучшего идиота".
Так что я согласен со школой мышления "зависит от того, что в классе".
Редактировать следующие первые 3 комментария eljenso:
Я никогда не сталкивался с проблемой кражи блокировки, но вот воображаемый сценарий:
Допустим, ваша система является контейнером сервлетов, а рассматриваемый нами объект - это ServletContext
реализация. это getAttribute
метод должен быть потокобезопасным, поскольку атрибуты контекста являются общими данными; так что вы объявляете это как synchronized
, Давайте также представим, что вы предоставляете публичный хостинг на основе реализации вашего контейнера.
Я ваш клиент и развернул мой "хороший" сервлет на вашем сайте. Бывает, что мой код содержит вызов getAttribute
,
Хакер, замаскированный под другого клиента, размещает свой вредоносный сервлет на вашем сайте. Он содержит следующий код в init
метод:
синхронизированный (this.getServletConfig().getServletContext()) { while (true) {} }
Предполагая, что у нас один и тот же контекст сервлета (разрешено спецификацией, если два сервлета находятся на одном виртуальном хосте), мой вызов getAttribute
заблокирован навсегда. Хакер достиг DoS на моем сервлете.
Эта атака невозможна, если getAttribute
синхронизируется с частной блокировкой, потому что сторонний код не может получить эту блокировку.
Я признаю, что этот пример является надуманным и упрощенно показывает, как работает контейнер сервлетов, но ИМХО это доказывает суть.
Поэтому я бы сделал свой выбор дизайна исходя из соображений безопасности: получу ли я полный контроль над кодом, который имеет доступ к экземплярам? Каковы будут последствия того, что поток удерживает блокировку экземпляра на неопределенный срок?
Это зависит от ситуации.
Если существует только один объект совместного использования или более одного.
Смотрите полный рабочий пример здесь
Небольшое вступление.
Потоки и общие объекты
Возможно, что несколько потоков могут получить доступ к одному и тому же объекту, например, для нескольких соединений Threads, совместно использующих одно сообщение. Поскольку потоки выполняются одновременно, может возникнуть вероятность переопределения своих данных другими, что может привести к путанице.
Таким образом, нам нужен какой-то способ обеспечить доступ к разделяемому объекту одновременно только одному потоку. (Параллелизм).
Синхронизированный блок
Блок synchronized() - это способ обеспечить одновременный доступ к разделяемому объекту.
Сначала небольшая аналогия
Предположим, что в умывальной комнате находятся два человека P1, P2 (нити), раковина (разделяемый объект) и дверь (замок).
Теперь мы хотим, чтобы один человек пользовался раковиной одновременно.
Подход заключается в том, чтобы запереть дверь с помощью P1, когда дверь заперта. P2 ждет, пока p1 не завершит свою работу.
P1 отпирает дверь
тогда только p1 может использовать умывальник.
синтаксис.
synchronized(this)
{
SHARED_ENTITY.....
}
"this" обеспечивает внутреннюю блокировку, связанную с классом (Java-разработчик разработал класс Object таким образом, что каждый объект может работать как монитор). Вышеупомянутый подход работает нормально, когда есть только одна общая сущность и несколько потоков (1: N).
N разделяемых объектов-M потоков
Теперь представьте себе ситуацию, когда внутри умывальника есть две раковины и только одна дверь. Если мы используем предыдущий подход, только p1 может использовать одну раковину за раз, в то время как p2 будет ждать снаружи. Это пустая трата ресурсов, так как никто не использует B2 (умывальник).
Более разумным подходом было бы создать меньшую комнату внутри уборной и предоставить им одну дверь на раковину. Таким образом, P1 может получить доступ к B1, а P2 может получить доступ к B2 и наоборот.
washbasin1;
washbasin2;
Object lock1=new Object();
Object lock2=new Object();
synchronized(lock1)
{
washbasin1;
}
synchronized(lock2)
{
washbasin2;
}
Подробнее о темах----> здесь
Похоже, в C# и Java-лагерях существует другое мнение по этому вопросу. Большая часть кода Java, который я видел, использует:
// apply mutex to this instance
synchronized(this) {
// do work here
}
в то время как большая часть кода C# выбирает более безопасный:
// instance level lock object
private readonly object _syncObj = new object();
...
// apply mutex to private instance level field (a System.Object usually)
lock(_syncObj)
{
// do work here
}
Идиома C#, безусловно, безопаснее. Как упоминалось ранее, за пределами экземпляра нельзя получить злонамеренный / случайный доступ к блокировке. В коде Java тоже есть этот риск, но кажется, что со временем сообщество Java тяготело к чуть менее безопасной, но немного более краткой версии.
Это не означает, что нужно разбираться с Java, а просто отражает мой опыт работы с обоими языками.
- Сделайте ваши данные неизменными, если это возможно (
final
переменные) - Если вы не можете избежать мутации общих данных в нескольких потоках, используйте высокоуровневые программные конструкции [например, гранулированные
Lock
API ]
Блокировка обеспечивает эксклюзивный доступ к общему ресурсу: только один поток за раз может получить блокировку, и весь доступ к общему ресурсу требует, чтобы блокировка была получена первой.
Пример кода для использования ReentrantLock
который реализует Lock
интерфейс
class X {
private final ReentrantLock lock = new ReentrantLock();
// ...
public void m() {
lock.lock(); // block until condition holds
try {
// ... method body
} finally {
lock.unlock()
}
}
}
Преимущества блокировки перед синхронизированной (это)
Использование синхронизированных методов или операторов заставляет все получение и снятие блокировки происходить блочно-структурированным способом.
Реализации блокировки предоставляют дополнительные функциональные возможности по сравнению с использованием синхронизированных методов и операторов, предоставляя
- Неблокирующая попытка получить блокировку (
tryLock()
) - Попытка получить блокировку, которая может быть прервана (
lockInterruptibly()
) - Попытка получить блокировку, которая может истечь (
tryLock(long, TimeUnit)
).
- Неблокирующая попытка получить блокировку (
Класс Lock также может предоставлять поведение и семантику, которые сильно отличаются от неявной блокировки монитора, такие как
- гарантированный заказ
- не повторное использование
- Обнаружение тупика
Посмотрите на этот вопрос SE относительно различных типов Locks
:
Синхронизация против блокировки
Вы можете достичь безопасности потоков, используя расширенный API параллелизма вместо синхронизированных блоков. На этой странице документации представлены хорошие программные конструкции для обеспечения безопасности потоков.
Объекты блокировки поддерживают идиомы блокировки, которые упрощают многие параллельные приложения.
Исполнители определяют высокоуровневый API для запуска и управления потоками. Реализации исполнителя, предоставляемые java.util.concurrent, обеспечивают управление пулом потоков, подходящее для крупномасштабных приложений.
Параллельные коллекции облегчают управление большими коллекциями данных и могут значительно сократить потребность в синхронизации.
Атомарные переменные имеют функции, которые минимизируют синхронизацию и помогают избежать ошибок согласованности памяти.
ThreadLocalRandom (в JDK 7) обеспечивает эффективную генерацию псевдослучайных чисел из нескольких потоков.
См. Также пакеты java.util.concurrent и java.util.concurrent.atomic для других программных конструкций.
java.util.concurrent
Пакет значительно уменьшил сложность моего многопоточного кода. У меня есть только неподтвержденные доказательства, но большинство работ, с которыми я видел synchronized(x)
по-видимому, повторно реализует блокировку, семафор или защелку, но использует мониторы нижнего уровня.
Имея это в виду, синхронизация с использованием любого из этих механизмов аналогична синхронизации на внутреннем объекте, а не утечке блокировки. Это выгодно тем, что вы абсолютно уверены в том, что управляете входом в монитор двумя или более потоками.
Если вы решили, что:
- все, что вам нужно сделать, это заблокировать текущий объект; а также
- вы хотите заблокировать его с гранулярностью меньше, чем у целого метода;
тогда я не вижу табу на синхронизацию (это).
Некоторые люди сознательно используют синхронизированный (это) (вместо того, чтобы помечать метод как синхронизированный) внутри всего содержимого метода, потому что они думают, что читателю "понятнее", какой объект на самом деле синхронизируется. Пока люди делают осознанный выбор (например, понимают, что таким образом они фактически вставляют дополнительные байтовые коды в метод, и это может оказать влияние на потенциальную оптимизацию), я не вижу особой проблемы с этим, Вы должны всегда документировать параллельное поведение вашей программы, поэтому я не вижу аргумента "синхронизированный" публикует поведение "как столь убедительный.
Что касается вопроса о том, какую блокировку объекта вы должны использовать, я думаю, что нет ничего плохого в синхронизации текущего объекта, если этого ожидала бы логика того, что вы делаете, и то, как обычно будет использоваться ваш класс. Например, в случае коллекции объектом, который вы, по логике, ожидаете заблокировать, обычно является сама коллекция.
Блокировка используется либо для видимости, либо для защиты некоторых данных от одновременной модификации, которая может привести к гонке.
Когда вам нужно просто сделать примитивные операции типа атомарными, есть такие опции, как AtomicInteger
и лайки.
Но предположим, что у вас есть два целых числа, которые связаны друг с другом, как x
а также y
координаты, которые связаны друг с другом и должны быть изменены атомарно. Тогда вы защитите их, используя тот же замок.
Блокировка должна защищать только то состояние, которое связано друг с другом. Не меньше и не больше. Если вы используете synchronized(this)
тогда в каждом методе, даже если состояние класса не связано, все потоки будут сталкиваться с конфликтом, даже если обновляется несвязанное состояние.
class Point{
private int x;
private int y;
public Point(int x, int y){
this.x = x;
this.y = y;
}
//mutating methods should be guarded by same lock
public synchronized void changeCoordinates(int x, int y){
this.x = x;
this.y = y;
}
}
В приведенном выше примере у меня есть только один метод, который мутирует оба x
а также y
а не два разных метода как x
а также y
связаны, и если бы я дал два разных метода мутирования x
а также y
отдельно тогда это не было бы потокобезопасным.
Этот пример просто для демонстрации и не обязательно так, как он должен быть реализован. Лучший способ сделать это - сделать его неизменным.
Сейчас в оппозиции к Point
Например, есть пример TwoCounters
уже предоставлено @Andreas, где состояние, которое защищается двумя различными блокировками, поскольку состояние не связано друг с другом.
Процесс использования различных блокировок для защиты несвязанных состояний называется блокированием чередования или разделением блокировок.
Я думаю, что есть хорошее объяснение того, почему каждый из этих жизненно важных приемов находится под вашим пристальным вниманием, в книге Брайана Гетца под названием "Параллелизм Java на практике". Он делает одно замечание очень ясным - вы должны использовать один и тот же замок "ВЕЗДЕ" для защиты состояния вашего объекта. Синхронизированный метод и синхронизация на объекте часто идут рука об руку. Например, Вектор синхронизирует все свои методы. Если у вас есть дескриптор векторного объекта и вы собираетесь выполнить команду "положить, если отсутствует", то простая синхронизация Vector с собственными индивидуальными методами не защитит вас от искажения состояния. Вам нужно синхронизировать с помощью synchronized (vectorHandle). Это приведет к тому, что ЖЕ блокировка будет получена каждым потоком, который имеет дескриптор вектора, и будет защищать общее состояние вектора. Это называется блокировкой на стороне клиента. Мы действительно знаем, что вектор синхронизирует (это) / синхронизирует все свои методы и, следовательно, синхронизация с объектом vectorHandle приведет к правильной синхронизации состояния векторных объектов. Глупо полагать, что вы потокобезопасны только потому, что используете потокобезопасную коллекцию. Именно поэтому ConcurrentHashMap явно ввел метод putIfAbsent - чтобы сделать такие операции атомарными.
В итоге
- Синхронизация на уровне метода позволяет блокировку на стороне клиента.
- Если у вас есть объект частной блокировки - это делает невозможной блокировку на стороне клиента. Это хорошо, если вы знаете, что ваш класс не имеет функциональности типа "положить, если отсутствует".
- Если вы разрабатываете библиотеку - тогда синхронизировать этот метод или синхронизировать метод часто разумнее. Потому что вы редко можете решить, как будет использоваться ваш класс.
- Если бы Вектор использовал частный объект блокировки - было бы невозможно получить правильное "положить, если его не было". Клиентский код никогда не получит дескриптор закрытой блокировки, нарушая тем самым основное правило использования EXACT SAME LOCK для защиты своего состояния.
- Синхронизация в этом или синхронизированных методах действительно имеет проблему, как указывали другие: кто-то может получить блокировку и никогда не снимать ее. Все остальные потоки будут ждать, пока будет снята блокировка.
- Так что знайте, что вы делаете, и примите тот, который является правильным.
- Кто-то утверждал, что наличие объекта частной блокировки обеспечивает лучшую детализацию - например, если две операции не связаны - они могут быть защищены различными блокировками, что приведет к лучшей пропускной способности. Но это, я думаю, запах проекта, а не запах кода - если две операции совершенно не связаны, почему они являются частью того же класса? Почему в классе клуба вообще не должно быть функциональных возможностей? Может быть, служебный класс? Хмммм - какой-то утилитой, обеспечивающей манипулирование строками и форматирование календарной даты через один и тот же экземпляр?? ... не имеет никакого смысла для меня по крайней мере!
Краткий ответ: Вы должны понимать разницу и делать выбор в зависимости от кода.
Длинный ответ: В общем, я бы предпочел избегать синхронизации (это), чтобы уменьшить конкуренцию, но частные блокировки увеличивают сложность, о которой вы должны знать. Так что используйте правильную синхронизацию для правильной работы. Если вы не имеете большого опыта в многопоточном программировании, я бы предпочел использовать блокировку экземпляров и прочитать эту тему. (При этом просто использование синхронизации (это) не делает ваш класс полностью поточно-ориентированным.) Это непростая тема, но как только вы к ней привыкнете, ответ, использовать синхронизацию (это) или нет, возникает естественным образом.,
Нет, не всегда. Тем не менее, я склонен избегать этого, когда существует множество проблем с конкретным объектом, которые должны быть поточно-ориентированными по отношению к самим себе. Например, у вас может быть изменяемый объект данных с полями "метка" и "родитель"; они должны быть потокобезопасными, но изменение одного не должно блокировать запись / чтение другого. (На практике я бы избежал этого, объявив поля volatile и / или используя оболочки AtomicFoo из java.util.concurrent).
Синхронизация в целом немного неуклюжа, так как она устанавливает большую блокировку, а не думает, как именно потоки могут работать друг с другом. С помощью synchronized(this)
он даже неуклюжий и антиобщественный, так как говорит, что "никто не может ничего изменить в этом классе, пока я держу замок". Как часто вам действительно нужно это делать?
Я бы предпочел иметь более гранулированные замки; даже если вы хотите остановить все изменения (возможно, вы сериализуете объект), вы можете просто получить все блокировки, чтобы добиться того же самого, плюс это более явным образом. Когда вы используете synchronized(this)
, не совсем понятно, почему вы синхронизируете или какие могут быть побочные эффекты. Если вы используете synchronized(labelMonitor)
или даже лучше labelLock.getWriteLock().lock()
, ясно, что вы делаете и чем ограничены эффекты вашего критического раздела.
Как уже было сказано, синхронизированный блок может использовать пользовательскую переменную в качестве объекта блокировки, когда синхронизированная функция использует только "this". И, конечно, вы можете манипулировать областями вашей функции, которые должны быть синхронизированы и так далее.
Но все говорят, что нет никакой разницы между синхронизированной функцией и блоком, который охватывает всю функцию, используя "this" в качестве объекта блокировки. Это не так, разница в байт-коде, который будет сгенерирован в обеих ситуациях. В случае использования синхронизированного блока должна быть выделена локальная переменная, которая содержит ссылку на "это". И как результат, у нас будет немного больший размер функции (не имеет значения, если у вас всего несколько функций).
Более подробное объяснение разницы вы можете найти здесь: http://www.artima.com/insidejvm/ed2/threadsynchP.html
Также использование синхронизированного блока не является хорошим из-за следующей точки зрения:
Ключевое слово synchronized очень ограничено в одной области: при выходе из синхронизированного блока все потоки, ожидающие эту блокировку, должны быть разблокированы, но только один из этих потоков получает блокировку; все остальные видят, что замок снят, и возвращаются в заблокированное состояние. Это не просто потраченные впустую циклы обработки: часто переключение контекста для разблокировки потока также включает в себя разбиение памяти на диск, и это очень и очень дорого.
Для более подробной информации в этой области, я бы рекомендовал вам прочитать эту статью: http://java.dzone.com/articles/synchronized-considered
Это на самом деле просто дополняет другие ответы, но если ваше основное возражение против использования закрытых объектов для блокировки состоит в том, что он загромождает ваш класс полями, не связанными с бизнес-логикой, тогда Project Lombok имеет @Synchronized
генерировать шаблон во время компиляции:
@Synchronized
public int foo() {
return 0;
}
компилируется в
private final Object $lock = new Object[0];
public int foo() {
synchronized($lock) {
return 0;
}
}
Причина для того, чтобы не выполнять синхронизацию, заключается в том, что иногда вам требуется более одной блокировки (вторая блокировка часто снимается после некоторого дополнительного размышления, но она все еще нужна в промежуточном состоянии). Если вы фиксируете это, вы всегда должны помнить, какой из двух блокировок это; если вы заблокируете закрытый объект, имя переменной сообщит вам об этом.
С точки зрения читателя, если вы видите блокировку на этом, вы всегда должны ответить на два вопроса:
- какой доступ защищен этим?
- действительно ли достаточно одной блокировки, кто-то не представил ошибку?
Пример:
class BadObject {
private Something mStuff;
synchronized setStuff(Something stuff) {
mStuff = stuff;
}
synchronized getStuff(Something stuff) {
return mStuff;
}
private MyListener myListener = new MyListener() {
public void onMyEvent(...) {
setStuff(...);
}
}
synchronized void longOperation(MyListener l) {
...
l.onMyEvent(...);
...
}
}
Если два потока начинаются longOperation()
на двух разных случаях BadObject
они приобретают свои замки; когда пришло время вызывать l.onMyEvent(...)
У нас есть тупик, потому что ни один из потоков не может получить блокировку другого объекта.
В этом примере мы можем устранить тупик, используя две блокировки, одну для коротких операций и одну для длинных.
Синхронизация включает 3 части: атомарность, видимость и упорядочение.
Синхронизированный блок - это очень грубый уровень синхронизации. Он обеспечивает видимость и упорядоченность, как вы и ожидали. Но для атомарности это не обеспечивает особой защиты. Атомарность требует глобальных знаний о программе, а не местных знаний. (И это очень усложняет многопоточное программирование)
Допустим, у нас есть класс, имеющий метод
deposit
а также
withdraw
. Они оба синхронизируются на основе частной блокировки, например:
class Account {
private Object lock = new Object();
void withdraw(int amount) {
synchronized(lock) {
// ...
}
}
void deposit(int amount) {
synchronized(lock) {
// ...
}
}
}
Учитывая, что нам нужно реализовать класс более высокого уровня, который обрабатывает передачу, например:
class AccountManager {
void transfer(Account fromAcc, Account toAcc, int amount) {
if (fromAcc.getBalance() > amount) {
fromAcc.setBalance(fromAcc.getBalance() - amount);
toAcc.setBalance(toAcc.getBalance + amount);
}
}
}
Предполагая, что у нас сейчас 2 аккаунта,
Account john;
Account marry;
Если
Account.deposit()
и запираются только внутренним замком. Это вызовет проблему, когда у нас работают 2 потока:
// Some thread
void threadA() {
john.withdraw(500);
}
// Another thread
void threadB() {
accountManager.transfer(john, marry, 100);
}
Потому что это возможно для обоих
threadA
а также
threadB
работать одновременно. И поток B завершает условную проверку, поток A отходит, а поток B отходит снова. Это означает, что мы можем снять с Джона 100 долларов, даже если на его счету недостаточно денег. Это нарушит атомарность.
Вы можете предложить: почему бы не добавить
withdraw()
а также
deposit()
тогда? Но по этому предложению нам нужно создать многопоточную
Map
который сопоставляет разные учетные записи с их замками. Нам нужно снять блокировку после выполнения (иначе будет утечка памяти). И мы также должны гарантировать, что никто не получит доступ к
Account.withdraw()
напрямую. Это внесет множество тонких ошибок.
Правильный и наиболее идиоматичный способ - выставить блокировку в
Account
. И пусть
AccountManager
использовать замок. Но в таком случае почему бы тогда просто не использовать сам объект?
class Account {
synchronized void withdraw(int amount) {
// ...
}
synchronized void deposit(int amount) {
// ...
}
}
class AccountManager {
void transfer(Account fromAcc, Account toAcc, int amount) {
// Ensure locking order to prevent deadlock
Account firstLock = fromAcc.hashCode() < toAcc.hashCode() ? fromAcc : toAcc;
Account secondLock = fromAcc.hashCode() < toAcc.hashCode() ? toAcc : fromAcc;
synchronized(firstLock) {
synchronized(secondLock) {
if (fromAcc.getBalance() > amount) {
fromAcc.setBalance(fromAcc.getBalance() - amount);
toAcc.setBalance(toAcc.getBalance + amount);
}
}
}
}
}
Говоря простым языком, приватная блокировка не работает для немного более сложной многопоточной программы.
(Перепечатано с /questions/12897760/v-kriticheskih-razdelah-java-chto-ya-dolzhen-sinhronizirovat/57949806#57949806)
Мои два цента в 2019 году, хотя этот вопрос уже мог быть решен.
Блокировка "this" - это неплохо, если вы знаете, что делаете, но за сценой стоит блокировка "this" (что, к сожалению, позволяет синхронизированное ключевое слово в определении метода).
Если вы действительно хотите, чтобы пользователи вашего класса могли "украсть" вашу блокировку (то есть не допустить, чтобы другие потоки имели с ней дело), вы действительно хотите, чтобы все синхронизированные методы ожидали, пока запущен другой метод синхронизации, и так далее. Он должен быть преднамеренным и хорошо продуманным (и, следовательно, задокументированным, чтобы помочь вашим пользователям понять это).
Чтобы уточнить, наоборот, вы должны знать, что вы "получаете" (или "теряете"), если блокируете недоступную блокировку (никто не может "украсть" вашу блокировку, вы полностью контролируете и так далее)...).
Для меня проблема в том, что ключевое слово synchronized в сигнатуре определения метода позволяет программистам слишком легко не думать о том, что блокировать, о чем очень важно думать, если вы не хотите сталкиваться с проблемами в мульти программа.
Нельзя утверждать, что "как правило" вы не хотите, чтобы пользователи вашего класса могли делать такие вещи, или что "обычно" вы хотите... Это зависит от того, какую функциональность вы кодируете. Вы не можете сделать правило большого пальца, поскольку вы не можете предсказать все варианты использования.
Рассмотрим, например, устройство печати, которое использует внутреннюю блокировку, но затем люди пытаются использовать ее из нескольких потоков, если они не хотят, чтобы их выходные данные чередовались.
Если ваша блокировка доступна вне класса или нет, это ваше решение как программиста, исходя из того, какие функциональные возможности есть в классе. Это часть API. Например, вы не можете перейти от синхронизированного (this) к синхронизированному (provateObjet), не рискуя нарушить изменения в коде, использующем его.
Примечание 1: я знаю, что вы можете добиться того, что синхронизируется (это) "достигает", используя явный объект блокировки и выставляя его, но я думаю, что это не нужно, если ваше поведение хорошо документировано и вы действительно знаете, что означает блокировка "this".
Примечание 2: я не согласен с аргументом, что если какой-то код случайно украл вашу блокировку, это ошибка, и вы должны ее решить. В некотором смысле это тот же аргумент, что и я могу сделать все мои методы публичными, даже если они не предназначены для публичности. Если кто-то "случайно" вызывает мой намеренный закрытый метод, это ошибка. Зачем включать эту аварию в первую очередь!!! Если способность украсть ваш замок - проблема для вашего класса, не позволяйте это. Так просто, как, что.
Это зависит от задачи, которую вы хотите выполнить, но я бы не стал ее использовать. Кроме того, проверьте, не удалось ли в первую очередь выполнить синхронизацию потоков, которую вы хотите выполнить, с помощью синхронизации (это)? Есть также несколько хороших блокировок в API, которые могут вам помочь:)
Избегать использования synchronized(this)
как механизм блокировки: блокирует весь экземпляр класса и может вызвать взаимные блокировки. В таких случаях реорганизуйте код, чтобы заблокировать только определенный метод или переменную, чтобы весь класс не блокировался. Synchronised
может использоваться внутри уровня метода.
Вместо того, чтобы использовать synchronized(this)
Код ниже показывает, как вы можете просто заблокировать метод.
public void foo() {
if(operation = null) {
synchronized(foo) {
if (operation == null) {
// enter your code that this method has to handle...
}
}
}
}
Я только хочу упомянуть возможное решение для уникальных частных ссылок в атомарных частях кода без зависимостей. Вы можете использовать статический Hashmap с блокировками и простой статический метод с именем atomic(), который автоматически создает необходимые ссылки, используя информацию стека (полное имя класса и номер строки). Затем вы можете использовать этот метод в операторах синхронизации без написания нового объекта блокировки.
// Synchronization objects (locks)
private static HashMap<String, Object> locks = new HashMap<String, Object>();
// Simple method
private static Object atomic() {
StackTraceElement [] stack = Thread.currentThread().getStackTrace(); // get execution point
StackTraceElement exepoint = stack[2];
// creates unique key from class name and line number using execution point
String key = String.format("%s#%d", exepoint.getClassName(), exepoint.getLineNumber());
Object lock = locks.get(key); // use old or create new lock
if (lock == null) {
lock = new Object();
locks.put(key, lock);
}
return lock; // return reference to lock
}
// Synchronized code
void dosomething1() {
// start commands
synchronized (atomic()) {
// atomic commands 1
...
}
// other command
}
// Synchronized code
void dosomething2() {
// start commands
synchronized (atomic()) {
// atomic commands 2
...
}
// other command
}
Хороший пример для синхронизированного использования (это).
// add listener
public final synchronized void addListener(IListener l) {listeners.add(l);}
// remove listener
public final synchronized void removeListener(IListener l) {listeners.remove(l);}
// routine that raise events
public void run() {
// some code here...
Set ls;
synchronized(this) {
ls = listeners.clone();
}
for (IListener l : ls) { l.processEvent(event); }
// some code here...
}
Как вы можете видеть здесь, мы используем для этого синхронизацию, чтобы облегчить совместную работу длинного (возможно, бесконечного цикла метода выполнения) с некоторыми синхронизированными методами.
Конечно, это может быть очень легко переписано с использованием синхронизации на частном поле. Но иногда, когда у нас уже есть некоторый дизайн с синхронизированными методами (то есть унаследованный класс, от которого мы наследуем, синхронизированный (это) может быть единственным решением).
Я думаю, что пункты один (кто-то другой использует вашу блокировку) и два (все методы используют ту же блокировку без необходимости) могут возникнуть в любом довольно большом приложении. Особенно, когда нет хорошего общения между разработчиками.
Это не камень, это в основном вопрос хорошей практики и предотвращения ошибок.