V6070. Unsafe synchronization on an object.
Анализатор обнаружил синхронизацию по объекту, которая может привести к скрытым проблемам параллелизма из-за того, что синхронизированный объект может неявно использоваться в других логически несвязанных частях программы.
Проблема заключается в том, что если производить синхронизацию по:
- 'this',
- объектам целочисленных классов оберток (Byte, Short, Integer, Long),
- объекту класса обертки для логического типа (Boolean),
- объекту класса String,
то это может приводить к потенциальным тупикам и недетерминированному поведению.
Причиной этому может служить то, что вышеперечисленные объекты могут повторно использоваться в разных частях программы.
Суть проблемы синхронизации по приведенным объектам состоит в том, что к объекту, используемому для блокировки, имеется общий доступ. Такой объект может быть использован для блокировки в другом месте без ведома разработчика, использовавшего объект для блокировки в первый раз. Это, в свою очередь, и создаёт вероятность возникновения взаимоблокировки на один и тот же объект.
Приведём синтетический пример взаимоблокировки при синхронизации по 'this':
class SynchroThis
{
void doSmt()
{
synchronized(this)
{
// do smt
}
}
}
....
SynchroThis obj = new SynchroThis();
synchronized(obj)
{
Thread t = new Thread(() -> obj.doSmt());
t.start();
t.join();
}
....
В результате программа никогда не завершится, т.к. происходит deadlock по экземпляру класса SynchroThis (первая блокировка в основном потоке по 'obj', вторая - в потоке 't' по 'this').
Для того, чтобы избежать возможных взаимных блокировок, в качестве объекта блокировки стоит использовать, например, приватное поле:
class A
{
private final Object lock = new Object();
void foo()
{
synchronized(lock)
{
// do smt
}
}
}
Рассмотрим синтетический пример синхронизации по объекту типа Byte:
class FirstClass
{
private final Byte idLock;
....
public FirstClass(Byte id, ....)
{
idLock = id;
....
}
....
public void calculateFromFirst(....)
{
synchronized (idLock) // <=
{
....
}
}
}
class SecondClass
{
private final Byte idLock;
....
public SecondClass(Byte id, ....)
{
idLock = id;
....
}
....
public void calculateFromSecond(....)
{
synchronized (idLock) // <=
{
....
}
}
}
Обусловим, что поток N1 оперирует объектом класса 'FirstClass', а поток N2 - 'SecondClass'.
Теперь давайте рассмотрим сценарий:
- У объекта класса 'FirstClass' поле 'idLock' равно 100, у объекта класса 'SecondClass' тоже 100;
- Поток N1 начинает выполнять метод 'calculateFromFirst', и какое-то время выполняется;
- Поток N2 (сразу же следом) начинает выполнять метод 'calculateFromSecond'.
Итак, у нас 2 разных потока выполняют совершенно разную логику программы для разных объектов. Что же получится? А получится то, что поток N2 будет находиться в состоянии ожидания до тех пор, пока поток N1 не закончит работу в синхронизированном блоке по объекту 'idLock'. Почему же так получается?
Как и все объекты, переменные созданные с помощью классов оберток будут храниться в куче. У каждого такого объекта будет свой адрес в куче. Но есть небольшой нюанс, который нужно всегда учитывать. Целочисленные классы обертки, полученные при помощи автоупаковки, со значением в диапазоне [-128..127] кэшируются JVM. Поэтому такие обертки с одинаковыми значениями в этом диапазоне будут являться ссылками на один объект.
Так и получается в нашем случае. Синхронизация производится по одному и тому же объекту в памяти, чего вовсе и не ожидалось.
Также, помимо целочисленных классов оберток, не следует использовать для синхронизации объекты классов:
- Boolean;
- String (так как может получиться, что синхронизация будет производиться по строке, которая хранится в пуле строк).
Использование синхронизации по таким объектам небезопасно. Рекомендуется использовать вышеописанный способ с приватным полем, но если по каким-либо причинам Вам это не подходит, то создавайте объекты явно при помощи конструктора. Такой способ гарантирует, что у объектов будут разные адреса. Пример безопасного кода:
class FirstClass
{
private final Byte idLock;
....
public FirstClass(Byte id, ....)
{
idLock = new Byte(id);
....
}
....
public void calculateFromFirst(....)
{
synchronized (idLock)
{
....
}
}
}
....
Дополнительную информацию можно посмотреть здесь.
Выявляемые диагностикой ошибки классифицируются согласно ГОСТ Р 71207–2024 как критические и относятся к типу: Ошибки при работе с многопоточными примитивами (интерфейсами запуска потоков на выполнение, синхронизации и обмена данными между потоками и пр.). |
Данная диагностика классифицируется как:
|