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


Object
, а значит, есть у каждого объекта в Java.
Как работает оператор synchronized в Java
Давай познакомимся с новым ключевым словом — synchronized. Им помечается определенный кусок нашего кода. Если блок кода помечен ключевым словом synchronized, это значит, что блок может выполняться только одним потоком одновременно. Синхронизацию можно реализовать по-разному. Например, создать целый синхронизированный метод:
public synchronized void doSomething() {
//...логика метода
}
Или же написать блок кода, где синхронизация осуществляется по какому-то объекту:
public class Main {
private Object obj = new Object();
public void doSomething() {
//...какая-то логика, доступная для всех потоков
synchronized (obj) {
//логика, которая одновременно доступна только для одного потока
}
}
}
Смысл прост. Если один поток зашел внутрь блока кода, который помечен словом 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 объекта MyClass
— myClass1
и 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;
}
}
}
В принципе, ты мог до этого додуматься самостоятельно: раз объектов нет, значит механизм синхронизации должен быть как-то «зашит» в сами классы. Так оно и есть: по классам тоже можно синхронизироваться.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