Избегать синхронизации (это) в Java?


381

Всякий раз, когда в SO возникает вопрос о синхронизации Java, некоторые люди очень хотят указать, чего synchronized(this)следует избегать. Вместо этого, они утверждают, что блокировка частной ссылки должна быть предпочтительной.

Некоторые из приведенных причин:

Другие люди, включая меня, утверждают, что synchronized(this)эта идиома, которая часто используется (также в библиотеках Java), безопасна и понятна. Этого не следует избегать, потому что у вас есть ошибка, и вы не знаете, что происходит в вашей многопоточной программе. Другими словами: если это применимо, то используйте его.

Я заинтересован в том, чтобы увидеть некоторые примеры из реальной жизни (без всяких вещей в foobar), где thisпредпочтительнее избегать блокировки, когда synchronized(this)это тоже можно сделать.

Поэтому: следует ли вам всегда избегать synchronized(this)и заменять его блокировкой частной ссылки?


Некоторая дополнительная информация (обновляется по мере получения ответов):

  • мы говорим о синхронизации экземпляров
  • как неявные ( synchronizedметоды), так и явные формы synchronized(this)рассматриваются
  • если вы цитируете Блоха или другие авторитетные источники по этому вопросу, не пропускайте части, которые вам не нравятся (например, «Эффективная Java», пункт «Потокобезопасность». Обычно это блокировка самого экземпляра, но есть исключения).
  • если вам нужна гранулярность в блокировке, отличной от synchronized(this)предоставленной, то synchronized(this)это не применимо, так что это не проблема

4
Я также хотел бы отметить, что контекст важен - бит «Обычно это блокировка самого экземпляра» находится в разделе о документировании условно-ориентированного на поток класса, когда вы делаете блокировку общедоступной. Другими словами, это предложение применяется, когда вы уже приняли это решение.
Джон Скит

По словам Блоха, в отсутствие внутренней синхронизации и когда необходима внешняя синхронизация, блокировка часто является самим экземпляром. Так почему же это не относится к внутренней синхронизации с блокировкой «this»? (Важность документации - еще одна проблема.)
eljenso

Существует компромисс между расширенной гранулярностью и дополнительными затратами на кэш-память ЦП и запросами шины, поскольку блокировка внешнего объекта, скорее всего, потребует изменения отдельной строки кеша и обмена между кешами ЦП (см. MESIF и MOESI).
ArtemGr

1
Я думаю, что в мире защитного программирования вы предотвращаете ошибки не по идиомам, а по коду. Когда кто-то задает мне вопрос: «Насколько оптимизирована ваша синхронизация?», Я хочу сказать «Очень» вместо «Очень», если кто-то другой не следует этой идиоме ».
Sgene9

Ответы:


132

Я расскажу о каждой точке отдельно.

  1. Какой-то злой код может украсть вашу блокировку (очень популярный, также имеет «случайно» вариант)

    Меня больше беспокоит случайно . Это означает, что это использование thisявляется частью открытого интерфейса вашего класса и должно быть задокументировано. Иногда требуется способность другого кода использовать вашу блокировку. Это верно для таких вещей, как Collections.synchronizedMap(см. Javadoc).

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

    Это слишком упрощенное мышление; просто избавление от synchronized(this)не решит проблему. Надлежащая синхронизация для пропускной способности займет больше мысли.

  3. Вы (излишне) выставляете слишком много информации

    Это вариант № 1. Использование synchronized(this)является частью вашего интерфейса. Если вы не хотите / нуждаетесь в этом, не делайте этого.


1. «synchronized» не является частью открытого интерфейса вашего класса. 2. согласен 3. см. 1.
eljenso

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

15
Аналогичный. См. Javadoc для Collections.synchronizedMap () - возвращенный объект использует синхронизированный (this) для внутреннего использования, и они ожидают, что потребитель воспользуется этим, чтобы использовать ту же блокировку для крупномасштабных атомарных операций, таких как итерация.
Даррон

3
На самом деле Collections.synchronizedMap () НЕ использует синхронизированный (это) внутри, он использует частный объект окончательной блокировки.
Бас Лейдеккерс

