Автор оригинала: 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 .