Как использовать синхронизированные блоки по классам?

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

//class 1
public static Object obj = new Object();

someMethod(){
     synchronized(obj){
         //code
     }
}


//class 2
someMethod(){
     synchronized(firstClass.obj){
         //code
     }
}

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

Ответ 1

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

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

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

1) по возможности устранять изменяемое состояние; неизменяемые объекты и функции без сохранения состояния идеальны, потому что не требуется защищать состояние и не требуется блокировка.

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

3) использовать библиотеки параллелизма и фреймворки, а не блокировать собственные объекты с блокировкой Познакомьтесь с классами в java.util.concurrent. Они написаны намного лучше, чем все, что разработчик приложений может собрать вместе.

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

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

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

Ответ 2

ВАРИАНТ 1:

Более простым способом было бы создать отдельный объект (singleton), используя enum или статический внутренний класс. Затем используйте его для блокировки обоих классов, это выглядит элегантно:

// use any singleton object, at it simplest can use any unique string in double quotes
  public enum LockObj {
    INSTANCE;
  }

  public class Class1 {
    public void someMethod() {
      synchronized (LockObj.INSTANCE) {
        // some code
      }
    }
  }

  public class Class2 {
    public void someMethod() {
      synchronized (LockObj.INSTANCE) {
        // some code
      }
    }
  }

ВАРИАНТ: 2

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

     public class Class1 {
    public void someMethod() {
      synchronized ("MyUniqueString") {
        // some code
      }
    }
  }

   public class Class2 {
        public void someMethod() {
          synchronized ("MyUniqueString") {
            // some code
          }
        }
      }

Ответ 3

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

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

Другой подход заключается в синхронизации на FirstClass, например.

synchronized (FirstClass.class) {
// do what you have to do
} 

Однако каждый synchronized метод в FirstClass идентичен синхронизированному блоку выше. Другими словами, они также synchronized на одном и том же объекте. - В зависимости от контекста это может быть лучше.

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

Ответ 4

Я думаю, что вы идете не в ту сторону, используя синхронизированные блоки вообще. Начиная с Java 1.5 существует пакет java.util.concurrent, который дает вам высокий уровень контроля над проблемами синхронизации.

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

Semaphore s = new Semaphore(1);
s.acquire();
try {
   // critical section
} finally {
   s.release();
}

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

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

Если вы загляните дальше в параллельный пакет, вы найдете более конкретные классы синхронизации, такие как ReentrantReadWriteLock, который позволяет определить, что может быть много одновременных операций чтения, в то время как только операции записи фактически синхронизируются с другими операциями чтения/пишет. Вы найдете Phaser, который позволяет вам синхронизировать потоки так, что конкретные задачи будут выполняться синхронно (что-то вроде противоположности synchornized), а также множество структур данных, которые могут сделать синхронизацию ненужной вообще в определенных ситуациях.

В целом: вообще не используйте обычный synchronized, если вы точно не знаете, почему или вы не застряли с Java 1.4. Трудно читать и понимать, и, скорее всего, вы реализуете хотя бы части более высоких функций Semaphore или Lock.

Ответ 5

Я думаю, что вы хотите это сделать. У вас есть два рабочих класса, которые выполняют некоторые операции в одном и том же объекте контекста. Затем вы хотите заблокировать оба рабочих класса в объекте context.Then следующий код будет работать для вас.

public class Worker1 {

    private final Context context;

    public Worker1(Context context) {
        this.context = context;
    }

    public void someMethod(){
        synchronized (this.context){
            // do your work here
        }
    }
}

public class Worker2 {

    private final Context context;

    public Worker2(Context context) {
        this.context = context;
    }

    public void someMethod(){
        synchronized (this.context){
            // do your work here
        }
    }
}


public class Context {

    public static void main(String[] args) {
        Context context = new Context();
        Worker1 worker1 = new Worker1(context);
        Worker2 worker2 = new Worker2(context);

        worker1.someMethod();
        worker2.someMethod();
    }
}

Ответ 6

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

public class LockingSupport {
    private static final LockingSupport INSTANCE = new LockingSupport();

    private Object printLock = new Object();
    // you may have different lock
    private Object galaxyLock = new Object();

    public static LockingSupport get() {
        return INSTANCE;
    }

    public Object getPrintLock() {
        return printLock;
    }

    public Object getGalaxyLock() {
        return galaxyLock;
    }
}

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

public static void unsafeOperation() {
    Object lock = LockingSupport.get().getPrintLock();
    synchronized (lock) {
        // perform your operation
    }
}

public void unsafeOperation2() { //notice static modifier does not matter
    Object lock = LockingSupport.get().getPrintLock();
    synchronized (lock) {
        // perform your operation
    }
}

Ниже приведены несколько преимуществ:

  • Используя этот подход, вы можете использовать ссылки на методы для поиска всех мест, в которых используется совместная блокировка.
  • Вы можете написать расширенную логику, чтобы вернуть другой объект блокировки (например, на основе пакета класса вызывающего объекта, чтобы вернуть тот же объект блокировки для всех классов одного пакета, но другой объект блокировки для классов другого пакета и т.д.).
  • Вы можете постепенно обновить реализацию Lock, чтобы использовать API java.util.concurrent.locks.Lock. как показано ниже

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

public static void unsafeOperation2() {
    Lock lock = LockingSupport.get().getGalaxyLock();
    lock.lock();
    try {
        // perform your operation
    } finally {
        lock.unlock();
    }
}

Надеется, что это поможет.

Ответ 7

Прежде всего, вот проблемы с вашим текущим подходом:

  • Объект блокировки не называется lock или аналогичным. (Да... nitpick)
  • Переменная не final. Если что-то случайно (или намеренно) изменится на obj, ваша синхронизация сломается.
  • Переменная public. Это означает, что другой код может вызвать проблемы, приобретя блокировку.

Я предполагаю, что некоторые из этих эффектов лежат в основе вашей критики: "это кажется мне плохой кодировкой".

На мой взгляд, здесь есть две основные проблемы:

  • У вас есть нечеткая абстракция. Публикация объекта блокировки за пределами "класса 1" каким-либо образом (как публичная или пакетная переменная ИЛИ через геттер) представляет механизм блокировки. Этого следует избегать.

  • Использование единой "глобальной" блокировки означает, что у вас есть узкое место concurrency.

Первая проблема может быть решена путем абстрагирования блокировки. Например:

someMethod() {
     Class1.doWithLock(() -> { /* code */ });
}

где doWithLock() - статический метод, который принимает Runnable или Callable или аналогичный, а затем запускает его с соответствующей блокировкой. Реализация doWithLock() может использовать собственный private static final Object lock... или другой механизм блокировки в соответствии с его спецификацией.

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