Думаю, многие знают цирковой номер, в котором из крохотного Volkswagen Beetle ("Запорожец" по-немецки) вылезает вереница клоунов, которой, кажется, нет конца. Невольно задумаешься: как же их туда упаковали? Смоделируем эту ситуацию программно.
Даны два класса на Java:
0. Нельзя изменять данные Вам файлы Clowns.java и Volkswagen.java.
1. Программа должна работать с включенным SecurityManager'ом (т.е. с опцией -Djava.security.manager). Поэтому воспользоваться reflection'ом, чтобы изменить private поле, не получится.
2. Никакие другие опции JVM использовать нельзя. В частности, нельзя подсовывать JVM свой подхаченный ClassLoader, чтобы подменить стандартную реализацию java.util.HashMap
3. Должна выполниться именно указанная строка указанного класса, а не другая строка каким-то хитрым образом скопированного или унаследованного класса.
В остальном Вы вольны делать всё, что Вашей душе угодно. Можете писать сколь угодно грязный код, даже если за такое в приличном обществе принятобить канделябром отрывать руки.
Оригинал задачи на английском я нашёл здесь.
Решение
На первый взгляд, задача не имеет решения. Каждый раз при добавлении клоуна сначала проверяется размер HashSet, и только после этого вызывается add(). При этом хитрый автор задачи объявил все методы synchronized, так что создать искусственный race condition и тем самым пропихнуть несколько лишних клоунов не выйдет.
Похоже, надо каким-то образом сделать так, чтобы, пройдя проверку один раз, мы могли запихнуть несколько клоунов за раз. Другими словами, нужно встроить свой код куда-то между вызовами HashSet.size() и HashSet.add().
Каждый, кто хотя бы немного программировал на Java, знает, что HashSet и HashMap используют методы hashCode() и equals(), чтобы распределять объекты по хэш-таблице. Беглый просмотр исходников HashMap подтверждает очевидный факт: метод hashCode() вновь добавляемого ключа вызывается до того, как увеличивается счётчик size.
А ведь это ключ решению! Поскольку компилятор не накладывает никаких ограничений на реализацию hashCode(), мы можем делать в ней всё, что захотим. Например, рекурсивно вызывать добавление очередного клоуна.
Даны два класса на Java:
public class Clown { // nothing }
public class Volkswagen { private static final int CAPACITY = 5; private Set<Clown> clowns = new HashSet<Clown>(); public synchronized void add(Clown clown) { if (clowns.size() >= CAPACITY) { throw new IllegalStateException("I'm full"); } else { clowns.add(clown); } } public synchronized void done() { if (clowns.size() == 20) { // The goal is to reach this line System.out.println("I'm a Volkswagen with 20 clowns!"); } } }Нужно написать код, который в конце концов вызовет
done()
и напечатает указанную строку. Начать можно с чего-то такого (работать, конечно, этот пример не будет):public class Clowns { public static void main(String args[]) { Volkswagen vw = new Volkswagen(); for (int i=0; i<20; i++) { vw.add(new Clown()); } vw.done(); } }Несколько ограничений:
0. Нельзя изменять данные Вам файлы Clowns.java и Volkswagen.java.
1. Программа должна работать с включенным SecurityManager'ом (т.е. с опцией -Djava.security.manager). Поэтому воспользоваться reflection'ом, чтобы изменить private поле, не получится.
2. Никакие другие опции JVM использовать нельзя. В частности, нельзя подсовывать JVM свой подхаченный ClassLoader, чтобы подменить стандартную реализацию java.util.HashMap
3. Должна выполниться именно указанная строка указанного класса, а не другая строка каким-то хитрым образом скопированного или унаследованного класса.
В остальном Вы вольны делать всё, что Вашей душе угодно. Можете писать сколь угодно грязный код, даже если за такое в приличном обществе принято
Оригинал задачи на английском я нашёл здесь.
Решение
На первый взгляд, задача не имеет решения. Каждый раз при добавлении клоуна сначала проверяется размер HashSet, и только после этого вызывается add(). При этом хитрый автор задачи объявил все методы synchronized, так что создать искусственный race condition и тем самым пропихнуть несколько лишних клоунов не выйдет.
Похоже, надо каким-то образом сделать так, чтобы, пройдя проверку один раз, мы могли запихнуть несколько клоунов за раз. Другими словами, нужно встроить свой код куда-то между вызовами HashSet.size() и HashSet.add().
Каждый, кто хотя бы немного программировал на Java, знает, что HashSet и HashMap используют методы hashCode() и equals(), чтобы распределять объекты по хэш-таблице. Беглый просмотр исходников HashMap подтверждает очевидный факт: метод hashCode() вновь добавляемого ключа вызывается до того, как увеличивается счётчик size.
А ведь это ключ решению! Поскольку компилятор не накладывает никаких ограничений на реализацию hashCode(), мы можем делать в ней всё, что захотим. Например, рекурсивно вызывать добавление очередного клоуна.
public class Clowns { private static int count = 0; private static Volkswagen vw = new Volkswagen(); public static void main(String args[]) { vw.add(new RecursiveClown()); vw.done(); } private static class RecursiveClown extends Clown { @Override public int hashCode() { if (++count < 20) { vw.add(new RecursiveClown()); } return 0; } } }
Супер! Наглядная демонстрация вреда рекурсивных мьютексов.
ОтветитьУдалитьХулиган! :)
ОтветитьУдалитьСпасибо за задачу и решение