В критических разделах Java, на что я должен синхронизироваться?

В Java, идиоматический способ объявления критических разделов в коде выглядит следующим образом:

private void doSomething() {
  // thread-safe code
  synchronized(this) {
    // thread-unsafe code
  }
  // thread-safe code
}

Почти все блоки синхронизируются на this, но есть ли для этого конкретная причина? Существуют ли другие возможности? Существуют ли какие-либо рекомендации по тому, какой объект синхронизируется? (например, частные экземпляры Object?)

Ответ 1

Во-первых, обратите внимание, что следующие фрагменты кода идентичны.

public void foo() {
    synchronized (this) {
        // do something thread-safe
    }
}

а также:

public synchronized void foo() {
    // do something thread-safe
}

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

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

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

Vector v = getSomeGlobalVector();
synchronized (v) {
    // some thread-safe operation on the vector
}

Я предлагаю вам получить больше знаний о параллельном программировании, оно будет вам очень полезно, если вы точно знаете, что происходит за кулисами. Вы должны проверить Concurrent Programming на Java, отличную книгу на эту тему. Если вы хотите быстро погрузиться в тему, ознакомьтесь с Java Concurrency @Sun

Ответ 2

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

Особенно уродливый случай возникает, если вы решите синхронизировать с java.lang.String. Строки могут быть (и на практике почти всегда) интернированы. Это означает, что каждая строка равного содержания - в ENTIRE JVM - оказывается той же строкой за кулисами. Это означает, что если вы синхронизируете на любой String, другой (полностью разрозненный) раздел кода, который также блокирует String с тем же контентом, также заблокирует ваш код.

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

Ответ 3

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

public class Foo {
    private final Object syncObject = new Object();
    …
}

Теперь я могу использовать этот объект для синхронизации, не опасаясь, что кто-нибудь "украдет" замок.

Ответ 4

Просто чтобы подчеркнуть, что есть также ReadWriteLocks, доступные в Java, найдены как java.util.concurrent.locks.ReadWriteLock.

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

В большинстве случаев вам никогда не придется беспокоиться о проблемах concurrency, если вы просто делаете чтение. Это когда вы пишете, что беспокоитесь о параллельных обновлениях (в результате чего теряете данные) или читаете во время записи (частичные обновления), о которых вам нужно беспокоиться.

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

Ответ 5

Вы хотите синхронизировать объект, который может служить Mutex. Если текущий экземпляр (эта ссылка) подходит (например, не Singleton), вы можете его использовать, как и в Java, любой объект может служить в качестве Mutex.

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

Это зависит от среды, в которой вы работаете, и от типа системы, которую вы строите. В большинстве приложений Java EE, которые я видел, на самом деле нет реальной необходимости синхронизации...

Ответ 6

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

В первом случае использование this позволяет атомам вызывать несколько методов. Одним из примеров является PrintWriter, где вы можете вывести несколько строк (скажем, трассировку стека на консоль/регистратор) и гарантировать, что они будут отображаться вместе - в этом случае внутренняя целостность объекта синхронизации является реальной болью. Другим таким примером являются синхронизированные обертки коллекции - там вы должны синхронизировать сам объект коллекции для повторения; поскольку итерация состоит из нескольких вызовов методов, которые вы не можете полностью защитить.

В последнем случае я использую простой объект:

private Object mutex=new Object();

Однако, увидев много дампов JVM и трассировки стека, которые говорят, что блокировка является "экземпляром java.lang.Object()", я должен сказать, что использование внутреннего класса часто может быть более полезным, как предложили другие.

Во всяком случае, мои два бита стоят.

Изменить: Еще одна вещь, при синхронизации на this Я предпочитаю синхронизировать методы и сохранять методы очень гранулированными. Я считаю это более ясным и более кратким.

Ответ 7

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

Используя другую ссылку специально для блокировки, например, объявляя и инициализируя частное поле Object lock = new Object(), это то, что мне никогда не понадобилось или не использовалось. Я думаю, что это полезно только тогда, когда вам нужна внешняя синхронизация на двух или более несинхронизированных ресурсах внутри объекта, хотя я бы всегда старался реорганизовать такую ​​ситуацию в более простую форму.

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

Ответ 8

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

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

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

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

Ответ 9

Почти все блоки синхронизируются на этом, но есть ли для этого конкретная причина? Существуют ли другие возможности?

Это объявление синхронизирует весь метод.

private synchronized void doSomething() {

Это объявление синхронизировало часть блока кода вместо целого метода.

private void doSomething() {
  // thread-safe code
  synchronized(this) {
    // thread-unsafe code
  }
  // thread-safe code
}

Из документации oracle страница

Синхронизация этих методов имеет два эффекта:

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

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

Существует множество возможностей и альтернатив синхронизации. Вы можете сделать свой поток кода безопасным, используя API-интерфейс высокого уровня concurrency (доступный после выпуска JDK 1.5)

Lock objects
Executors
Concurrent collections
Atomic variables
ThreadLocalRandom

Для получения более подробной информации см. ниже вопросы SE:

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

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

Ответ 10

Лучшая практика заключается в создании объекта исключительно для обеспечения блокировки:

private final Object lock = new Object();

private void doSomething() {
  // thread-safe code
  synchronized(lock) {
    // thread-unsafe code
  }
  // thread-safe code
}

Делая это, вы уверены, что ни один вызывающий код не сможет synchronized(yourObject) ваш метод непреднамеренно synchronized(yourObject) строкой.

(Авторы @jared и @yuval-adam, которые объяснили это более подробно выше.)

Я предполагаю, что популярность использования this в учебниках пришла из раннего javadoc Sun: https://docs.oracle.com/javase/tutorial/essential/concurrency/locksync.html