Доказательство следующего кода небезопасно

Как я могу быстро доказать, что следующий класс не является потокобезопасным (поскольку он использует Lazy Initialization и не использует синхронизацию) путем написания кода? Другими словами, если я тестирую следующий класс для безопасности потоков, как я могу его терпеть?

public class LazyInitRace {
  private ExpensiveObject instance = null;

  public ExpensiveObject getInstance() {
     if (instance == null)
        instance = new ExpensiveObject();
    return instance;
  }
}

Ответ 1

Ну... Результат этого кода будет ложным, где вы ожидаете истинного.

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

public class LazyInitRace {

    public class ExpensiveObject {
        public ExpensiveObject() {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
            }
        }
    }

    private ExpensiveObject instance = null;

    public ExpensiveObject getInstance() {
        if (instance == null)
            instance = new ExpensiveObject();
        return instance;
    }

    public static void main(String[] args) {
        final LazyInitRace lazyInitRace = new LazyInitRace();

        FutureTask<ExpensiveObject> target1 = new FutureTask<ExpensiveObject>(
                new Callable<ExpensiveObject>() {

                    @Override
                    public ExpensiveObject call() throws Exception {
                        return lazyInitRace.getInstance();
                    }
                });
        new Thread(target1).start();

        FutureTask<ExpensiveObject> target2 = new FutureTask<ExpensiveObject>(
                new Callable<ExpensiveObject>() {

                    @Override
                    public ExpensiveObject call() throws Exception {
                        return lazyInitRace.getInstance();
                    }
                });
        new Thread(target2).start();

        try {
            System.out.println(target1.get() == target2.get());
        } catch (InterruptedException e) {
        } catch (ExecutionException e) {
        }
    }
}

Ответ 2

По определению условия гонки не могут быть проверены детерминистически, если только вы не контролируете планировщик потоков (чего у вас нет). Самое близкое, что вы можете сделать, это либо добавить настраиваемую задержку в методе getInstance(), либо написать код, в котором проблема может проявляться и запускаться тысячами раз в цикле.

Кстати, ничто из этого действительно не является "доказательством". Формальная проверка, но очень и очень сложно сделать даже для относительно небольших количеств кода.

Ответ 3

Можете ли вы заставить ExpensiveObject занять много времени, чтобы построить в своем тесте? Если это так, просто вызовите getInstance() дважды из двух разных потоков, за короткое время, когда первый конструктор не будет выполнен до того, как будет сделан второй вызов. В итоге вы создадите два разных экземпляра, в которых вы должны потерпеть неудачу.

Выполнение наивной двойной проверки блокировки будет сложнее, заметьте... (хотя это не безопасно без указания volatile для переменной).

Ответ 4

Это не использует код, но вот пример того, как я это докажу. Я забываю стандартный формат для диаграмм выполнения, подобных этому, но значение должно быть достаточно очевидным.

| Thread 1              | Thread 2              |
|-----------------------|-----------------------|
| **start**             |                       |
| getInstance()         |                       |
| if(instance == null)  |                       |
| new ExpensiveObject() |                       |
| **context switch ->** | **start**             |
|                       | getInstance()         |
|                       | if(instance == null)  | //instance hasn't been assigned, so this check doesn't do what you want
|                       | new ExpensiveObject() |
| **start**             | **<- context switch** |
| instance = result     |                       |
| **context switch ->** | **start**             |
|                       | instance = result     |
|                       | return instance       |
| **start**             | **<- context switch** |
| return instance       |                       |

Ответ 5

Так как это Java, вы можете использовать библиотеку thread-weaver для ввода пауз или разрывов в ваш код и управления несколькими потоками выполнения, Таким образом, вы можете получить медленный конструктор ExpensiveObject без необходимости изменять код конструктора, как и другие (правильно).

Ответ 6

Положите очень длинный расчет в конструкторе:

public ExpensiveObject()
{
    for(double i = 0.0; i < Double.MAX_VALUE; ++i)
    {
        Math.pow(2.0,i);
    }
}

Возможно, вы захотите уменьшить условие завершения до Double.MAX_VALUE/2.0 или делить на большее число, если MAX_VALUE занимает слишком много времени для вашей симпатии.

Ответ 7

Вы можете легко доказать это с помощью отладчика.

  • Напишите программу, которая вызывает getInstance() на двух отдельных потоки.
  • Установить точку останова на конструкции ExpensiveObject. Убедиться отладчик только приостанавливает thread, а не VM.
  • Затем, когда первый поток останавливается точку останова, оставьте ее приостановленной.
  • Когда второй поток останавливается, вы просто продолжаете.
  • Если вы проверите результат getInstance() для обоих потоков, они будут ссылаться на разные экземпляров.

Преимущество такого способа заключается в том, что на самом деле вам не нужен объект ExpensiveObject, любой объект фактически даст те же результаты. Вы просто используете отладчик, чтобы запланировать выполнение этой конкретной строки кода и тем самым создать детерминированный результат.

Ответ 8

Ну, это не потокобезопасно. Доказательство безопасности потока является случайным, но довольно простым:

  • Сделать конструктор ExpensiveObject полностью безопасным:

    synchronized ExpensiveObject() {...

  • Поместите в код конструктора, который проверяет, существует ли другая копия объекта - затем создайте исключение.

  • Создайте потокобезопасный метод очистки переменной "instance"

  • Поместите последовательный код getInstance/clearInstance в цикл для выполнения несколькими потоками и ожидайте исключение из (2)