Java

Часть 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) должен дождаться, чтобы также получить ключ. Достаточно простой принцип, не так ли?

По факту из сказанного следует, что нам нужен объект, который будет содержать ключ, делая подобную синхронизацию возможной. В случае, приведённом выше, мы поместили ключевое слово 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

Предыдущая статьяСинхронизация в Java. Часть 1
Следующая статьяПроверка типов в Python