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;
       }
   }

}
В принципе, ты мог до этого додуматься самостоятельно: раз объектов нет, значит механизм синхронизации должен быть как-то «зашит» в сами классы. Так оно и есть: по классам тоже можно синхронизироваться.
Комментарии (106)
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ
Vlad Уровень 20, Рязань, Russian Federation
29 июня 2022
задачу конечно решил, но не путем таких умозаключений, скорее интуитивно...видимо еще рано, нужно руки набивать вместе с шишками))))
Rylero Уровень 31, Москва, Russian Federation
15 апреля 2022
Непонятно, что имеется ввиду под блокировкой участка кода и объекта - блокируется весь объект со всеми полями, или конкретный участок кода? И если у меня два разных метода с директивой synchronize, но работают они с одними и теми же полями объекта, то в таком случае мьютексы помогут если два разных потока вызовут два этих метода?
Евгений Уровень 24, Санкт-Петербург, Russian Federation
24 февраля 2022
все равно не понял этот .class надо еще чуть глубже капнуть
Александр Уровень 32, Санкт-Петербург
3 февраля 2022
Граждане, говорю вам как уже год отработавший и перешедший в мидлы - хотите понять многопоточку - посмотрите Ваню Головача. Истинно великий человек! Потом раскошельтесь на 1000 деревянных и купите курс Заура Трегулова. JAVA rush - это про задачи. Это не про теорию.
Vladimir Уровень 19, Нижний Новгород, Россия
12 октября 2021
Можно согласиться с Евгенеем, хотелось бы получить эту инфу перед задачами...так как я не понимал почему synchronized(Class.class)... Хотя если посмотреть на ситуацию по другому легло это сейчас хорошо, т.к. получил востребованные знания, в отличии от того когда тебе дали информацию и ты читая не принимаешь т.к. пока не видишь прикладного назначения
Руслан Уровень 30, Стерлитамак, Россия
22 июля 2021
Лекция норм. Я сначала читаю, потом решаю)))
Dmitry Уровень 20, Екатеринбург, Россия
28 февраля 2021
По мне-так норм статья.
Евгений Уровень 22, Москва
17 февраля 2021
"В принципе, ты мог до этого додуматься самостоятельно..." Да, что вы говорите? Как это я сам не додумался. По данной логике, я мог вообще открыть IDE, чего то начать тыкать и до всей многопоточности додуматься самостоятельно! Бестолковая статья. Дана уже после задач, когда мы реально сами до всего додумываемся, с помощью других ресурсов. Тут идет непрекращающийся уже годами холивор о том, почему теория зачастую дается после практики и о том, что приходится гуглить самим многое. Соответственно есть 2 лагеря учеников: 1. Все норм, так специально задумано, чтобы мы гуглить учились 2. Такой метод не работает. Заплатили деньги, хотим сразу готовый материал. А мне ту видится причина номер три - вот курс так бестолково слепили в начале и просто никто ничего делать не хочет. Все равно же деньги заносят студенты.
Павел Фролов Уровень 19
24 января 2021
Спасибо огромное, все бы так объясняли!!!)))
🦔 Виктор Уровень 20, Москва, Россия Expert
23 января 2021
Спасибо, статья гораздо подробнее раскрывает тему, чем соответствующая лекция, в которой надо было сразу и дать сюда ссылку. Ещё очень понравился топовый коммент от мастера Гоффа, который расставляет всё на свои места: «Проще понять логику блокировок с другой стороны. Мы можем мьютекс повесить на объект, а можем на класс. Это как бы флажок. Когда какой-нить поток заходит в синхронизированный блок, он поднимает флажок. Причём этот мьютекс НЕ ОБЯЗАТЕЛЬНО должен быть привязан к объекту, с которым мы работаем, можно синхронизироваться и по любому другому объекту. Если у нас мьютекс повешен на объект, то никакие другие потоки не смогут зайти в синхронизированные блоки (или выполнить синхронизированные методы), у которых указан мьютекс этого объекта и пока флажок поднят. Это относится ко всем синхронизированным по этому объекту блокам. То-есть блокируются не только тот блок, который флажок поднял, но и все остальные тоже. А если мьютекс прикручен к классу (точнее - он прикручивается к объекту MyClass.class), то никакой поток не сможет зайти в блок, который синхронизирован по мьютексу класса, если там поднят флажок другим потоком. Но сможет зайти в любой синхронизированный по другому мьютексу блок, если там флажок опущен. По-хорошему, между ними принципиальной разницы нет. Тут наверное еще можно добавить, что мьютекс класса и мьютексы экземпляров этого класса - это разные мьютексы». Всё получится!