1
@Bas Leijdekkers: в документации четко указано, что синхронизация происходит в возвращенном экземпляре карты. Интересно то, что представления возвращаются keySet()и values()не блокируются (их) this, а экземпляром карты, что важно для обеспечения согласованного поведения для всех операций с картой. Причина, по которой объект блокировки преобразован в переменную, заключается в том, что подкласс SynchronizedSortedMapнуждается в нем для реализации вложенных карт, которые блокируют исходный экземпляр карты.
Хольгер

86

Ну, во-первых, следует отметить, что:

public void blah() {
  synchronized (this) {
    // do stuff
  }
}

семантически эквивалентно:

public synchronized void blah() {
  // do stuff
}

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

Закрытый замок - это защитный механизм, который никогда не бывает плохой идеей.

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

synchronized(this) просто действительно ничего тебе не дает.


4
«синхронизированный (это) просто действительно ничего вам не дает». Хорошо, я заменяю его синхронизацией (myPrivateFinalLock). Что это мне дает? Вы говорите о том, что это защитный механизм. От чего я защищен?
eljenso

14
Вы защищены от случайного (или злонамеренного) блокирования 'this' внешними объектами.
Клет

14
Я совсем не согласен с этим ответом: блокировка всегда должна удерживаться в течение как можно более короткого промежутка времени, и именно поэтому вы хотите «делать вещи» вокруг синхронизированного блока, а не синхронизировать весь метод. ,
Оливье

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

16
В целом я не согласен с тем, что «Х - это защитный механизм, который никогда не бывает плохой идеей». Из-за такого отношения существует много ненужного раздутого кода.
finnw

54

Пока вы используете синхронизированный (это), вы используете экземпляр класса в качестве самой блокировки. Это означает, что пока блокировка получена потоком 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 разных блокировки для двух разных переменных:

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


Это очень опасно. Вы ввели требование упорядочения блокировки на стороне клиента (пользователя этого класса). Если два потока вызывают method1 () и method2 () в другом порядке, они могут зайти в тупик, но пользователь этого класса не знает, что это так.
daveb

7
Детализация, не обеспеченная "synchronized (this)", выходит за рамки моего вопроса. И не должны ли ваши поля блокировки быть окончательными?
eljenso

10
чтобы зайти в тупик, мы должны выполнить вызов из блока, синхронизированного A, в блок, синхронизированный B. daveb, вы ошибаетесь ...
Андреас Бакуров

3
Насколько я вижу, в этом примере тупика нет. Я принимаю, что это просто псевдокод, но я бы использовал одну из реализаций java.util.concurrent.locks.Lock, как java.util.concurrent.locks.ReentrantLock
Шон Вейдер,

15

Хотя я согласен с тем, что не следует слепо придерживаться догматических правил, сценарий «воровства замков» кажется вам таким эксцентричным? Поток действительно может получить блокировку вашего объекта «извне» ( synchronized(theObject) {...}), блокируя другие потоки, ожидающие синхронизированных методов экземпляра.

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

«Случайная» версия кажется менее вероятной, но, как говорится, «сделайте что-нибудь защищенное от идиота, и кто-то изобрел лучшего идиота».

Так что я согласен со школой мышления «зависит от того, что в классе».


Редактировать следующие первые 3 комментария eljenso:

Я никогда не сталкивался с проблемой кражи блокировки, но вот воображаемый сценарий:

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

Я ваш клиент и развернул мой "хороший" сервлет на вашем сайте. Бывает, что мой код содержит вызов getAttribute.

Хакер, замаскированный под другого клиента, размещает свой вредоносный сервлет на вашем сайте. Он содержит следующий код в initметоде:

синхронизированный (this.getServletConfig (). getServletContext ()) {
   while (true) {}
}

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

Эта атака невозможна, если getAttributeона синхронизирована с частной блокировкой, поскольку сторонний код не может получить эту блокировку.

Я допускаю, что этот пример является надуманным и упрощенно показывает, как работает контейнер сервлетов, но ИМХО это доказывает суть.

Поэтому я бы выбрал свой дизайн исходя из соображений безопасности: получу ли я полный контроль над кодом, который имеет доступ к экземплярам? Каковы будут последствия того, что поток удерживает блокировку экземпляра на неопределенный срок?


это зависит от того, что делает класс: если это «важный» объект, тогда заблокировать частную ссылку? Иначе блокировки экземпляра будет достаточно?
eljenso

