Привет! Сегодня продолжим рассматривать особенности многопоточного программирования и поговорим о синхронизации потоков.
Если коротко: синхронизация потоков это механизм, который не дает нескольким потокам одновременно изменять общий ресурс. В Java за нее отвечает ключевое слово
synchronized: помеченный им метод или блок кода в каждый момент времени выполняет только один поток, а остальные ждут своей очереди. Работает это благодаря мьютексу, который встроен в каждый объект.
Кратко
- Синхронизация это согласование работы потоков с общим ресурсом: файлом, консолью, набором данных.
- Без синхронизации потоки перезаписывают данные друг друга, и результат становится непредсказуемым.
- Мьютекс («взаимное исключение») встроен в каждый объект Java: пока один поток держит мьютекс, другие ждут.
- Ключевое слово synchronized можно повесить на весь метод или на отдельный блок кода.
- synchronized(this) синхронизирует по текущему объекту, а для статических методов синхронизация идет по классу: synchronized(MyClass.class).
- Потоки, работающие с разными объектами, друг другу не мешают: у каждого объекта свой мьютекс.
![Синхронизация потоков в Java и ключевое слово synchronized]()
Что же такое «синхронизация»?
Вне области программирования под этим подразумевается некая настройка, позволяющая двум устройствам или программам работать совместно. Например, смартфон и компьютер можно синхронизировать с Google-аккаунтом, а личный кабинет на сайте связать с аккаунтами в социальных сетях, чтобы логиниться с их помощью.
У синхронизации потоков похожий смысл: это настройка взаимодействия потоков между собой.
В предыдущих лекциях наши потоки жили и работали обособленно друг от друга. Один что-то считал, второй спал, третий выводил что-то на консоль, но друг с другом они не взаимодействовали.
В реальных программах такие ситуации редки. Несколько потоков могут активно работать, например, с одним и тем же набором данных и что-то в нем менять. Это создает проблемы.
Представь, что несколько потоков записывают текст в одно и то же место, например в текстовый файл или консоль.
Этот файл или консоль в данном случае становится общим ресурсом. Потоки не знают о существовании друг друга, поэтому просто записывают все, что успеют за то время, которое планировщик потоков им выделит.
В недавней лекции курса у нас был пример, к чему это приведет, давай его вспомним:
![Искаженный вывод в консоль, когда несколько потоков пишут в общий ресурс без синхронизации]()
Причина кроется в том, что потоки работали с общим ресурсом, консолью, не согласовывая действия друг с другом. Если планировщик потоков выделил время Потоку-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, но они выглядели иначе:
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;
}
}
}
В принципе, ты мог до этого додуматься самостоятельно: раз объектов нет, значит механизм синхронизации должен быть как-то «зашит» в сами классы. Так оно и есть: по классам тоже можно синхронизироваться.
Способы синхронизации: сравнение
Вот все способы синхронизации, которые мы разобрали, в одной таблице.
| Способ | По чему синхронизация | Что охватывает | Когда удобно |
|---|
| synchronized-метод (нестатический) | по текущему объекту (this) | весь метод | когда синхронизировать нужно всю логику метода |
| synchronized (this) { ... } | по текущему объекту (this) | выбранный блок | когда критична только часть метода |
| synchronized (obj) { ... } | по указанному объекту obj | выбранный блок | когда нужен отдельный «замок» |
| static synchronized-метод | по классу (MyClass.class) | весь статический метод | синхронизация статической логики |
| synchronized (MyClass.class) { ... } | по классу | выбранный блок в static | тонкая настройка статической синхронизации |
Вопросы и ответы
Что такое синхронизация потоков в Java?
Это механизм, который не дает нескольким потокам одновременно изменять общий ресурс. Он гарантирует, что критическую секцию кода в каждый момент времени выполняет только один поток.
Для чего нужно ключевое слово synchronized?
Оно помечает метод или блок кода, который может выполняться только одним потоком одновременно. Поток, вошедший в такой блок, захватывает мьютекс объекта, а остальные ждут, пока он освободится.
Чем synchronized-метод отличается от synchronized-блока?
Метод, помеченный synchronized, синхронизируется целиком по текущему объекту (this). Блок synchronized(obj) синхронизирует только часть метода и позволяет выбрать, по какому объекту. Блок удобнее, когда защитить нужно лишь часть логики.
Что такое мьютекс?
Мьютекс (mutual exclusion, «взаимное исключение») это встроенный в каждый объект Java «замок». Пока мьютекс объекта занят одним потоком, другие потоки не могут войти в синхронизированный по этому объекту код.
Как синхронизировать статический метод?
У статического метода нет объекта, поэтому синхронизация идет по классу. Запись public static synchronized void m() равнозначна блоку synchronized(MyClass.class) { ... }.
Есть ли у synchronized альтернативы?
Да. Начиная с Java 5, в пакете java.util.concurrent.locks есть явные блокировки, например ReentrantLock. Они дают больше возможностей: попытку захвата без блокировки, справедливую очередь, прерываемое ожидание. synchronized проще, а Lock гибче.
Источник:
Oracle Java Tutorials: Synchronization.
Читайте также