Избегать синхронизации (это) в Java? - PullRequest
358 голосов
/ 14 января 2009

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

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

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

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

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


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

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

Ответы [ 21 ]

3 голосов
/ 14 января 2009

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

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

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

3 голосов
/ 14 января 2009

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

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

2 голосов
/ 20 ноября 2013

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

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

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

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

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

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

public int foo() {
    synchronized($lock) {
        return 0;
    }
}
1 голос
/ 16 июля 2013

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

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

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

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

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

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

1 голос
/ 20 февраля 2013

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

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

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

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

0 голосов
/ 26 февраля 2019

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

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

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

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

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

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

0 голосов
/ 14 января 2009

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

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

...