Синхронизация в Java

Синхронизация нужна не «потокам вообще», а конкретному инварианту общего изменяемого состояния. Она решает три связанные задачи:

  • взаимное исключение составной операции;
  • видимость записей между потоками;
  • порядок действий по правилам happens-before.

Инструменты

ЗадачаИнструмент
защитить несколько полей одним инвариантомsynchronized или Lock
опубликовать независимое состояние/флагvolatile
атомарно изменить одно значениеatomic-классы
передать элементыBlockingQueue
ждать завершения N работCountDownLatch
встретить участников на повторяемом рубежеCyclicBarrier
ограничить параллельный доступСемафор в Java
хранить потоконезависимые ключ-значениеConcurrentHashMap
final class Account {
    private final Object lock = new Object();
    private long balance;
 
    void debit(long amount) {
        synchronized (lock) {
            if (amount > balance) throw new IllegalStateException();
            balance -= amount;
        }
    }
}

Одна и та же изменяемая сущность должна всегда защищаться одним согласованным механизмом. Если запись идет под lockA, а чтение под lockB, синхронизации между ними нет.

Lock и synchronized

synchronized автоматически освобождается при выходе и обычно проще. ReentrantLock нужен для tryLock, interruptible lock, fairness или нескольких Condition:

lock.lock();
try {
    changeState();
} finally {
    lock.unlock();
}

Что синхронизацией не является

  • sleep() и yield();
  • надежда на порядок планировщика;
  • потокобезопасная коллекция вокруг многошагового алгоритма без атомарного метода;
  • volatile для counter++;
  • объект lock, который каждый вызов создает заново.

См. Ключевое слово synchronized, Монитор в Java, Способы управления потоками java.util.concurrent.

На собеседовании

  • Что дает synchronized кроме exclusive access? Видимость через unlock/последующий lock одного монитора.
  • Можно ли защищать одно поле разными lock? Нет, между ними нет общей границы памяти.
  • Когда volatile достаточно? Когда операция — независимое чтение/запись и нет составного инварианта.
  • Почему concurrent-коллекция не делает весь алгоритм атомарным? Отдельные вызовы безопасны, их последовательность может гоняться.
  • Чем Lock опаснее? Его нужно обязательно освободить вручную.