JavaRush/Java блог/Java Developer/Синхронизация потоков. Оператор synchronized в Java
Автор
Jesse Haniel
Главный архитектор программного обеспечения в Tribunal de Justiça da Paraíba

Синхронизация потоков. Оператор 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;
       }
   }

}
В принципе, ты мог до этого додуматься самостоятельно: раз объектов нет, значит механизм синхронизации должен быть как-то «зашит» в сами классы. Так оно и есть: по классам тоже можно синхронизироваться.
Комментарии (132)
  • популярные
  • новые
  • старые
Для того, чтобы оставить комментарий Вы должны авторизоваться
Сергей
Уровень 23
6 апреля, 14:54
хорошая статья
Anonymous #3336441
Уровень 44
25 октября 2023, 18:22
а если синхронизация идет по статическому полю? то получается что синхронизация будет как со статическим методом( то есть по классу)? И синхронизация по классу означает что, если у нас, допустим, два потока Т1 и Т2 от двух объектах класса, выполняться они будут по очереди? Так, да)
Afonya
Уровень 22
18 октября 2023, 13:36
почему с самого начала раздела/темы нельзя разместить эту статью? почему в начале темы/раздела нам втюхивают за деньги маразм???
Anonymous #3268884
Уровень 35
15 декабря 2023, 15:08
Это делается для того, чтобы нормальные люди могли посмеяться над теми, кто не умеет гуглить и устраивает по этому поводу истерики))
Andrey Vysotsky
Уровень 29
13 марта, 20:14
т.е процесс обучения по твоему выстроен так - сначала идут задачи без пояснения, и ты гуглишь, потом тебе дают пояснение, но оно тебе не нужно по тому что ты уже гуглил? Звучит разумно. Как и твой комментарий. Ты молодец. Мы тобой очень гордимся
Dmitry Vidonov
Уровень 29
Expert
24 сентября 2023, 10:12
Автор молодец!
Nikolas Backend Developer в Meta
3 сентября 2023, 18:23
Ребят объясните пожалуйста, пытался сам разобраться, не вышло. У меня есть класс (нить) в которой есть метод удалить элемент из ArrayList и сразу же добавить туда новый, этот метод отмечен synchronized. Этот метод выполняется в методе run(). Если я создаю два объекта этой нити и запускаю одновременно. Все равно выбрасывает исключение. Почему они не по очереди создают и удаляют этот элемент листа
Anonymous #13
Уровень 32
4 сентября 2023, 19:42
возможно синхронизацию нужно делать именно по ArrayList, а не по всему объекту, тем более, что объект будет самой нитью.
Georgius #2914078 Java Developer
18 августа 2023, 07:18
Две вещи которые мне в этой лекции очень понравились(как дополнение к основным). {1.В первом случае создаешь синхронизированный блок кода сразу же при входе в метод. Он синхронизируется по объекту this, то есть по текущему объекту. 2.Если метод, в котором содержится критически важная «многопоточная» логика, статический, синхронизация будет осуществляться по классу.}
Кристина
Уровень 31
15 июля 2023, 16:55
образовался огромный вопрос нить которая дойдет до части когда которая помечена synchronized захватит монитор, и планировщик передаст время другой нити, а другая нить не сможет зайти в этот метод и пометить это метод ожиданием, но продолжит выполнять части когда которые не synchronized, и например эта нить оборвется на выполнении части кода которая не synchronized, и в след раз когда ей будет выделено время она начнет свою работу с момента где ее оборвали или там где она метод ожидает
Alex C. Green
Уровень 17
16 июля 2023, 19:28
Обрыв нити вызовет исключение InterruptedExeption (намек на Thread.iterrupt() ). Ценность нити в том, что она целая, а оборванную нить можно просто выкинуть. Значит, работа нити продолжится с того места, где ее приостановили -- принудительно или планово.
No Name
Уровень 32
1 июля 2023, 04:11
+ статья в копилке
Ислам
Уровень 33
26 июня 2023, 13:17
Автор реально понятно объяснил, Спасибо
Happy Seal
Уровень 18
12 мая 2023, 09:03
Очень доходчиво и понятно. Спасибо!