6
Да, мне кажется, что сценарий кражи блокировки надуман Все упоминают об этом, но кто на самом деле это сделал или испытал? Если вы «случайно» заблокировали объект, который вы не должны делать, тогда для этого типа ситуации есть название: это ошибка. Почини это.
eljenso

4
Кроме того, блокировка внутренних ссылок не свободна от «внешней атаки синхронизации»: если вы знаете, что определенная синхронизированная часть кода ожидает внешнего события (например, запись файла, значение в БД, событие таймера), вы, вероятно, можете организуйте это, чтобы заблокировать также.
eljenso

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

12

Это зависит от ситуации.
Если существует только один объект совместного использования или более одного.

Смотрите полный рабочий пример здесь

Небольшое вступление.

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

Синхронизированный блок Синхронизированный блок
() - это способ обеспечить одновременный доступ к разделяемому объекту.
Сначала небольшая аналогия
Предположим, что в умывальнике есть два человека 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;
  }

введите описание изображения здесь
введите описание изображения здесь

Подробнее о темах ----> здесь


11

Похоже, что в лагерях 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, а просто отражает мой опыт работы с обоими языками.


3
Возможно, поскольку C # является более молодым языком, они извлекли уроки из плохих шаблонов, которые были обнаружены в лагере Java, и такого рода программных вещей лучше? Есть ли также меньше синглетонов? :)
Билл К

3
Хе хе Вполне возможно, правда, но я не собираюсь подниматься на удочку! Одно можно сказать наверняка, это то, что в коде C # больше заглавных букв;)
serg10

1
Просто не соответствует действительности (
мягко

7

java.util.concurrentПакет значительно уменьшить сложность моей потокобезопасной коды. У меня есть только неподтвержденные доказательства, но большинство работ, которые я видел сsynchronized(x) которыми похоже, заново реализуют блокировку, семафор или защелку, но с использованием мониторов более низкого уровня.

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


6
  1. Сделайте ваши данные неизменными, если это возможно ( final переменные)
  2. Если вы не можете избежать мутации общих данных в нескольких потоках, используйте высокоуровневые программные конструкции [например, гранулированный LockAPI]

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

Пример кода для использования, 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()
     }
   }
 }

Преимущества блокировки перед синхронизированной (это)

  1. Использование синхронизированных методов или операторов заставляет все получение и снятие блокировки происходить блочно-структурированным способом.

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

    1. Неблокирующая попытка получить блокировку ( tryLock())
    2. Попытка получить блокировку, которая может быть прервана ( lockInterruptibly())
    3. Попытка получить блокировку, которая может тайм-аут ( tryLock(long, TimeUnit)).
  3. Класс Lock также может предоставлять поведение и семантику, которые сильно отличаются от неявной блокировки монитора, такие как

    1. гарантированный заказ
    2. не повторное использование
    3. Обнаружение тупика

Посмотрите на этот вопрос SE относительно различных типов Locks :

Синхронизация против блокировки

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

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

Исполнители определяют высокоуровневый API для запуска и управления потоками. Реализации исполнителя, предоставляемые java.util.concurrent, обеспечивают управление пулом потоков, подходящее для крупномасштабных приложений.

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

Атомарные переменные имеют функции, которые минимизируют синхронизацию и помогают избежать ошибок согласованности памяти.

ThreadLocalRandom (в JDK 7) обеспечивает эффективную генерацию псевдослучайных чисел из нескольких потоков.

См. Также пакеты java.util.concurrent и java.util.concurrent.atomic для других программных конструкций.


5

Если вы решили, что:

  • вам нужно заблокировать текущий объект; а также
  • вы хотите заблокировать его с гранулярностью меньше, чем у всего метода;

тогда я не вижу табу на синхронизацию (это).

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

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


1
«если этого и следовало ожидать по логике ...» - это точка, которую я тоже пытаюсь донести. Я не вижу смысла всегда использовать закрытые блокировки, хотя общее мнение, похоже, заключается в том, что это лучше, так как это не повредит и является более оборонительным.
eljenso

4

