Реализовать код для отображения "Тик" и "Так" с разницей в одну секунду, используя два потока

5. Синхронизировать вывод «Тик» и «Так» двумя потоками

Условие задачи:
Необходимо реализовать программу с двумя потоками:

  • первый поток выводит Тик;

  • второй поток выводит Так;

  • сообщения должны выводиться строго по очереди;

  • между соседними выводами должна проходить примерно одна секунда.

Ожидаемый результат:

Тик
Так
Тик
Так
Тик
Так
...

Спойлеры к решению

Подсказки
💡 Для координации потоков можно использовать общий монитор и wait() / notifyAll().
💡 Нужен флаг, определяющий, чей сейчас ход.
💡 Проверку условия перед wait() следует выполнять через while, поскольку возможны ложные пробуждения.
💡 Чтобы между Тик и Так была пауза в одну секунду, следующий поток нельзя допускать к печати раньше времени.

Решение
public class TickTock {

    private final Object lock = new Object();
    private boolean tickTurn = true;

    public void tick() throws InterruptedException {
        print("Тик", true);
    }

    public void tock() throws InterruptedException {
        print("Так", false);
    }

    private void print(String text, boolean expectedTurn)
            throws InterruptedException {

        synchronized (lock) {
            while (tickTurn != expectedTurn) {
                lock.wait();
            }

            System.out.println(text);

            // Не позволяем второму потоку напечатать сообщение
            // раньше чем через одну секунду.
            Thread.sleep(1000);

            tickTurn = !tickTurn;
            lock.notifyAll();
        }
    }

    public static void main(String[] args)
            throws InterruptedException {

        TickTock tickTock = new TickTock();

        Thread tickThread = new Thread(() -> {
            try {
                while (!Thread.currentThread().isInterrupted()) {
                    tickTock.tick();
                }
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        });

        Thread tockThread = new Thread(() -> {
            try {
                while (!Thread.currentThread().isInterrupted()) {
                    tickTock.tock();
                }
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        });

        tickThread.start();
        tockThread.start();
    }
}

Флаг:

private boolean tickTurn = true;

определяет, какой поток может выполнить печать.

Поток, для которого сейчас не его очередь, ожидает:

while (tickTurn != expectedTurn) {
    lock.wait();
}

После вывода сообщения выполняется пауза:

Thread.sleep(1000);

Затем очередь передаётся другому потоку:

tickTurn = !tickTurn;
lock.notifyAll();

while, а не if, важен при работе с wait(): после пробуждения поток должен повторно проверить условие.

В данном учебном варианте sleep() выполняется внутри synchronized намеренно: пока текущий поток ждёт одну секунду, второй не сможет напечатать следующее сообщение раньше времени.

В исходной задаче одного wait() / notify() недостаточно — без отдельного признака очереди нельзя гарантировать, что первым будет именно Тик и что сообщения всегда будут строго чередоваться.