JavaRush /Java блог /Архив info.javarush /пришло время поразбирать synchronized, synchronized сам н...
billybonce
29 уровень
Москва

пришло время поразбирать synchronized, synchronized сам не поразбирается

Статья из группы Архив info.javarush
Навеяно статьей Взаимодействие между потоками 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() из разных потоков в консоль тоже вполне может начать работать не совсем корректно, потому как вывод у нас вообще говоря в основном потоке программы. Но в данном случае это неважно.

Всем спасибо за внимание - вопросы и предложения по дальнейшему улучшению - в комментарии
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