Я думаю, что есть хорошее объяснение того, почему каждый из этих жизненно важных приемов находится под вашим пристальным вниманием, в книге Брайана Гетца «Практический параллелизм Java». Он делает одно замечание очень ясным - вы должны использовать один и тот же замок «ВЕЗДЕ» для защиты состояния вашего объекта. Синхронизированный метод и синхронизация на объекте часто идут рука об руку. Например, Вектор синхронизирует все свои методы. Если у вас есть дескриптор векторного объекта и вы собираетесь выполнить команду «положить, если отсутствует», то простая синхронизация Vector с собственными индивидуальными методами не защитит вас от искажения состояния. Вам нужно синхронизировать с помощью synchronized (vectorHandle). Это приведет к тому, что ЖЕ блокировка будет получена каждым потоком, имеющим дескриптор вектора, и защитит общее состояние вектора. Это называется блокировкой на стороне клиента. Мы действительно знаем, что вектор синхронизирует (это) / синхронизирует все свои методы, и, следовательно, синхронизация с объектом vectorHandle приведет к правильной синхронизации состояния векторных объектов. Глупо полагать, что вы потокобезопасны только потому, что используете потокобезопасную коллекцию. Именно поэтому ConcurrentHashMap явно ввел метод putIfAbsent - чтобы сделать такие операции атомарными.

В итоге

  1. Синхронизация на уровне метода позволяет блокировку на стороне клиента.
  2. Если у вас есть объект частной блокировки - это делает невозможной блокировку на стороне клиента. Это хорошо, если вы знаете, что ваш класс не имеет функциональности типа «положить, если отсутствует».
  3. Если вы проектируете библиотеку - тогда синхронизировать этот метод или синхронизировать метод часто разумнее. Потому что вы редко можете решить, как будет использоваться ваш класс.
  4. Если бы Вектор использовал частный объект блокировки - было бы невозможно получить правильное «положить, если его не было». Клиентский код никогда не получит дескриптор частной блокировки, нарушая тем самым основное правило использования EXACT SAME LOCK для защиты своего состояния.
  5. Синхронизация на этом или синхронизированных методах действительно имеет проблему, как указывали другие: кто-то может получить блокировку и никогда не снимать ее. Все остальные потоки будут ждать, пока будет снята блокировка.
  6. Так что знайте, что вы делаете, и примите тот, который является правильным.
  7. Кто-то утверждал, что наличие объекта частной блокировки обеспечивает лучшую детализацию - например, если две операции не связаны - они могут быть защищены различными блокировками, что приведет к лучшей пропускной способности. Но я думаю, что это запах дизайна, а не запах кода - если две операции совершенно не связаны, почему они являются частью одного и того же класса? Почему в классном клубе вообще не должно быть функциональных возможностей? Может быть, служебный класс? Хмммм - какой-то утилитой, обеспечивающей манипулирование строками и форматирование календарной даты через один и тот же экземпляр? ... не имеет никакого смысла для меня по крайней мере!

3

Нет, вы не должны всегда . Тем не менее, я склонен избегать этого, когда существует множество проблем для конкретного объекта, которые должны быть поточно-ориентированными по отношению к самим себе. Например, у вас может быть изменяемый объект данных с полями «метка» и «родитель»; они должны быть потокобезопасными, но изменение одного не должно блокировать запись / чтение другого. (На практике я бы избежал этого, объявив поля volatile и / или используя оболочки AtomicFoo из java.util.concurrent).

Синхронизация в целом немного неуклюжа, так как она устанавливает большую блокировку, а не думает, как именно потоки могут работать друг с другом. Использование synchronized(this)еще более неуклюже и антисоциально, так как говорит, что «никто не может ничего изменить в этом классе, пока я держу замок». Как часто вам действительно нужно это делать?

Я бы предпочел иметь более гранулированные замки; даже если вы хотите остановить все изменения (возможно, вы сериализуете объект), вы можете просто получить все блокировки, чтобы добиться того же самого, плюс это более явным образом. Когда вы используете synchronized(this), неясно, почему именно вы синхронизируете или какие могут быть побочные эффекты. Если вы используете synchronized(labelMonitor), или даже лучше labelLock.getWriteLock().lock(), становится ясно, что вы делаете и чем ограничены эффекты вашего критического раздела.


3

Краткий ответ : Вы должны понимать разницу и делать выбор в зависимости от кода.

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


Правильно ли я вас понимаю, когда вы говорите, что это зависит от вашего опыта?
eljenso

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

2

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

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

Процесс использования различных блокировок для защиты несвязанных состояний называется « чередование блокировок» или «разделение блокировок».


