Продолжаем исследовать способы выстрелить себе в ногу на Java. Ещё одна головоломка с уже известного нам сайта.
Иногда бывает такое, что я просыпаюсь только для того, чтобы понять, что на самом-то деле я всё ещё сплю. Каждый раз, когда это случается, я чувствую себя несколько не в своей тарелке. Чтобы этого избежать, я начал считать уровни рекурсии, погружаясь в сон:
Теперь я со спокойной душой ложусь спать следующим образом:
Решение
Никакими хитростями не получится заставить класс
Дело в том, что код внутри блока
Например, класс
Иногда бывает такое, что я просыпаюсь только для того, чтобы понять, что на самом-то деле я всё ещё сплю. Каждый раз, когда это случается, я чувствую себя несколько не в своей тарелке. Чтобы этого избежать, я начал считать уровни рекурсии, погружаясь в сон:
public class Sleeper { private int level; public synchronized int enter(Dream dream) { level++; try { dream.dream(this); } finally { level--; } return level; } }Выглядит надёжно, не так ли? Погружаясь в сон, я увеличиваю счётчик. Выходя из сна, я уменьшаю его. Благодаря блоку
finally
я уверен, что не забуду уменьшить счётчик, даже если на каком-то уровне рекурсии очередной сон выбросит исключение. Поскольку метод объявлен synchronized
, я не боюсь, что в мой сон влезет какой-то другой поток.Теперь я со спокойной душой ложусь спать следующим образом:
public class Main { public static void main(String[] args) { if (new Sleeper().enter(new Dream()) != 0) { // The goal is to reach this line System.out.println("Am I still dreaming?"); } } }Есть ли ошибка в моих рассуждениях? Можно ли написать такой класс
Dream
, что он сломает мою программу, и она напечатать помеченную строку? Изменять классы Sleeper
и Main
нельзя.public class Dream { public void dream(Sleeper s) { // TODO implement me } }Действуют те же ограничения, что и в задаче про клоунов. Вкратце, нельзя использовать reflection и манипулировать байт-кодом на лету. В остальном можете писать неограниченно грязный код, при условии, конечно, что компилятор его съест.
Решение
Никакими хитростями не получится заставить класс
Sleeper
выйти из метода enter()
, не уменьшив счётчик, а потом напечатать нужную нам строку. С этой частью рассуждений всё правильно. Слабое место - утверждение, что благодаря объявлению метода synchronized
можно забыть о проблеме нескольких потоков.Дело в том, что код внутри блока
synchronized
может на какое-то время освободить монитор, не выходя из блока. Достаточно вызвать метод wait()
, и наш поток отправится спать, а другие потоки получат возможность захватить монитор и что-нибудь сломать.Например, класс
Dream
может запустить второй поток, который сначала зайдёт в сон, увеличив счётчик, а затем вернёт управление главному потоку. Осталось сделать так, чтобы второй поток никогда не вышел из метода dream()
, и дело в шляпе.public class Dream { public void dream(final Sleeper s) { Thread t = new Thread() { public void run() { s.enter(new Dream() { public void dream(Sleeper s) { s.notifyAll(); waitQuietly(s); } }); } }; t.setDaemon(true); t.start(); waitQuietly(s); } private static void waitQuietly(Object object) { try { object.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } }Чтобы избежать таких проблем, нужно прятать монитор, делая его недоступным чужому коду:
public class Sleeper { private final Object lock = new Object(); private int level; public int enter(Dream dream) { synchronized (lock) { level++; try { dream.dream(this); } finally { level--; } return level; } } }
Я следую таким принципам:
ОтветитьУдалить1. Никогда не использовать рекурсивные мьютексы. К сожалению, в Java рекурсивные мьютексы - это дефолт.
2. Не вызывать виртуальные методы под мьютексом.