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

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

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

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

11 ответов

Решение

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

private Object mutex=new Object();

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

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

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

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

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

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

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

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

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

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

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

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

private synchronized void doSomething() {

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

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

Со страницы документации оракула

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

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

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

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

Lock objects
Executors
Concurrent collections
Atomic variables
ThreadLocalRandom

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

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

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

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

private final Object lock = new Object();

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

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

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

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

Синхронизация включает 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);
                }
            }
        }
    }
}

Говоря простым языком, приватная блокировка не работает для немного более сложной многопоточной программы.

Другие вопросы по тегам