synchronized — это один из ключевых механизмов в Java для управления многопоточностью и обеспечения безопасности при доступе к разделяемым ресурсам. Этот модификатор используется для того, чтобы ограничить доступ к критическим секциям кода, тем самым предотвращая состояния гонки (race conditions).

Как работает synchronized
Synchronized может применяться к методам или блокам кода. Когда поток входит в метод или блок, помеченный synchronized, он получает монитор (lock) на объект, связанный с этим кодом. Другие потоки не могут получить доступ к этому коду до тех пор, пока монитор не будет освобожден. Это гарантирует, что только один поток может одновременно выполнять данный код.

В этом примере, synchronized гарантирует, что методы increment и getCount не будут выполняться одновременно несколькими потоками, предотвращая некорректные обновления переменной count.

public class Counter {
    private int count = 0;
 
    public synchronized void increment() {
        count++;
    }
 
    public synchronized int getCount() {
        return count;
    }
}

Иногда удобнее синхронизировать не весь метод, а только его часть. В этом случае можно использовать блок synchronized:

public void increment() {
    synchronized (this) {
        count++;
    }
}

Особенности

  • Мониторы привязаны к объектам, а не к методам. Если вы синхронизируете метод экземпляра (instance method), то монитором является текущий объект (this). Если метод статический, то монитором выступает сам класс (ClassName.class).
  • Блокировки могут привести к взаимной блокировке (deadlock), если два потока пытаются захватить друг у друга мониторы в неправильном порядке.

Ограничения

  • Синхронизация снижает производительность, так как потоки вынуждены ожидать освобождения блокировки.
  • Недостаточная гибкость: при сложных задачах многопоточности использование synchronized может стать неэффективным, и тогда лучше рассмотреть более современные и гибкие альтернативы, такие как классы из пакета java.util.concurrent (например, ReentrantLock).

Частые проблемы и ошибки

Чрезмерная синхронизация (Over-synchronization)

Проблема: Синхронизация всего метода или слишком большого объема кода может снизить производительность программы, так как потоки вынуждены ждать доступа к синхронизированным участкам, даже если это не требуется.

Решение: Следует синхронизировать только ту часть кода, которая действительно требует защиты.

Синхронизация на неправильном объекте

Проблема: Если синхронизация происходит на локальном или временном объекте, это не защитит данные, так как каждый поток будет работать с собственной копией объекта.

public class Counter {
    private int count = 0;
 
    public void increment() {
        // Неправильная синхронизация на локальном объекте
        Object lock = new Object();
        synchronized (lock) {
            count++;
        }
    }
}

Здесь каждый поток создает собственный объект lock, и, следовательно, синхронизация не работает.

Решение: Синхронизацию нужно выполнять на общем объекте (например, this или другом общем объекте).

Синхронизация на статических и нестатических методах

Проблема: Если не понимать, как работают мониторы для статических и нестатических методов, можно случайно создать ситуацию, когда доступ к объектам будет синхронизирован некорректно.

public class StaticSyncExample {
    private static int staticCount = 0;
    private int instanceCount = 0;
 
    public static synchronized void incrementStatic() {
        staticCount++;
    }
 
    public synchronized void incrementInstance() {
        instanceCount++;
    }
}

Здесь вызов incrementStatic синхронизируется на классе StaticSyncExample.class, а incrementInstance — на конкретном объекте класса. Это может привести к недоразумениям в случаях, когда ожидается один и тот же монитор для всего кода.

Еще один пример

public class Container {
	private static final List<String> list = new ArrayList<>();
 
	synchronized void addEntry(String s) {
		list.add(s)
	}
}

Здесь у нас будет проблема, если будет 2+ объекта класса Container, так как каждый из них будет синхронизироваться на своем объекте, а переменная list у нас статическая и ее экземпляр единственный на всю программу

Решение: Нужно учитывать, что статические и нестатические методы используют разные мониторы. Чтобы синхронизировать весь класс (включая как статические, так и нестатические данные), можно использовать общий объект для синхронизации.

Пропуск синхронизации для чтения и записи

Проблема: Разрешение чтения переменных без синхронизации, тогда как их запись синхронизирована, может привести к непредсказуемым результатам, поскольку значения могут быть прочитаны в неконсистентном состоянии.

public class Counter {
    private int count = 0;
 
    public synchronized void increment() {
        count++;
    }
 
    // Неправильное чтение без синхронизации
    public int getCount() {
        return count;
    }
}

В этом случае разные потоки могут получить некорректное значение переменной count.


Мета информация

Область:: 00 Java разработка
Родитель:: Многопоточность в Java
Источник::
Создана:: 2024-10-09
Автор::

Дополнительные материалы

Дочерние заметки