Часть 1, Часть 2

Состояние гонки

Вновь приветствую вас в теме “Синхронизация в Java”! Надеюсь, что вы прочли мою предыдущую статью.

Давайте разберёмся, что же такое состояние гонки. Это состояние проявляется, когда нам нужно обратиться к данным параллельно. Хорошо, тогда что же значит параллельное обращение к данным? Проще говоря, это означает, что два разных потока могут считывать одну и ту же переменную, поле, или даже массив, определённые внутри класса Java. Давайте возьмём популярный шаблон проектирования “Singleton” и посмотрим, как в нём проявляется такое состояние гонки.

public class Singleton{
    private static Singleton instance;
    private Singleton() {}
    public static Singleton getInstance() {
        if(instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}

Два потока пытаются выполнить этот блок кода. Представьте себе ситуацию, где поток 1 (T1) задерживается в блоке if, в это время в процесс включается T2 и в итоге завершает блок if созданием “статического экземпляра Singleton”, после чего опять запускается T1 и уничтожает только что созданный T2 экземпляр.

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

public class Singleton{
    private static Singleton instance;
    private Singleton() {}
    public static synchronized Singleton getInstance() {
        if(instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}

Значит синхронизация решит проблему? Да, именно так. Теперь давайте посмотрим её в действии на примере образов. В них мы увидим, как ключевое слово synchronized защищает методы. 

Для большей наглядности я подготовил серию рисунков. Образ человека будет представлять собой поток. Взгляните на то, как этот человек просит ключ и попадает с его помощью в метод, а затем, покидая этот метод, возвращает ключ обратно. Поэтому другой человек (поток 2) должен дождаться, чтобы также получить ключ. Достаточно простой принцип, не так ли?

Telegram-канал по теме
NOP::HumorИТ-юмор и мемы о боли разработчиков
Подписаться

По факту из сказанного следует, что нам нужен объект, который будет содержать ключ, делая подобную синхронизацию возможной. В случае, приведённом выше, мы поместили ключевое слово synchronized в public static method. А что же для преодоления блокировки в таком случае использует JVM? Объект Singleton.class. Т.е. схожим образом в случае синхронизации в нестатических методах JVM использует в качестве объекта синхронизации конкретный экземпляр, в котором Singleton.class находится.

public synchronized String getName() {
        return this.name;
    }

Давайте используем для синхронизации явный объект

Мы можем использовать для выполнения синхронизации явный объект, как это показано в блоке кода ниже. Да, достаточно только самого класса объекта. Я думаю, что вы уже знаете почему. Вместо синхронизирования метода getName() мы можем использовать синхронизированный блок внутри этого метода и передать объект key в качестве параметра ключевого слова synchronized. Помните, что это всегда будет удачным решением.

public class Organization {
    private final Object key = new Object();
    public String getName() {
        synchronized(key) {
            //делает что-нибудь
        }
    }
}

Синхронизация более чем одного метода

Предположим, что у нас есть класс Student с двумя синхронизированными методами getName() и getMarks(). Объект блокировки, используемый JVM, находится в самом объекте Student. Когда конкретный поток захочет выполнить getName(), он возьмёт этот объект блокировки, тем самым лишая другой поток возможности выполнить этот же метод одновременно с ним. Поскольку мы не объявляли явный объект в синхронизации наших методов, будет использован тот же объект key. Итак, теперь становится понятно, что для независимого выполнения этих двух методов в одно и то же время нам нужно создать в классе Student два объекта блокировки и синхронизировать эти два блока кода из 2 блокировок (2 разных объектов).

Теперь предположим, что у нас есть два экземпляра класса Student: Student1 и Student2. Синхронизирование более одного метода заблокирует объекты двумя ключами. 

Потоку, выполняющему getName(), объект Student1 не мешает выполнить getMarks() в объекте Student2.

И он не пересечётся с другим потоком, который будет затем выполнять тот же метод getName() в объекте Student2. Думаю, что приведённые рисунки достаточно наглядно это демонстрируют. 

А что, если мы хотим помешать двум потокам выполнять метод getName() одновременно во всех экземплярах класса Student?

Теперь вы понимаете, для чего нам нужен объект блокировки, который привязан ни к одному из экземпляров класса Student, а к самому классу, не так ли? В таком случае это должно быть поле Static самого класса Student. 

Теперь становится ясно, что поток, который выполняет Student1, удерживая ключ, не даст другим потокам выполнять методы в экземпляре Student2 класса Student.

ReEntrant Lock (блокировка с повторным входом)

Снова представим, что у нас есть два экземпляра класса Student с несколькими блоками synchronized.

Одни и те же ключи должны открывать одни и те же замки, согласны?

В этих двух экземплярах method1() и method3() защищены одной и той же красной блокировкой, и вам нужен именно красный ключ. То же самое касается method4() и method1(). Итак, поток, выполняющий method1 в объекте Student1, получает ключ, и в определённый момент он войдёт в синхронизированный method3 экземпляра Student2. Поскольку этот поток уже имеет правильный ключ, то он будет допущен к выполнению и другого метода. 

Когда поток удерживает блокировку, он может войти в блок, синхронизированный с этой блокировкой.

Благодарю вас за чтение!

Читайте также:


Перевод статьи Sachintha Hewawasam: Java Synchronization- part 2

Мы в Telegram — подписывайтесь
NOP::Nuances of ProgrammingСтатьи и переводы для разработчиков — каждый день
Подписаться
NOP::HumorИТ-юмор и мемы о боли разработчиков
Подписаться
NOP::Recruiter RemoteУдалённые ИТ-вакансии с вилками
Подписаться
Предыдущая статьяСинхронизация в Java. Часть 1
Следующая статьяПроверка типов в Python