User Professor Hans Noodles
Professor Hans Noodles
41 уровень

Синхронизация потоков. Оператор synchronized в Java

Статья из группы Java Developer
Привет! Сегодня продолжим рассматривать особенности многопоточного программирования и поговорим о синхронизации потоков.
Синхронизация потоков. Оператор synchronized - 1
Что же такое «синхронизация»? Вне области программирования под этим подразумевается некая настройка, позволяющая двум устройствам или программам работать совместно. Например, смартфон и компьютер можно синхронизировать с Google-аккаунтом, личный кабинет на сайте — с аккаунтами в социальных сетях, чтобы логиниться с их помощью. У синхронизации потоков похожий смысл: это настройка взаимодействия потоков между собой. В предыдущих лекциях наши потоки жили и работали обособленно друг от друга. Один что-то считал, второй спал, третий выводил что-то на консоль, но друг с другом они не взаимодействовали. В реальных программах такие ситуации редки. Несколько потоков могут активно работать, например, с одним и тем же набором данных и что-то в нем менять. Это создает проблемы. Представь, что несколько потоков записывают текст в одно и то же место — например, в текстовый файл или консоль. Этот файл или консоль в данном случае становится общим ресурсом. Потоки не знают о существовании друг друга, поэтому просто записывают все, что успеют за то время, которое планировщик потоков им выделит. В недавней лекции курса у нас был пример, к чему это приведет, давай его вспомним: Синхронизация потоков. Оператор synchronized - 2Причина кроется в том, что потоки работали с общим ресурсом, консолью, не согласовывая действия друг с другом. Если планировщик потоков выделил время Потоку-1, тот моментально пишет все в консоль. Что там уже успели или не успели написать другие потоки — неважно. Результат, как видишь, плачевный. Поэтому в многопоточном программировании ввели специальное понятие мьютекс (от англ. «mutex», «mutual exclusion» — «взаимное исключение»). Задача мьютекса — обеспечить такой механизм, чтобы доступ к объекту в определенное время был только у одного потока. Если Поток-1 захватил мьютекс объекта А, остальные потоки не получат к нему доступ, чтобы что-то в нем менять. До тех пор, пока мьютекс объекта А не освободится, остальные потоки будут вынуждены ждать. Пример из жизни: представь, что ты и еще 10 незнакомых людей участвуете в тренинге. Вам нужно поочередно высказывать идеи и что-то обсуждать. Но, поскольку друг друга вы видите впервые, чтобы постоянно не перебивать друг друга и не скатываться в гвалт, вы используете правило c «говорящим мячиком»: говорить может только один человек — тот, у кого в руках мячик. Так дискуссия получается адекватной и плодотворной. Так вот, мьютекс, по сути, и есть такой мячик. Если мьютекс объекта находится в руках одного потока, другие потоки не смогут получить доступ к работе с этим объектом. Не нужно ничего делать, чтобы создать мьютекс: он уже встроен в класс Object, а значит, есть у каждого объекта в Java.

Как работает оператор synchronized в Java

Давай познакомимся с новым ключевым словом — synchronized. Им помечается определенный кусок нашего кода. Если блок кода помечен ключевым словом synchronized, это значит, что блок может выполняться только одним потоком одновременно. Синхронизацию можно реализовать по-разному. Например, создать целый синхронизированный метод:

public synchronized void doSomething() {
  
   //...логика метода
}
Или же написать блок кода, где синхронизация осуществляется по какому-то объекту:

public class Main {

   private Object obj = new Object();

   public void doSomething() {
      
       //...какая-то логика, доступная для всех потоков

       synchronized (obj) {

           //логика, которая одновременно доступна только для одного потока
       }
   }
}
Смысл прост. Если один поток зашел внутрь блока кода, который помечен словом synchronized, он моментально захватывает мьютекс объекта, и все другие потоки, которые попытаются зайти в этот же блок или метод вынуждены ждать, пока предыдущий поток не завершит свою работу и не освободит монитор. Синхронизация потоков. Оператор synchronized - 3Кстати! В лекциях курса ты уже видел примеры synchronized, но они выглядели иначе:

public void swap()
{
   synchronized (this)
   {
       //...логика метода
   }
}
Тема для тебя новая, и путаница с синтаксисом, само собой, первое время будет. Поэтому запомни сразу, чтобы не путаться потом в способах написания. Эти два способа записи означают одно и то же:

public void swap() {

   synchronized (this)
   {
       //...логика метода
   }
}


