воскресенье, 1 апреля 2012 г.

Сколько клоунов влезет в Фольксваген?

Думаю, многие знают цирковой номер, в котором из крохотного Volkswagen Beetle ("Запорожец" по-немецки) вылезает вереница клоунов, которой, кажется, нет конца. Невольно задумаешься: как же их туда упаковали? Смоделируем эту ситуацию программно.

Даны два класса на 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;
        }
    }
}

2 комментария:

  1. Супер! Наглядная демонстрация вреда рекурсивных мьютексов.

    ОтветитьУдалить
  2. Хулиган! :)
    Спасибо за задачу и решение

    ОтветитьУдалить