Навеяно статьей Взаимодействие между потоками JAVA или задачка 'Робот'
Если в двух словах - то задача про то как с помощью синхронизации заставить два потока строго по-очереди выполнять свои обязанности. И будем разбирать как её нужно правильно делать через synchronized.
А сначала - лирическое отступление про то, что же делают wait() и notify()/notifyAll(). В первую очередь нужно очень жирно подчеркнуть что это именно методы объекта на котором происходит синхронизация, а не методы потока или чего-то там еще. Открываем джавадоки и читаем описание оттуда:
notifyAll() - будит все потоки, находящиеся в состоянии [ожидания на мониторе данного объекта]. ...
wait() - при вызове без аргументов, переводит [текущий поток] в состояние waiting до тех пор пока какой-либо поток не вызовет notify() или notifyAll() на [объекте-мониторе, на котором синхронизация]. ...
Для базового понимания достаточно - теперь можем писать сам код:
и вызов:
spurious wakeups - описано в джавадоке внутри wait(). Поиск в гугле показал что если писать только под Windows то на нём их вроде как не бывает вообще, но на некоторых системах бывают. И раз об этом написано даже в джавадоке - то уж точно не фигня.
А вторая проблема(в конкретном примере пока не нужная, но мы ведь любим смотреть в будущее) - про возможность deadlock (и раз уж нам придется добавлять код для обработки spirious wakeups - то то состояние - можно будет использовать чтобы в будущем через него определять чей сейчас ход).
Итак поменяем код внутри synchronized на:
Что же мы выберем как CONDITION? Тот поток который отработал последним! Измененный код станет таким:
Итог - наш код становится лучше, и более устойчивым к возможным косякам в будущем. Еще можно упомянуть про то что System.out.println() из разных потоков в консоль тоже вполне может начать работать не совсем корректно, потому как вывод у нас вообще говоря в основном потоке программы. Но в данном случае это неважно.
Всем спасибо за внимание - вопросы и предложения по дальнейшему улучшению - в комментарии
Если в двух словах - то задача про то как с помощью синхронизации заставить два потока строго по-очереди выполнять свои обязанности. И будем разбирать как её нужно правильно делать через 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() из разных потоков в консоль тоже вполне может начать работать не совсем корректно, потому как вывод у нас вообще говоря в основном потоке программы. Но в данном случае это неважно.
Всем спасибо за внимание - вопросы и предложения по дальнейшему улучшению - в комментарии
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