public synchronized void swap() {
      
   }
}
В первом случае создаешь синхронизированный блок кода сразу же при входе в метод. Он синхронизируется по объекту this, то есть по текущему объекту. А во втором примере вешаешь слово synchronized на весь метод. Тут уже нет нужды явно указывать какой-то объект, по которому осуществляется синхронизация. Раз словом помечен целый метод, этот метод автоматически будет синхронизированным для всех объектов класса. Не будем углубляться в рассуждения, какой способ лучше. Пока выбирай то, что больше нравится :) Главное — помни: объявить метод синхронизированным можно только тогда, когда вся логика внутри него выполняется одним потоком одновременно. Например, в этом случае сделать метод doSomething() синхронизированным будет ошибкой:

 public class Main {

   private Object obj = new Object();

   public void doSomething() {
      
       //...какая-то логика, доступная для всех потоков

       synchronized (obj) {

           //логика, которая одновременно доступна только для одного потока
       }
   }
}
Как видишь, кусочек метода содержит логику, для которой синхронизация не обязательна. Код в нем могут выполнять несколько потоков одновременно, а все критически важные места выделены в отдельный блок synchronized. И еще один момент. Давай рассмотрим «под микроскопом» наш пример из лекции с обменом именами:

public void swap()
{
   synchronized (this)
   {
       //...логика метода
   }
}
Обрати внимание: синхронизация проводится по this. То есть по конкретному объекту MyClass. Представь, что у нас есть 2 потока (Thread-1 и Thread-2) и всего один объект MyClass myClass. В этом случае, если Thread-1 вызовет метод myClass.swap(), мьютекс объекта будет занят, и Thread-2 при попытке вызвать myClass.swap() повиснет в ожидании, когда мьютекс освободится. Если же у нас будет 2 потока и 2 объекта MyClassmyClass1 и myClass2 — на разных объектах наши потоки спокойно смогут одновременно выполнять синхронизированные методы. Первый поток выполняет:

myClass1.swap();
Второй выполняет:

myClass2.swap();
В этом случае ключевое слово synchronized внутри метода swap() не повлияет на работу программы, поскольку синхронизация осуществляется по конкретному объекту. А в последнем случае объектов у нас 2. Поэтому потоки не создают друг другу проблем. Ведь у двух объектов есть 2 разных мьютекса, и их захват не зависит друг от друга.

Особенности синхронизации в статических методах

А что делать, если нужно синхронизировать статический метод?

class MyClass {
   private static String name1 = "Оля";
   private static String name2 = "Лена";

   public static synchronized void swap() {
       String s = name1;
       name1 = name2;
       name2 = s;
   }

}
Непонятно, что будет выполнять роль мьютекса в этом случае. Ведь мы уже определились, что у каждого объекта есть мьютекс. Но проблема в том, что для вызова статического метода MyClass.swap() нам не нужны объекты: метод-то статический! И что дальше? :/ На самом деле, проблемы в этом нет. Создатели Java обо всем позаботились :) Если метод, в котором содержится критически важная «многопоточная» логика, статический, синхронизация будет осуществляться по классу. Для большей ясности, приведенный выше код можно переписать так:

class MyClass {
   private static String name1 = "Оля";
   private static String name2 = "Лена";

