Нужно ли использовать `synchronized` вместе с `volatile` в Java?

Ответ

Эти ключевые слова решают разные задачи, и их совместное использование требуется только в специфических сценариях.

Различия:

  • volatile гарантирует видимость изменений переменной для всех потоков, но не обеспечивает атомарность составных операций (например, count++).
  • synchronized обеспечивает и видимость, и атомарность (взаимное исключение), но вводит накладные расходы.

Когда использовать вместе? Если над volatile-переменной выполняется операция «проверка-действие» (check-then-act), необходима синхронизация для атомарности.

Пример:

public class Example {
    private volatile boolean flag = false;

    // Без synchronized два потока могут одновременно инвертировать флаг
    public synchronized void toggleFlag() {
        flag = !flag; // Неатомарная операция над volatile-переменной
    }

    // Простое чтение volatile переменной — synchronized не нужен
    public boolean isFlag() {
        return flag;
    }
}

Вывод: Используйте synchronized с volatile только для атомарных операций над volatile-полем. Для простого чтения/записи достаточно volatile.

Ответ 18+ 🔞

А, ну вот, опять про эту парочку, volatile и synchronized, как два соседа по коммуналке — вроде вместе живут, но задачи у них, блядь, разные, как у унитаза и холодильника.

Смотри, суть-то проще говоря: volatile — это как крикнуть на всю квартиру «Я ПОШЁЛ ЗА ПИВОМ!». Все потоки, блядь, услышат этот крик и поймут, что переменная изменилась. Видимость обеспечивает, нихуя не спорю. Но он нихуя не скажет, что пока ты орал, тебя уже в дверях обогнал другой поток и схватил последнюю банку. Атомарности — ноль, блядь.

А вот synchronized — это уже не крик, а полный, блядь, локдаун на дверь в холодильник. Зашёл один поток, закрылся на ключ, сделал что надо — и только потом другой зайти может. И видимость там сразу наводится, и атомарность полная. Но и возни с ним, ядрёна вошь, дохуя — накладные расходы, очереди, вся эта хуйня.

Так когда же их, этих уродов, вместе пихать? А тогда, ёпта, когда у тебя над volatile-переменной не просто «прочитал-записал», а какая-нибудь хитрая жопа по типу «проверь, а потом сделай». Классика жанра — инвертирование флага.

Смотри на этот пиздопроебибенный код:

public class Example {
    private volatile boolean flag = false; // Кричим на всю квартиру про флаг

    // А вот это — ловушка, блядь! Два потока могут влететь одновременно.
    // Один прочитает false, другой тоже прочитает false, оба инвертируют — и будет у нас true, а не false, как ожидалось. Пиздец логике.
    public void toggleFlag() {
        flag = !flag; // Неатомарная операция, ёбана!
    }
}

Вот чтобы такой пиздец не случился, и нужен synchronized на эту операцию. Загоним всех в очередь, как в военкомате.

    // Теперь норм. Один зашёл, переключил, вышел — крикнул на всю квартиру (volatile сработал). Потом следующий.
    public synchronized void toggleFlag() {
        flag = !flag;
    }

    // А вот просто почитать флаг — можно и без очереди. Зашёл, глянул, ушёл. synchronized тут как корове седло.
    public boolean isFlag() {
        return flag;
    }
}

Короче, вывод, чтобы в печёнках сидело: Кидай synchronized на volatile только тогда, когда над переменной делаешь не одну операцию, а целую хуйню, которую нельзя разрывать. Для обычного «взял-положил» — volatile сам справится, не парись. А то насинхронизируешься, что производительность в тартарары, а толку — хуй с горы.