Синхронизация
Синхронизация в многопоточности — это важный аспект программирования, который помогает избежать проблем, возникающих при одновременном доступе нескольких потоков к общим ресурсам. Когда несколько потоков работают одновременно, они могут пытаться получить доступ к одним и тем же данным или ресурсам, что может привести к различным проблемам, например к состоянию гонки
Создадим класс разделяемого ресурса
public class CommonResource {
private int counter = 0;
public void increaseCounter() {
counter++;
}
public int getCounter() {
return counter;
}
}
И класс потока, который будет менять разделяемый ресурс
public class ThreadExample extends Thread {
// Разделяемый ресурс
CommonResource resource;
ThreadExample(CommonResource resource) {
this.resource = resource;
}
// Метод увеличивает счетчик на 1
@Override
public void run() {
for (var i = 0; i < 10000; i++) {
resource.increaseCounter();
}
}
}
Создадим два потока, которые параллельно будут менять разделяемый ресурс, никак не сихронизируясь
public class Example {
public static void main(String[] args) {
CommonResource resource = new CommonResource();
// Создадим два потока, которые будут менять разделяемый ресурс
ThreadExample thread1 = new ThreadExample(resource);
ThreadExample thread2 = new ThreadExample(resource);
// Запускаем потоки
thread1.start();
thread2.start();
// Дожидаемся окончания выполнения потоков
try {
thread1.join();
thread2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
// Проверяем результат
System.out.println("Size: " + resource.getCounter()); // => 17097
}
}
Как мы уже видели, результат практически никогда не будет равен ожидаемым 20000
Решением этих проблем является синхронизация — механизм, который позволяет контролировать доступ к общим ресурсам, обеспечивая, что только один поток может получить доступ к ресурсу в определенный момент времени.
В Java блокировки могут быть реализованы как на уровне объекта, так и на уровне класса, и каждая из этих стратегий имеет свои особенности и применения.
В Java для синхронизации используются различные подходы, одним из которых является использование ключевого слова synchronized
. Это ключевое слово позволяет объявить метод или блок кода как синхронизированный.
Когда метод или блок кода объявлен как синхронизированный, только один поток может выполнить этот код в любой момент времени, что предотвращает одновременный доступ к ресурсам. При вызове синхронизированного метода поток получает блокировку на объекте, к которому он принадлежит, и другие потоки, пытающиеся вызвать этот метод, будут ждать, пока блокировка не будет освобождена. Это обеспечивает согласованность данных и предотвращает возникновение ошибок, связанных с конкурентным доступом
Доработаем класс разделяемого ресурса, сделаем его метод increaseCounter()
синхронизированным
public class CommonResource {
private int counter = 0;
// Метод, у которого нужно ограничить выполнение
// Одновременно только один поток может выполнять метод increaseCounter()
public synchronized void increaseCounter() {
counter++;
}
public int getCounter() {
return counter;
}
}
Попробуем теперь запустить потоки
public class Example {
public static void main(String[] args) {
CommonResource resource = new CommonResource();
ThreadExample thread1 = new ThreadExample(resource);
ThreadExample thread2 = new ThreadExample(resource);
// Запускаем потоки
thread1.start();
thread2.start();
// Дожидаемся окончания выполнения потоков
try {
thread1.join();
thread2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
// Проверяем результат
System.out.println("Size: " + resource.getCounter()); // => 20000
}
}
Теперь результат всегда будет равен 20000
Блокировки
В Java блокировки могут быть реализованы как на уровне объекта, так и на уровне класса, и каждая из этих стратегий имеет свои особенности и применения.
Блокировки на уровне объекта
Когда используется синхронизация на уровне объекта, блокировка применяется к конкретному экземпляру класса. Это означает, что только один поток может выполнять синхронизированный нестатический метод или не статический блок кода для данного объекта в любой момент времени. Другие потоки, пытающиеся получить доступ к тем же синхронизированным методам или блокам кода этого объекта, будут заблокированы до тех пор, пока текущий поток не завершит выполнение
// Блокировка применяется ко всему методу
public synchronized void instanceMethod() {
// код, который будет выполнен только одним потоком за раз
}
public synchronized void instanceMethod() {
// Блокировка применяется только к блоку кода внутри метода
synchronized(this) {
// код, который будет выполнен только одним потоком за раз
}
}
В этом случае, если у вас есть несколько экземпляров класса, каждый экземпляр будет иметь свою собственную блокировку, и потоки, работающие с разными экземплярами, смогут выполняться параллельно.
Блокировки реализуются с помощью механизма, называемого мьютексом (от английского "mutual exclusion", что означает "взаимное исключение"). Мьютекс — это объект, который обеспечивает эксклюзивный доступ к ресурсу, позволяя только одному потоку выполнять определенный код в данный момент времени
Блокировки на уровне класса
Блокировка на уровне класса применяется к статическим методам или блокам кода, и в этом случае блокировка относится ко всему классу, а не к отдельным экземплярам. Это означает, что только один поток может выполнять синхронизированный статический метод или блок кода для данного класса в любой момент времени. Другие потоки, пытающиеся вызвать такие методы, будут заблокированы.
public static synchronized void classMethod() {
// код, который будет выполнен только одним потоком за раз для всего класса
}
Нюансы
Что будет, если поток выполнил synchronized метод с ошибкой
Если поток выполняет синхронизированный метод и возникает ошибка или исключение, JVM всегда снимает блокировку после выхода потока из метода, независимо от того, завершилось ли выполнение успешно или с ошибкой. Это означает, что другие потоки смогут получить доступ к ресурсу, как только текущий поток завершит свою работу, что предотвращает зависание программы.
Вызов из одного synchronized метода другого synchronized метода
В Java синхронизация реализована таким образом, что если один синхронизированный метод вызывает другой синхронизированный метод в том же объекте, поток, который уже захватил блокировку (замок), может войти в этот метод без необходимости повторного захвата замка.
Когда поток вызывает синхронизированный метод, он захватывает замок, связанный с объектом, к которому этот метод принадлежит. Если в этом методе происходит вызов другого синхронизированного метода того же объекта, поток не будет блокироваться, так как он уже является владельцем замка. Это позволяет избежать ситуации, когда поток застревает, ожидая сам себя
Эффективность использования synchronized
Использование ключевого слова synchronized
в Java может вносить дополнительные затраты на производительность, так как оно требует захвата и освобождения замка, что может привести к задержкам, особенно если другие потоки уже удерживают этот замок. Синхронизация ограничивает уровень параллелизма, так как только один поток может выполнять синхронизированный код в данный момент, что может создать узкие места в производительности. Поэтому важно использовать synchronized
только в тех случаях, когда это действительно необходимо для обеспечения безопасности данных и согласованности состояния.

Остались вопросы? Задайте их в разделе «Обсуждение»
Вам ответят команда поддержки Хекслета или другие студенты
Для полного доступа к курсу нужен базовый план
Базовый план откроет полный доступ ко всем курсам, упражнениям и урокам Хекслета, проектам и пожизненный доступ к теории пройденных уроков. Подписку можно отменить в любой момент.