   public static void swap() {
      
       synchronized (MyClass.class) {
           String s = name1;
           name1 = name2;
           name2 = s;
       }
   }

}
В принципе, ты мог до этого додуматься самостоятельно: раз объектов нет, значит механизм синхронизации должен быть как-то «зашит» в сами классы. Так оно и есть: по классам тоже можно синхронизироваться.
Комментарии (92)
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ СДЕЛАТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ
Руслан Уровень 23, Стерлитамак, Россия
22 июля 2021
Лекция норм. Я сначала читаю, потом решаю)))
Dmitry Уровень 18, Екатеринбург, Россия
28 февраля 2021
По мне-так норм статья.
Евгений Уровень 22, Москва
17 февраля 2021
"В принципе, ты мог до этого додуматься самостоятельно..." Да, что вы говорите? Как это я сам не додумался. По данной логике, я мог вообще открыть IDE, чего то начать тыкать и до всей многопоточности додуматься самостоятельно! Бестолковая статья. Дана уже после задач, когда мы реально сами до всего додумываемся, с помощью других ресурсов. Тут идет непрекращающийся уже годами холивор о том, почему теория зачастую дается после практики и о том, что приходится гуглить самим многое. Соответственно есть 2 лагеря учеников: 1. Все норм, так специально задумано, чтобы мы гуглить учились 2. Такой метод не работает. Заплатили деньги, хотим сразу готовый материал. А мне ту видится причина номер три - вот курс так бестолково слепили в начале и просто никто ничего делать не хочет. Все равно же деньги заносят студенты.
Павел Фролов Уровень 19
24 января 2021
Спасибо огромное, все бы так объясняли!!!)))
🦔 Виктор Уровень 20, Москва, Россия Expert
23 января 2021
Спасибо, статья гораздо подробнее раскрывает тему, чем соответствующая лекция, в которой надо было сразу и дать сюда ссылку. Ещё очень понравился топовый коммент от мастера Гоффа, который расставляет всё на свои места: «Проще понять логику блокировок с другой стороны. Мы можем мьютекс повесить на объект, а можем на класс. Это как бы флажок. Когда какой-нить поток заходит в синхронизированный блок, он поднимает флажок. Причём этот мьютекс НЕ ОБЯЗАТЕЛЬНО должен быть привязан к объекту, с которым мы работаем, можно синхронизироваться и по любому другому объекту. Если у нас мьютекс повешен на объект, то никакие другие потоки не смогут зайти в синхронизированные блоки (или выполнить синхронизированные методы), у которых указан мьютекс этого объекта и пока флажок поднят. Это относится ко всем синхронизированным по этому объекту блокам. То-есть блокируются не только тот блок, который флажок поднял, но и все остальные тоже. А если мьютекс прикручен к классу (точнее - он прикручивается к объекту MyClass.class), то никакой поток не сможет зайти в блок, который синхронизирован по мьютексу класса, если там поднят флажок другим потоком. Но сможет зайти в любой синхронизированный по другому мьютексу блок, если там флажок опущен. По-хорошему, между ними принципиальной разницы нет. Тут наверное еще можно добавить, что мьютекс класса и мьютексы экземпляров этого класса - это разные мьютексы». Всё получится!
Deniska Уровень 20, Москва, Россия
29 ноября 2020
Жутко интересно, но ни фига не понятно!
Андрей Уровень 25, Москва, Россия Expert
25 сентября 2020
В общем synchronized объекта хотя бы одного метода объекта — это та самая вредная тётка в окне на почте, которая «Вас много, а я одна!». Её окно и она есть объект, а каждая её функция с клиентом(потоком) synchronized. Чтобы не напороть косых, она обслуживает только один поток-клиент и только в рамках одной функции(synchronized метода). При этом у этой тётки-объекта могут быть not-synchronized функции, которые могут выпоняться вне зависимости от synchronized. Например, трындеть с коллегой Люской, принимать 10 копеек от другого потока в методе яВамДолженВотЗанёс(int credit), отвечать на телефон, etc. Таких окон с тётками, объектами класса Оператор(), может быть сколь угодно реализаций. Что же касается static synchronized method(), то это аппарат выдачи чеков электронной очереди на той же почте или в банке. Он стоит ,весь такой статический, да даёт только один порядковый талон одному потоку одновременно. Как правило такой аппарат на всё заведение один на входе, что логично.
Александр Ефимов Уровень 17, Томск, Россия
30 мая 2020
Здравствуйте, 😉 Не могу понять в чём разница.... 1. Создал класс расшарил до нити. 2. Синхронизировал выполнения блока "ран". 3. Запустил два потока. А в результате всё равно печатется ответ в разнобой... Пожскажите в чём ошибка?

package Synchronized;

public class Synchronized_002 {
    public static int a = 5;

    public static void main(String[] args) throws InterruptedException {
        Sinc_1 sinc_1 = new Sinc_1();
        sinc_1.start();

        Sinc_1 sinc_2 = new Sinc_1();
        sinc_2.start();

//        Sinc_2 sinc_2 = new Sinc_2();
//        sinc_2.start();
    }

    static class Sinc_1 extends Thread {
        @Override
        public void run() {
            synchronized (this) {
                for (int i = 0; i < a; i++) {
                    System.out.println(i + " " + currentThread().getName());
                }
            }
        }

    }

//    static class Sinc_2 extends Thread {
//        @Override
//        public void run() {
//            synchronized (this) {
//                for (int i = 0; i < a; i++) {
//                    System.out.println(i + " -");
//                }
//
//            }
//        }
//    }
}

Результат: 0 Thread-0 0 Thread-1 1 Thread-0 1 Thread-1 2 Thread-0 2 Thread-1 3 Thread-0 3 Thread-1 4 Thread-0 4 Thread-1
Любовь Уровень 33, Санкт-Петербург
14 мая 2020
Вот все-таки этот момент насчет this и MyClass.class не особо поняла. В чем отличие и когда что писать нужно, может кто-нибудь объяснить?
Ernest Aydinov Уровень 35
20 декабря 2019
вопрос - что происходит при блокировке по классу? Блокируются все объекты класса или блокируются для вызова все статические методы?