Рубрики
Без рубрики

Плохие Практики С Синхронизацией

Автор оригинала: Anshul Bansal.

1. Обзор

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

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

2. Принцип синхронизации

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

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

Теперь давайте обсудим принципы синхронизации, основанные на определенных типах, таких как String , Boolean , Integer и Object .

3. Строковый литерал

3.1. Плохая практика

Строковые литералы объединяются в пул и часто повторно используются в Java. Поэтому не рекомендуется использовать тип String с ключевым словом synchronized для синхронизации :

public void stringBadPractice1() {
    String stringLock = "LOCK_STRING";
    synchronized (stringLock) {
        // ...
    }
}

Аналогично, если мы используем private final String literal, на него по-прежнему ссылаются из пула констант:

private final String stringLock = "LOCK_STRING";
public void stringBadPractice2() {
    synchronized (stringLock) {
        // ...
    }
}

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

private final String internedStringLock = new String("LOCK_STRING").intern();
public void stringBadPractice3() {
  synchronized (internedStringLock) {
      // ...
  }
}

Согласно Javadocs , метод intern возвращает нам каноническое представление для объекта String . Другими словами, метод intern возвращает Строку из пула – и явно добавляет ее в пул, если ее там нет, – которая имеет то же содержимое, что и эта Строка .

Таким образом, проблема синхронизации на повторно используемых объектах сохраняется и для объекта internet String .

Примечание: Все Строковые литералы и строковые постоянные выражения автоматически интернируются .

3.2. Решение

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

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

Затем мы сохраняем объект private и final , чтобы предотвратить доступ к нему любого внешнего/ненадежного кода:

private final String stringLock = new String("LOCK_STRING");
public void stringSolution() {
    synchronized (stringLock) {
        // ...
    }
}

4. Логический литерал

Тип Boolean с двумя значениями/| true и false не подходит для целей блокировки. Подобно String литералам в JVM, boolean литеральные значения также разделяют уникальные экземпляры класса Boolean .

Давайте рассмотрим пример плохого кода, синхронизирующегося с объектом Boolean lock:

private final Boolean booleanLock = Boolean.FALSE;
public void booleanBadPractice() {
    synchronized (booleanLock) {
        // ...
    }
}

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

Поэтому мы не рекомендуем использовать объекты Boolean в качестве блокировки синхронизации.

5. Коробочный примитив

5.1. Плохая практика

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

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

private int count = 0;
private final Integer intLock = count; 
public void boxedPrimitiveBadPractice() { 
    synchronized (intLock) {
        count++;
        // ... 
    } 
}

5.2. Решение

Однако, в отличие от логического литерала, решением для синхронизации на коробочном примитиве является создание нового экземпляра.

Подобно объекту String , мы должны использовать ключевое слово new , чтобы создать уникальный экземпляр объекта Integer с его собственной внутренней блокировкой и сохранить его private и final :

private int count = 0;
private final Integer intLock = new Integer(count);
public void boxedPrimitiveSolution() {
    synchronized (intLock) {
        count++;
        // ...
    }
}

6. Синхронизация классов

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

Ненадежный код может получить и бесконечно удерживать внутреннюю блокировку доступного класса. Следовательно, это может привести к тупиковой ситуации.

6.1. Плохая практика

Например, давайте создадим класс Animal с синхронизированным методом setName и методом SetOwner с синхронизированным блоком:

public class Animal {
    private String name;
    private String owner;
    
    // getters and constructors
    
    public synchronized void setName(String name) {
        this.name = name;
    }

    public void setOwner(String owner) {
        synchronized (this) {
            this.owner = owner;
        }
    }
}

Теперь давайте напишем какой-нибудь плохой код, который создает экземпляр класса Animal и синхронизируется с ним:

Animal animalObj = new Animal("Tommy", "John");
synchronized (animalObj) {
    while(true) {
        Thread.sleep(Integer.MAX_VALUE);
    }
}

Здесь пример ненадежного кода вводит неопределенную задержку, препятствующую реализации методов setName и SetOwner получить одну и ту же блокировку.

6.2. Решение

Решением для предотвращения этой уязвимости является закрытый объект блокировки .

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

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

Итак, давайте внесем необходимые изменения в наш класс Animal :

public class Animal {
    // ...

    private final Object objLock1 = new Object();
    private final Object objLock2 = new Object();

    public void setName(String name) {
        synchronized (objLock1) {
            this.name = name;
        }
    }

    public void setOwner(String owner) {
        synchronized (objLock2) {
            this.owner = owner;
        }
    }
}

Здесь, для лучшего параллелизма, мы детализировали схему блокировки, определив несколько объектов private final lock, чтобы разделить наши проблемы синхронизации для обоих методов – setName и SetOwner .

Кроме того, если метод, реализующий блок synchronized , изменяет переменную static , мы должны синхронизировать, заблокировав объект static :

private static int staticCount = 0;
private static final Object staticObjLock = new Object();
public void staticVariableSolution() {
    synchronized (staticObjLock) {
        count++;
        // ...
    }
}

7. Заключение

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

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

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

Как обычно, исходный код доступен на GitHub .