>
>
>
V6070. Unsafe synchronization on an obj…


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 как критические и относятся к типу: Ошибки при работе с многопоточными примитивами (интерфейсами запуска потоков на выполнение, синхронизации и обмена данными между потоками и пр.).

Данная диагностика классифицируется как: