Избегайте synchronized (это) в Java?


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

некоторые из приведенных причин являются:

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

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

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


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

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

18 ответов:

Я рассмотрю каждый пункт отдельно.

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

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

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

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

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

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

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

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

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

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

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

synchronized (this.getServletConfig().getServletContext()) {
   while (true) {}
}

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

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

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

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

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

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

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

небольшое введение.

потоки и совместно используемые сущности
Это возможно для нескольких потоков, чтобы получить доступ к той же сущности, например, несколько connectionThreads совместного использования одного messageQueue. Поскольку потоки выполняются одновременно, может быть вероятность переопределение одних данных другими, что может быть запутанной ситуацией.
Поэтому нам нужен какой-то способ обеспечить доступ к разделяемой сущности только по одному потоку за раз. (ПАРАЛЛЕЛИЗМ.)

синхронизированного блока
synchronized () block-это способ обеспечения одновременного доступа к совместно используемому объекту.
Во-первых, небольшая аналогия
Предположим, что в туалете есть два человека P1, P2 (потоки) умывальник (общий объект) и есть дверь (замок.)
Теперь мы хотим, чтобы один человек использовал умывальник за раз.
Подход заключается в том, чтобы запереть дверь на P1, когда дверь заперта P2 ждет, пока p1 завершит свою работу
P1 отпирает дверь
тогда только p1 может использовать умывальник.

синтаксис.

synchronized(this)
{
  SHARED_ENTITY.....
}

" это " обеспечило внутреннюю блокировку, связанную с классом (Разработчик Java разработал класс объекта таким образом, что каждый объект может работать как монитор). Выше подход работает нормально, когда есть только один общий объект и несколько потоков (1: N).
enter image description here 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;
  }

enter image description here
enter image description here

Смотрите больше на темы----> здесь

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

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

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

тогда я не вижу табу над synchronizezd(это).

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

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

  1. сделайте ваши данные неизменяемыми, если это возможно ( final переменные)
  2. если вы не можете избежать мутации общих данных в нескольких потоках, используйте высокоуровневые программные конструкции [например, granular 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()
     }
   }
 }

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

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

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

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

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

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

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

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

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

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

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

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

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

смотрите java.утиль.одновременно и java.утиль.параллельный.атомный пакеты для других программных конструкций.

Я думаю, что есть хорошее объяснение того, почему каждый из этих жизненно важных методов под вашим поясом в книге под названием Java Concurrency in Practice by Brian Goetz. Он делает один момент очень ясным-вы должны использовать один и тот же замок "везде", чтобы защитить состояние вашего объекта. Синхронизированный метод и синхронизация на объекте часто идут рука об руку. Например, вектор синхронизирует все свои методы. Если у вас есть дескриптор векторного объекта и вы собираетесь сделать "put if absent", то просто вектор синхронизация собственных индивидуальных методов не защитит вас от коррупции государства. Вам нужно синхронизировать с помощью synchronised (vectorHandle). Это приведет к тому, что одна и та же блокировка будет получена каждым потоком, который имеет дескриптор вектора и будет защищать общее состояние вектора. Это называется блокировкой на стороне клиента. Мы знаем, что на самом деле вектор синхронизируется (это) / синхронизирует все свои методы и, следовательно, синхронизация на объекте vectorHandle приведет к правильная синхронизация состояния векторных объектов. Его глупо полагать, что вы потокобезопасны только потому, что вы используете потокобезопасную коллекцию. Именно по этой причине ConcurrentHashMap явно ввел метод putIfAbsent - чтобы сделать такие операции атомарными.

в резюме

  1. синхронизация на уровне метода позволяет блокировать клиентскую сторону.
  2. если у вас есть частный объект блокировки - это делает блокировку на стороне клиента невозможной. Это нормально, если вы знайте, что ваш класс не имеет типа функциональности "put if absent".
  3. если вы разрабатываете библиотеку - то синхронизация на этом или синхронизации метода часто мудрее. Потому что вы редко можете решить, как будет использоваться ваш класс.
  4. если бы вектор использовал частный объект блокировки-было бы невозможно получить "put if absent" правильно. Клиентский код никогда не получит дескриптор частной блокировки, тем самым нарушая основное правило использования точно такой же замок для защиты своего состояния.
  5. синхронизация или синхронизации способы действительно есть проблемы, если кто - то может захватить и не отпустить. Все остальные потоки будут ждать для блокировки.
  6. так что знайте, что вы делаете, и принять тот, который является правильным.
  7. кто - то утверждал, что наличие частного объекта блокировки дает вам лучшую детализацию - например, если две операции не связаны- они могут быть защищены по-разному замками приводящ к в более лучшем объем. Но я думаю, что это запах дизайна, а не запах кода - если две операции совершенно не связаны, почему они являются частью одного и того же класса? Почему классный клуб вообще должен иметь несвязанные функциональные возможности? Может быть служебный класс? Hmmmm-некоторые util, обеспечивающие обработку строк и форматирование даты календаря через один и тот же экземпляр?? ... по крайней мере, для меня это не имеет никакого смысла!!

нет, вы не должны всегда. Однако я склонен избегать этого, когда есть несколько проблем с конкретным объектом, которые должны быть только потокобезопасными по отношению к себе. Например, у вас может быть изменяемый объект данных с полями "label" и "parent"; они должны быть потокобезопасными, но изменение одного не должно блокировать запись/чтение другого. (На практике я бы избежал этого, объявив поля изменчивыми и / или используя java.утиль.atomicfoo concurrent обертки.)

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

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

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

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

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

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

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

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

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

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

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

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

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

хороший пример для использования synchronized (this).

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

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

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

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

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

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