Навеяно статьей Взаимодействие между потоками JAVA или задачка 'Робот'

Если в двух словах - то задача про то как с помощью синхронизации заставить два потока строго по-очереди выполнять свои обязанности. И будем разбирать как её нужно правильно делать через synchronized.

А сначала - лирическое отступление про то, что же делают wait() и notify()/notifyAll(). В первую очередь нужно очень жирно подчеркнуть что это именно методы объекта на котором происходит синхронизация, а не методы потока или чего-то там еще. Открываем джавадоки и читаем описание оттуда:
notifyAll() - будит все потоки, находящиеся в состоянии [ожидания на мониторе данного объекта]. ...
wait() - при вызове без аргументов, переводит [текущий поток] в состояние waiting до тех пор пока какой-либо поток не вызовет notify() или notifyAll() на [объекте-мониторе, на котором синхронизация]. ...
Для базового понимания достаточно - теперь можем писать сам код:

class Leg implements Runnable{ private final String name; private static final Object monitor = new Object() ; //монитор на котором будем синхронизироваться сделаем статически-финальным и сразу проинициализируем public Leg(String name){ this.name = name; } public void run() { try{ while (true) { //зациклим до бесконечности пока "нога не сломается" synchronized (monitor) { monitor.notifyAll(); monitor.wait(); System.out.println(name);//обязанностью каждой нити будет просто печатать своё имя в консоль } } } catch (InterruptedException e) { System.out.println("Leg error"); } } }
и вызов:
public class LegsSynchro { public static void main(String[] args) { Leg lg1 = new Leg("left"); Leg lg2 = new Leg("right"); new Thread(lg1).start(); new Thread(lg2).start(); } } В первом приближении и при не очень неудачных стечениях обстоятельств такой код работать будет. Но при неудачных у нас могут появиться две проблемы:
spurious wakeups - описано в джавадоке внутри wait(). Поиск в гугле показал что если писать только под Windows то на нём их вроде как не бывает вообще, но на некоторых системах бывают. И раз об этом написано даже в джавадоке - то уж точно не фигня.
А вторая проблема(в конкретном примере пока не нужная, но мы ведь любим смотреть в будущее) - про возможность deadlock (и раз уж нам придется добавлять код для обработки spirious wakeups - то то состояние - можно будет использовать чтобы в будущем через него определять чей сейчас ход).

Итак поменяем код внутри synchronized на:
synchronized (monitor) { monitor.notifyAll(); while (CONDITION) monitor.wait(); System.out.println(name); }

Что же мы выберем как CONDITION? Тот поток который отработал последним! Измененный код станет таким:
class Leg implements Runnable{ private final String name; private static final Object monitor = new Object() ; public static volatile Leg lastStepped; //состояние потока - последний отработавший поток public Leg(String name){ this.name = name; } public void run() { try{ while (true) { synchronized (monitor) { monitor.notifyAll(); while (lastStepped==this) monitor.wait(); //проверка условия что последний отработавший - текущий поток(и если он вдруг сразу проснулся после того как его усыпили - то усыпим его опять) lastStepped=this; //присваиваем что данный поток отработал последним System.out.println(name); } } } catch (InterruptedException e) { System.out.println("Leg error"); } } } А в мейне добавим установку начального состояния:
public class LegsSynchro { public static void main(String[] args) { Leg lg1 = new Leg("left"); Leg lg2 = new Leg("right"); Leg.lastStepped = lg1; //выбираем любую - не важно new Thread(lg1).start(); new Thread(lg2).start(); } }

Итог - наш код становится лучше, и более устойчивым к возможным косякам в будущем. Еще можно упомянуть про то что System.out.println() из разных потоков в консоль тоже вполне может начать работать не совсем корректно, потому как вывод у нас вообще говоря в основном потоке программы. Но в данном случае это неважно.

Всем спасибо за внимание - вопросы и предложения по дальнейшему улучшению - в комментарии