1

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

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

  1. какой доступ защищен этим ?
  2. действительно ли достаточно одной блокировки, кто-то не представил ошибку?

Пример:

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(...), мы имеем тупик, потому что ни один из потоков не может получить блокировку другого объекта.

В этом примере мы можем устранить взаимоблокировку, используя две блокировки: одну для коротких операций и одну для длинных.


2
Единственный способ получить тупик в этом примере - это когда BadObjectA вызывает longOperationB, минуя A myListener, и наоборот. Не невозможно, но довольно запутанно, поддерживая мои предыдущие пункты.
eljenso

1

Как уже было сказано, синхронизированный блок может использовать пользовательскую переменную в качестве объекта блокировки, когда синхронизированная функция использует только «this». И, конечно, вы можете манипулировать областями вашей функции, которые должны быть синхронизированы, и так далее.

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

Более подробное объяснение разницы вы можете найти здесь: http://www.artima.com/insidejvm/ed2/threadsynchP.html

Также использование синхронизированного блока не является хорошим из-за следующей точки зрения:

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

Для получения более подробной информации в этой области я рекомендую вам прочитать эту статью: http://java.dzone.com/articles/synchronized-considered


1

Это на самом деле просто дополняет другие ответы, но если ваше основное возражение против использования закрытых объектов для блокировки состоит в том, что он загромождает ваш класс полями, не связанными с бизнес-логикой, тогда Project Lombok должен @Synchronizedсгенерировать шаблон во время компиляции:

@Synchronized
public int foo() {
    return 0;
}

компилируется в

private final Object $lock = new Object[0];

public int foo() {
    synchronized($lock) {
        return 0;
    }
}

0

Хороший пример для использования синхронизированного (это).

// 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...
}

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

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


Любой объект может быть использован в качестве блокировки здесь. Это не должно быть this. Это может быть личное поле.
finnw

Правильно, но целью этого примера было показать, как сделать правильную синхронизацию, если мы решили использовать метод синхронизации.
Барт Прокоп

0

Это зависит от задачи, которую вы хотите выполнить, но я бы не стал ее использовать. Кроме того, проверьте, не удалось ли выполнить сохранение потока, которое вы хотите выполнить, с помощью синхронизации (это) в первую очередь? Есть также несколько хороших блокировок в API, которые могут вам помочь :)


0

Я только хочу упомянуть возможное решение для уникальных частных ссылок в атомарных частях кода без зависимостей. Вы можете использовать статический 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
}

0

Избегайте использования 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...
          }
        }
      }
    }

0

Мои два цента в 2019 году, хотя этот вопрос уже мог быть решен.

Блокировка «this» - это неплохо, если вы знаете, что делаете, но за сценой стоит блокировка «this» (что, к сожалению, позволяет синхронизированное ключевое слово в определении метода).

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

Более подробно, наоборот, вы должны знать, что вы «получаете» (или «теряете»), если вы блокируете недоступную блокировку (никто не может «украсть» вашу блокировку, вы полностью контролируете и так далее). ..).

Для меня проблема в том, что ключевое слово synchronized в сигнатуре определения метода позволяет программистам слишком легко не думать о том, что блокировать, о чем очень важно думать, если вы не хотите сталкиваться с проблемами в мульти программа

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

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

Если ваша блокировка доступна вне класса или нет, это ваше решение как программиста, исходя из того, какие функциональные возможности есть в классе. Это часть API. Например, вы не можете перейти от синхронизированного (этого) к синхронизированному (provateObjet), не рискуя нарушить изменения в коде, использующем его.

Примечание 1: я знаю, что вы можете добиться того, что синхронизируется (это) «достигает», используя явный объект блокировки и выставляя его, но я думаю, что это не нужно, если ваше поведение хорошо документировано и вы действительно знаете, что означает блокировка «this».

Примечание 2: я не согласен с аргументом, что если какой-то код случайно украл вашу блокировку, это ошибка, и вы должны ее решить. В некотором смысле это тот же аргумент, что и я могу сделать все мои методы публичными, даже если они не предназначены для публичности. Если кто-то «случайно» называет мой метод закрытым, это ошибка. Зачем включать эту аварию в первую очередь !!! Если способность украсть ваш замок - проблема для вашего класса, не позволяйте это. Так просто.


-3

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

Это не камень, это в основном вопрос хорошей практики и предотвращения ошибок.

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.